we (web engine): Experimental web browser project to understand the limits of Claude
1//! Display list, software rasterizer, Metal GPU compositor.
2//!
3//! Walks a layout tree, generates paint commands, and rasterizes them
4//! into a BGRA pixel buffer suitable for display via CoreGraphics.
5
6use std::collections::HashMap;
7
8use we_css::values::Color;
9use we_dom::NodeId;
10use we_image::pixel::Image;
11use we_layout::{BoxType, LayoutBox, LayoutTree, TextLine};
12use we_style::computed::{BorderStyle, TextDecoration, Visibility};
13use we_text::font::Font;
14
15/// A paint command in the display list.
16#[derive(Debug)]
17pub enum PaintCommand {
18 /// Fill a rectangle with a solid color.
19 FillRect {
20 x: f32,
21 y: f32,
22 width: f32,
23 height: f32,
24 color: Color,
25 },
26 /// Draw a text fragment at a position with styling.
27 DrawGlyphs {
28 line: TextLine,
29 font_size: f32,
30 color: Color,
31 },
32 /// Draw an image at a position with given display dimensions.
33 DrawImage {
34 x: f32,
35 y: f32,
36 width: f32,
37 height: f32,
38 node_id: NodeId,
39 },
40}
41
42/// A flat list of paint commands in painter's order.
43pub type DisplayList = Vec<PaintCommand>;
44
45/// Build a display list from a layout tree.
46///
47/// Walks the tree in depth-first pre-order (painter's order):
48/// backgrounds first, then borders, then text on top.
49pub fn build_display_list(tree: &LayoutTree) -> DisplayList {
50 let mut list = DisplayList::new();
51 paint_box(&tree.root, &mut list);
52 list
53}
54
55fn paint_box(layout_box: &LayoutBox, list: &mut DisplayList) {
56 let visible = layout_box.visibility == Visibility::Visible;
57
58 if visible {
59 paint_background(layout_box, list);
60 paint_borders(layout_box, list);
61
62 // Emit image paint command for replaced elements.
63 if let Some((rw, rh)) = layout_box.replaced_size {
64 if let Some(node_id) = node_id_from_box_type(&layout_box.box_type) {
65 list.push(PaintCommand::DrawImage {
66 x: layout_box.rect.x,
67 y: layout_box.rect.y,
68 width: rw,
69 height: rh,
70 node_id,
71 });
72 }
73 }
74
75 paint_text(layout_box, list);
76 }
77
78 // Always recurse into children — they may override visibility.
79 for child in &layout_box.children {
80 paint_box(child, list);
81 }
82}
83
84/// Extract the NodeId from a BoxType, if it has one.
85fn node_id_from_box_type(box_type: &BoxType) -> Option<NodeId> {
86 match box_type {
87 BoxType::Block(id) | BoxType::Inline(id) => Some(*id),
88 BoxType::TextRun { node, .. } => Some(*node),
89 BoxType::Anonymous => None,
90 }
91}
92
93fn paint_background(layout_box: &LayoutBox, list: &mut DisplayList) {
94 let bg = layout_box.background_color;
95 // Only paint if the background is not fully transparent and the box has area.
96 if bg.a == 0 {
97 return;
98 }
99 if layout_box.rect.width > 0.0 && layout_box.rect.height > 0.0 {
100 // Background covers the padding box (content + padding), not including border.
101 list.push(PaintCommand::FillRect {
102 x: layout_box.rect.x,
103 y: layout_box.rect.y,
104 width: layout_box.rect.width,
105 height: layout_box.rect.height,
106 color: bg,
107 });
108 }
109}
110
111fn paint_borders(layout_box: &LayoutBox, list: &mut DisplayList) {
112 let b = &layout_box.border;
113 let r = &layout_box.rect;
114 let styles = &layout_box.border_styles;
115 let colors = &layout_box.border_colors;
116
117 // Border box starts at content origin minus padding and border.
118 let bx = r.x - layout_box.padding.left - b.left;
119 let by = r.y - layout_box.padding.top - b.top;
120 let bw = b.left + layout_box.padding.left + r.width + layout_box.padding.right + b.right;
121 let bh = b.top + layout_box.padding.top + r.height + layout_box.padding.bottom + b.bottom;
122
123 // Top border
124 if b.top > 0.0 && styles[0] != BorderStyle::None && styles[0] != BorderStyle::Hidden {
125 list.push(PaintCommand::FillRect {
126 x: bx,
127 y: by,
128 width: bw,
129 height: b.top,
130 color: colors[0],
131 });
132 }
133 // Right border
134 if b.right > 0.0 && styles[1] != BorderStyle::None && styles[1] != BorderStyle::Hidden {
135 list.push(PaintCommand::FillRect {
136 x: bx + bw - b.right,
137 y: by,
138 width: b.right,
139 height: bh,
140 color: colors[1],
141 });
142 }
143 // Bottom border
144 if b.bottom > 0.0 && styles[2] != BorderStyle::None && styles[2] != BorderStyle::Hidden {
145 list.push(PaintCommand::FillRect {
146 x: bx,
147 y: by + bh - b.bottom,
148 width: bw,
149 height: b.bottom,
150 color: colors[2],
151 });
152 }
153 // Left border
154 if b.left > 0.0 && styles[3] != BorderStyle::None && styles[3] != BorderStyle::Hidden {
155 list.push(PaintCommand::FillRect {
156 x: bx,
157 y: by,
158 width: b.left,
159 height: bh,
160 color: colors[3],
161 });
162 }
163}
164
165fn paint_text(layout_box: &LayoutBox, list: &mut DisplayList) {
166 for line in &layout_box.lines {
167 let color = line.color;
168 let font_size = line.font_size;
169
170 // Record index before pushing glyphs so we can insert the background
171 // before the text in painter's order.
172 let glyph_idx = list.len();
173
174 list.push(PaintCommand::DrawGlyphs {
175 line: line.clone(),
176 font_size,
177 color,
178 });
179
180 // Draw underline as a 1px line below the baseline.
181 if line.text_decoration == TextDecoration::Underline && line.width > 0.0 {
182 let baseline_y = line.y + font_size;
183 let underline_y = baseline_y + 2.0;
184 list.push(PaintCommand::FillRect {
185 x: line.x,
186 y: underline_y,
187 width: line.width,
188 height: 1.0,
189 color,
190 });
191 }
192
193 // Draw inline background if not transparent.
194 if line.background_color.a > 0 && line.width > 0.0 {
195 list.insert(
196 glyph_idx,
197 PaintCommand::FillRect {
198 x: line.x,
199 y: line.y,
200 width: line.width,
201 height: font_size * 1.2,
202 color: line.background_color,
203 },
204 );
205 }
206 }
207}
208
209/// Software renderer that paints a display list into a BGRA pixel buffer.
210pub struct Renderer {
211 width: u32,
212 height: u32,
213 /// BGRA pixel data, row-major, top-to-bottom.
214 buffer: Vec<u8>,
215}
216
217impl Renderer {
218 /// Create a new renderer with the given dimensions.
219 /// The buffer is initialized to white.
220 pub fn new(width: u32, height: u32) -> Renderer {
221 let size = (width as usize) * (height as usize) * 4;
222 let mut buffer = vec![0u8; size];
223 // Fill with white (BGRA: B=255, G=255, R=255, A=255).
224 for pixel in buffer.chunks_exact_mut(4) {
225 pixel[0] = 255; // B
226 pixel[1] = 255; // G
227 pixel[2] = 255; // R
228 pixel[3] = 255; // A
229 }
230 Renderer {
231 width,
232 height,
233 buffer,
234 }
235 }
236
237 /// Paint a layout tree into the pixel buffer.
238 pub fn paint(
239 &mut self,
240 layout_tree: &LayoutTree,
241 font: &Font,
242 images: &HashMap<NodeId, &Image>,
243 ) {
244 let display_list = build_display_list(layout_tree);
245 for cmd in &display_list {
246 match cmd {
247 PaintCommand::FillRect {
248 x,
249 y,
250 width,
251 height,
252 color,
253 } => {
254 self.fill_rect(*x, *y, *width, *height, *color);
255 }
256 PaintCommand::DrawGlyphs {
257 line,
258 font_size,
259 color,
260 } => {
261 self.draw_text_line(line, *font_size, *color, font);
262 }
263 PaintCommand::DrawImage {
264 x,
265 y,
266 width,
267 height,
268 node_id,
269 } => {
270 if let Some(image) = images.get(node_id) {
271 self.draw_image(*x, *y, *width, *height, image);
272 }
273 }
274 }
275 }
276 }
277
278 /// Get the BGRA pixel data.
279 pub fn pixels(&self) -> &[u8] {
280 &self.buffer
281 }
282
283 /// Width in pixels.
284 pub fn width(&self) -> u32 {
285 self.width
286 }
287
288 /// Height in pixels.
289 pub fn height(&self) -> u32 {
290 self.height
291 }
292
293 /// Fill a rectangle with a solid color.
294 pub fn fill_rect(&mut self, x: f32, y: f32, width: f32, height: f32, color: Color) {
295 let x0 = (x as i32).max(0) as u32;
296 let y0 = (y as i32).max(0) as u32;
297 let x1 = ((x + width) as i32).max(0).min(self.width as i32) as u32;
298 let y1 = ((y + height) as i32).max(0).min(self.height as i32) as u32;
299
300 if color.a == 255 {
301 // Fully opaque — direct write.
302 for py in y0..y1 {
303 for px in x0..x1 {
304 self.set_pixel(px, py, color);
305 }
306 }
307 } else if color.a > 0 {
308 // Semi-transparent — alpha blend.
309 let alpha = color.a as u32;
310 let inv_alpha = 255 - alpha;
311 for py in y0..y1 {
312 for px in x0..x1 {
313 let offset = ((py * self.width + px) * 4) as usize;
314 let dst_b = self.buffer[offset] as u32;
315 let dst_g = self.buffer[offset + 1] as u32;
316 let dst_r = self.buffer[offset + 2] as u32;
317 self.buffer[offset] =
318 ((color.b as u32 * alpha + dst_b * inv_alpha) / 255) as u8;
319 self.buffer[offset + 1] =
320 ((color.g as u32 * alpha + dst_g * inv_alpha) / 255) as u8;
321 self.buffer[offset + 2] =
322 ((color.r as u32 * alpha + dst_r * inv_alpha) / 255) as u8;
323 self.buffer[offset + 3] = 255;
324 }
325 }
326 }
327 }
328
329 /// Draw a line of text using the font to render glyphs.
330 fn draw_text_line(&mut self, line: &TextLine, font_size: f32, color: Color, font: &Font) {
331 let positioned = font.render_text(&line.text, font_size);
332
333 for glyph in &positioned {
334 if let Some(ref bitmap) = glyph.bitmap {
335 // Glyph origin: line position + glyph horizontal offset.
336 let gx = line.x + glyph.x + bitmap.bearing_x as f32;
337 // Baseline is at line.y + font_size (approximate: baseline at ~80% of font_size).
338 // bearing_y is distance from baseline to top of glyph (positive = above baseline).
339 let baseline_y = line.y + font_size;
340 let gy = baseline_y - bitmap.bearing_y as f32;
341
342 self.composite_glyph(gx, gy, bitmap, color);
343 }
344 }
345 }
346
347 /// Composite a grayscale glyph bitmap onto the buffer with anti-aliasing.
348 fn composite_glyph(
349 &mut self,
350 x: f32,
351 y: f32,
352 bitmap: &we_text::font::rasterizer::GlyphBitmap,
353 color: Color,
354 ) {
355 let x0 = x as i32;
356 let y0 = y as i32;
357
358 for by in 0..bitmap.height {
359 for bx in 0..bitmap.width {
360 let px = x0 + bx as i32;
361 let py = y0 + by as i32;
362
363 if px < 0 || py < 0 || px >= self.width as i32 || py >= self.height as i32 {
364 continue;
365 }
366
367 let coverage = bitmap.data[(by * bitmap.width + bx) as usize];
368 if coverage == 0 {
369 continue;
370 }
371
372 let px = px as u32;
373 let py = py as u32;
374
375 // Alpha-blend the glyph coverage with the text color onto the background.
376 let alpha = (coverage as u32 * color.a as u32) / 255;
377 if alpha == 0 {
378 continue;
379 }
380
381 let offset = ((py * self.width + px) * 4) as usize;
382 let dst_b = self.buffer[offset] as u32;
383 let dst_g = self.buffer[offset + 1] as u32;
384 let dst_r = self.buffer[offset + 2] as u32;
385
386 // Source-over compositing.
387 let inv_alpha = 255 - alpha;
388 self.buffer[offset] = ((color.b as u32 * alpha + dst_b * inv_alpha) / 255) as u8;
389 self.buffer[offset + 1] =
390 ((color.g as u32 * alpha + dst_g * inv_alpha) / 255) as u8;
391 self.buffer[offset + 2] =
392 ((color.r as u32 * alpha + dst_r * inv_alpha) / 255) as u8;
393 self.buffer[offset + 3] = 255; // Fully opaque.
394 }
395 }
396 }
397
398 /// Draw an RGBA8 image scaled to the given display dimensions.
399 ///
400 /// Uses nearest-neighbor sampling for scaling. The source image is in RGBA8
401 /// format; the buffer is BGRA. Alpha compositing is performed for semi-transparent pixels.
402 fn draw_image(&mut self, x: f32, y: f32, width: f32, height: f32, image: &Image) {
403 if image.width == 0 || image.height == 0 || width <= 0.0 || height <= 0.0 {
404 return;
405 }
406
407 let dst_x0 = (x as i32).max(0) as u32;
408 let dst_y0 = (y as i32).max(0) as u32;
409 let dst_x1 = ((x + width) as i32).max(0).min(self.width as i32) as u32;
410 let dst_y1 = ((y + height) as i32).max(0).min(self.height as i32) as u32;
411
412 let scale_x = image.width as f32 / width;
413 let scale_y = image.height as f32 / height;
414
415 for dst_y in dst_y0..dst_y1 {
416 let src_y = ((dst_y as f32 - y) * scale_y) as u32;
417 let src_y = src_y.min(image.height - 1);
418
419 for dst_x in dst_x0..dst_x1 {
420 let src_x = ((dst_x as f32 - x) * scale_x) as u32;
421 let src_x = src_x.min(image.width - 1);
422
423 let src_offset = ((src_y * image.width + src_x) * 4) as usize;
424 let r = image.data[src_offset] as u32;
425 let g = image.data[src_offset + 1] as u32;
426 let b = image.data[src_offset + 2] as u32;
427 let a = image.data[src_offset + 3] as u32;
428
429 if a == 0 {
430 continue;
431 }
432
433 let dst_offset = ((dst_y * self.width + dst_x) * 4) as usize;
434
435 if a == 255 {
436 // Fully opaque — direct write (RGBA → BGRA).
437 self.buffer[dst_offset] = b as u8;
438 self.buffer[dst_offset + 1] = g as u8;
439 self.buffer[dst_offset + 2] = r as u8;
440 self.buffer[dst_offset + 3] = 255;
441 } else {
442 // Alpha blend.
443 let inv_a = 255 - a;
444 let dst_b = self.buffer[dst_offset] as u32;
445 let dst_g = self.buffer[dst_offset + 1] as u32;
446 let dst_r = self.buffer[dst_offset + 2] as u32;
447 self.buffer[dst_offset] = ((b * a + dst_b * inv_a) / 255) as u8;
448 self.buffer[dst_offset + 1] = ((g * a + dst_g * inv_a) / 255) as u8;
449 self.buffer[dst_offset + 2] = ((r * a + dst_r * inv_a) / 255) as u8;
450 self.buffer[dst_offset + 3] = 255;
451 }
452 }
453 }
454 }
455
456 /// Set a single pixel to the given color (no blending).
457 fn set_pixel(&mut self, x: u32, y: u32, color: Color) {
458 if x >= self.width || y >= self.height {
459 return;
460 }
461 let offset = ((y * self.width + x) * 4) as usize;
462 self.buffer[offset] = color.b; // B
463 self.buffer[offset + 1] = color.g; // G
464 self.buffer[offset + 2] = color.r; // R
465 self.buffer[offset + 3] = color.a; // A
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472 use we_dom::Document;
473 use we_style::computed::{extract_stylesheets, resolve_styles};
474 use we_text::font::Font;
475
476 fn test_font() -> Font {
477 let paths = [
478 "/System/Library/Fonts/Geneva.ttf",
479 "/System/Library/Fonts/Monaco.ttf",
480 ];
481 for path in &paths {
482 let p = std::path::Path::new(path);
483 if p.exists() {
484 return Font::from_file(p).expect("failed to parse font");
485 }
486 }
487 panic!("no test font found");
488 }
489
490 fn layout_doc(doc: &Document) -> we_layout::LayoutTree {
491 let font = test_font();
492 let sheets = extract_stylesheets(doc);
493 let styled = resolve_styles(doc, &sheets).unwrap();
494 we_layout::layout(
495 &styled,
496 doc,
497 800.0,
498 600.0,
499 &font,
500 &std::collections::HashMap::new(),
501 )
502 }
503
504 #[test]
505 fn renderer_new_white_background() {
506 let r = Renderer::new(10, 10);
507 assert_eq!(r.width(), 10);
508 assert_eq!(r.height(), 10);
509 assert_eq!(r.pixels().len(), 10 * 10 * 4);
510 // All pixels should be white (BGRA: 255, 255, 255, 255).
511 for pixel in r.pixels().chunks_exact(4) {
512 assert_eq!(pixel, [255, 255, 255, 255]);
513 }
514 }
515
516 #[test]
517 fn fill_rect_basic() {
518 let mut r = Renderer::new(20, 20);
519 let red = Color::new(255, 0, 0, 255);
520 r.fill_rect(5.0, 5.0, 10.0, 10.0, red);
521
522 // Pixel at (7, 7) should be red (BGRA: 0, 0, 255, 255).
523 let offset = ((7 * 20 + 7) * 4) as usize;
524 assert_eq!(r.pixels()[offset], 0); // B
525 assert_eq!(r.pixels()[offset + 1], 0); // G
526 assert_eq!(r.pixels()[offset + 2], 255); // R
527 assert_eq!(r.pixels()[offset + 3], 255); // A
528
529 // Pixel at (0, 0) should still be white.
530 assert_eq!(r.pixels()[0], 255); // B
531 assert_eq!(r.pixels()[1], 255); // G
532 assert_eq!(r.pixels()[2], 255); // R
533 assert_eq!(r.pixels()[3], 255); // A
534 }
535
536 #[test]
537 fn fill_rect_clipping() {
538 let mut r = Renderer::new(10, 10);
539 let blue = Color::new(0, 0, 255, 255);
540 // Rect extends beyond the buffer — should not panic.
541 r.fill_rect(-5.0, -5.0, 20.0, 20.0, blue);
542
543 // All pixels should be blue (BGRA: 255, 0, 0, 255).
544 for pixel in r.pixels().chunks_exact(4) {
545 assert_eq!(pixel, [255, 0, 0, 255]);
546 }
547 }
548
549 #[test]
550 fn display_list_from_empty_layout() {
551 let doc = Document::new();
552 let font = test_font();
553 let sheets = extract_stylesheets(&doc);
554 let styled = resolve_styles(&doc, &sheets);
555 if let Some(styled) = styled {
556 let tree = we_layout::layout(
557 &styled,
558 &doc,
559 800.0,
560 600.0,
561 &font,
562 &std::collections::HashMap::new(),
563 );
564 let list = build_display_list(&tree);
565 assert!(list.len() <= 1);
566 }
567 }
568
569 #[test]
570 fn display_list_has_background_and_text() {
571 let mut doc = Document::new();
572 let root = doc.root();
573 let html = doc.create_element("html");
574 let body = doc.create_element("body");
575 let p = doc.create_element("p");
576 let text = doc.create_text("Hello world");
577 doc.append_child(root, html);
578 doc.append_child(html, body);
579 doc.append_child(body, p);
580 doc.append_child(p, text);
581
582 let tree = layout_doc(&doc);
583 let list = build_display_list(&tree);
584
585 let has_text = list
586 .iter()
587 .any(|c| matches!(c, PaintCommand::DrawGlyphs { .. }));
588
589 assert!(has_text, "should have at least one DrawGlyphs");
590 }
591
592 #[test]
593 fn paint_simple_page() {
594 let mut doc = Document::new();
595 let root = doc.root();
596 let html = doc.create_element("html");
597 let body = doc.create_element("body");
598 let p = doc.create_element("p");
599 let text = doc.create_text("Hello");
600 doc.append_child(root, html);
601 doc.append_child(html, body);
602 doc.append_child(body, p);
603 doc.append_child(p, text);
604
605 let font = test_font();
606 let tree = layout_doc(&doc);
607 let mut renderer = Renderer::new(800, 600);
608 renderer.paint(&tree, &font, &HashMap::new());
609
610 let pixels = renderer.pixels();
611
612 // The buffer should have some non-white pixels (from text rendering).
613 let has_non_white = pixels.chunks_exact(4).any(|p| p != [255, 255, 255, 255]);
614 assert!(
615 has_non_white,
616 "rendered page should have non-white pixels from text"
617 );
618 }
619
620 #[test]
621 fn bgra_format_correct() {
622 let mut r = Renderer::new(1, 1);
623 let color = Color::new(100, 150, 200, 255);
624 r.set_pixel(0, 0, color);
625 let pixels = r.pixels();
626 // BGRA format.
627 assert_eq!(pixels[0], 200); // B
628 assert_eq!(pixels[1], 150); // G
629 assert_eq!(pixels[2], 100); // R
630 assert_eq!(pixels[3], 255); // A
631 }
632
633 #[test]
634 fn paint_heading_produces_larger_glyphs() {
635 let mut doc = Document::new();
636 let root = doc.root();
637 let html = doc.create_element("html");
638 let body = doc.create_element("body");
639 let h1 = doc.create_element("h1");
640 let h1_text = doc.create_text("Big");
641 let p = doc.create_element("p");
642 let p_text = doc.create_text("Small");
643 doc.append_child(root, html);
644 doc.append_child(html, body);
645 doc.append_child(body, h1);
646 doc.append_child(h1, h1_text);
647 doc.append_child(body, p);
648 doc.append_child(p, p_text);
649
650 let tree = layout_doc(&doc);
651 let list = build_display_list(&tree);
652
653 // There should be DrawGlyphs commands with different font sizes.
654 let font_sizes: Vec<f32> = list
655 .iter()
656 .filter_map(|c| match c {
657 PaintCommand::DrawGlyphs { font_size, .. } => Some(*font_size),
658 _ => None,
659 })
660 .collect();
661
662 assert!(font_sizes.len() >= 2, "should have at least 2 text lines");
663 // h1 has font_size 32, p has font_size 16.
664 assert!(
665 font_sizes.iter().any(|&s| s > 20.0),
666 "should have a heading with large font size"
667 );
668 assert!(
669 font_sizes.iter().any(|&s| s < 20.0),
670 "should have a paragraph with normal font size"
671 );
672 }
673
674 #[test]
675 fn renderer_zero_size() {
676 let r = Renderer::new(0, 0);
677 assert_eq!(r.pixels().len(), 0);
678 }
679
680 #[test]
681 fn glyph_compositing_anti_aliased() {
682 // Render text and verify we get anti-aliased (partially transparent) pixels.
683 let mut doc = Document::new();
684 let root = doc.root();
685 let html = doc.create_element("html");
686 let body = doc.create_element("body");
687 let p = doc.create_element("p");
688 let text = doc.create_text("MMMMM");
689 doc.append_child(root, html);
690 doc.append_child(html, body);
691 doc.append_child(body, p);
692 doc.append_child(p, text);
693
694 let font = test_font();
695 let tree = layout_doc(&doc);
696 let mut renderer = Renderer::new(800, 600);
697 renderer.paint(&tree, &font, &HashMap::new());
698
699 let pixels = renderer.pixels();
700 // Find pixels that are not pure white and not pure black.
701 // These represent anti-aliased edges of glyphs.
702 let mut has_intermediate = false;
703 for pixel in pixels.chunks_exact(4) {
704 let b = pixel[0];
705 let g = pixel[1];
706 let r = pixel[2];
707 // Anti-aliased pixel: gray between white and black.
708 if r > 0 && r < 255 && r == g && g == b {
709 has_intermediate = true;
710 break;
711 }
712 }
713 assert!(
714 has_intermediate,
715 "should have anti-aliased (gray) pixels from glyph compositing"
716 );
717 }
718
719 #[test]
720 fn css_color_renders_correctly() {
721 let html_str = r#"<!DOCTYPE html>
722<html>
723<head><style>p { color: red; }</style></head>
724<body><p>Red text</p></body>
725</html>"#;
726 let doc = we_html::parse_html(html_str);
727 let font = test_font();
728 let sheets = extract_stylesheets(&doc);
729 let styled = resolve_styles(&doc, &sheets).unwrap();
730 let tree = we_layout::layout(
731 &styled,
732 &doc,
733 800.0,
734 600.0,
735 &font,
736 &std::collections::HashMap::new(),
737 );
738
739 let list = build_display_list(&tree);
740 let text_colors: Vec<&Color> = list
741 .iter()
742 .filter_map(|c| match c {
743 PaintCommand::DrawGlyphs { color, .. } => Some(color),
744 _ => None,
745 })
746 .collect();
747
748 assert!(!text_colors.is_empty());
749 // Text should be red.
750 assert_eq!(*text_colors[0], Color::rgb(255, 0, 0));
751 }
752
753 #[test]
754 fn css_background_color_renders() {
755 let html_str = r#"<!DOCTYPE html>
756<html>
757<head><style>div { background-color: yellow; }</style></head>
758<body><div>Content</div></body>
759</html>"#;
760 let doc = we_html::parse_html(html_str);
761 let font = test_font();
762 let sheets = extract_stylesheets(&doc);
763 let styled = resolve_styles(&doc, &sheets).unwrap();
764 let tree = we_layout::layout(
765 &styled,
766 &doc,
767 800.0,
768 600.0,
769 &font,
770 &std::collections::HashMap::new(),
771 );
772
773 let list = build_display_list(&tree);
774 let fill_colors: Vec<&Color> = list
775 .iter()
776 .filter_map(|c| match c {
777 PaintCommand::FillRect { color, .. } => Some(color),
778 _ => None,
779 })
780 .collect();
781
782 // Should have a yellow fill rect for the div background.
783 assert!(fill_colors.iter().any(|c| **c == Color::rgb(255, 255, 0)));
784 }
785
786 #[test]
787 fn border_rendering() {
788 let html_str = r#"<!DOCTYPE html>
789<html>
790<head><style>div { border: 2px solid red; }</style></head>
791<body><div>Bordered</div></body>
792</html>"#;
793 let doc = we_html::parse_html(html_str);
794 let font = test_font();
795 let sheets = extract_stylesheets(&doc);
796 let styled = resolve_styles(&doc, &sheets).unwrap();
797 let tree = we_layout::layout(
798 &styled,
799 &doc,
800 800.0,
801 600.0,
802 &font,
803 &std::collections::HashMap::new(),
804 );
805
806 let list = build_display_list(&tree);
807 let red_fills: Vec<_> = list
808 .iter()
809 .filter(|c| matches!(c, PaintCommand::FillRect { color, .. } if *color == Color::rgb(255, 0, 0)))
810 .collect();
811
812 // Should have 4 border fills (top, right, bottom, left).
813 assert_eq!(red_fills.len(), 4, "should have 4 border edges");
814 }
815
816 // --- Visibility painting tests ---
817
818 #[test]
819 fn visibility_hidden_not_painted() {
820 let html_str = r#"<!DOCTYPE html>
821<html><head><style>
822body { margin: 0; }
823div { visibility: hidden; background-color: red; width: 100px; height: 100px; }
824</style></head>
825<body><div>Hidden</div></body></html>"#;
826 let doc = we_html::parse_html(html_str);
827 let tree = layout_doc(&doc);
828 let list = build_display_list(&tree);
829
830 // No red background should be in the display list.
831 let red_fills = list.iter().filter(|c| {
832 matches!(c, PaintCommand::FillRect { color, .. } if *color == Color::rgb(255, 0, 0))
833 });
834 assert_eq!(red_fills.count(), 0, "hidden element should not be painted");
835
836 // No text should be rendered for the hidden element.
837 let hidden_text = list.iter().filter(
838 |c| matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text == "Hidden"),
839 );
840 assert_eq!(
841 hidden_text.count(),
842 0,
843 "hidden element text should not be painted"
844 );
845 }
846
847 #[test]
848 fn visibility_visible_child_of_hidden_parent_is_painted() {
849 let html_str = r#"<!DOCTYPE html>
850<html><head><style>
851body { margin: 0; }
852.parent { visibility: hidden; }
853.child { visibility: visible; }
854</style></head>
855<body>
856<div class="parent"><p class="child">Visible</p></div>
857</body></html>"#;
858 let doc = we_html::parse_html(html_str);
859 let tree = layout_doc(&doc);
860 let list = build_display_list(&tree);
861
862 // The visible child's text should appear in the display list.
863 let visible_text = list.iter().filter(
864 |c| matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text.contains("Visible")),
865 );
866 assert!(
867 visible_text.count() > 0,
868 "visible child of hidden parent should be painted"
869 );
870 }
871
872 #[test]
873 fn visibility_collapse_not_painted() {
874 let html_str = r#"<!DOCTYPE html>
875<html><head><style>
876body { margin: 0; }
877div { visibility: collapse; background-color: blue; width: 50px; height: 50px; }
878</style></head>
879<body><div>Collapsed</div></body></html>"#;
880 let doc = we_html::parse_html(html_str);
881 let tree = layout_doc(&doc);
882 let list = build_display_list(&tree);
883
884 let blue_fills = list.iter().filter(|c| {
885 matches!(c, PaintCommand::FillRect { color, .. } if *color == Color::rgb(0, 0, 255))
886 });
887 assert_eq!(
888 blue_fills.count(),
889 0,
890 "collapse element should not be painted"
891 );
892 }
893
894 #[test]
895 fn display_none_not_in_display_list() {
896 let html_str = r#"<!DOCTYPE html>
897<html><head><style>
898body { margin: 0; }
899.gone { display: none; background-color: green; }
900</style></head>
901<body>
902<p>Visible</p>
903<div class="gone">Gone</div>
904</body></html>"#;
905 let doc = we_html::parse_html(html_str);
906 let tree = layout_doc(&doc);
907 let list = build_display_list(&tree);
908
909 let green_fills = list.iter().filter(|c| {
910 matches!(c, PaintCommand::FillRect { color, .. } if *color == Color::rgb(0, 128, 0))
911 });
912 assert_eq!(
913 green_fills.count(),
914 0,
915 "display:none element should not be in display list"
916 );
917 }
918}