//! Display list, software rasterizer, Metal GPU compositor. //! //! Walks a layout tree, generates paint commands, and rasterizes them //! into a BGRA pixel buffer suitable for display via CoreGraphics. use we_css::values::Color; use we_layout::{LayoutBox, LayoutTree, TextLine}; use we_style::computed::{BorderStyle, TextDecoration}; use we_text::font::Font; /// A paint command in the display list. #[derive(Debug)] pub enum PaintCommand { /// Fill a rectangle with a solid color. FillRect { x: f32, y: f32, width: f32, height: f32, color: Color, }, /// Draw a text fragment at a position with styling. DrawGlyphs { line: TextLine, font_size: f32, color: Color, }, } /// A flat list of paint commands in painter's order. pub type DisplayList = Vec; /// Build a display list from a layout tree. /// /// Walks the tree in depth-first pre-order (painter's order): /// backgrounds first, then borders, then text on top. pub fn build_display_list(tree: &LayoutTree) -> DisplayList { let mut list = DisplayList::new(); paint_box(&tree.root, &mut list); list } fn paint_box(layout_box: &LayoutBox, list: &mut DisplayList) { paint_background(layout_box, list); paint_borders(layout_box, list); paint_text(layout_box, list); // Recurse into children. for child in &layout_box.children { paint_box(child, list); } } fn paint_background(layout_box: &LayoutBox, list: &mut DisplayList) { let bg = layout_box.background_color; // Only paint if the background is not fully transparent and the box has area. if bg.a == 0 { return; } if layout_box.rect.width > 0.0 && layout_box.rect.height > 0.0 { // Background covers the padding box (content + padding), not including border. list.push(PaintCommand::FillRect { x: layout_box.rect.x, y: layout_box.rect.y, width: layout_box.rect.width, height: layout_box.rect.height, color: bg, }); } } fn paint_borders(layout_box: &LayoutBox, list: &mut DisplayList) { let b = &layout_box.border; let r = &layout_box.rect; let styles = &layout_box.border_styles; let colors = &layout_box.border_colors; // Border box starts at content origin minus padding and border. let bx = r.x - layout_box.padding.left - b.left; let by = r.y - layout_box.padding.top - b.top; let bw = b.left + layout_box.padding.left + r.width + layout_box.padding.right + b.right; let bh = b.top + layout_box.padding.top + r.height + layout_box.padding.bottom + b.bottom; // Top border if b.top > 0.0 && styles[0] != BorderStyle::None && styles[0] != BorderStyle::Hidden { list.push(PaintCommand::FillRect { x: bx, y: by, width: bw, height: b.top, color: colors[0], }); } // Right border if b.right > 0.0 && styles[1] != BorderStyle::None && styles[1] != BorderStyle::Hidden { list.push(PaintCommand::FillRect { x: bx + bw - b.right, y: by, width: b.right, height: bh, color: colors[1], }); } // Bottom border if b.bottom > 0.0 && styles[2] != BorderStyle::None && styles[2] != BorderStyle::Hidden { list.push(PaintCommand::FillRect { x: bx, y: by + bh - b.bottom, width: bw, height: b.bottom, color: colors[2], }); } // Left border if b.left > 0.0 && styles[3] != BorderStyle::None && styles[3] != BorderStyle::Hidden { list.push(PaintCommand::FillRect { x: bx, y: by, width: b.left, height: bh, color: colors[3], }); } } fn paint_text(layout_box: &LayoutBox, list: &mut DisplayList) { for line in &layout_box.lines { let color = line.color; let font_size = line.font_size; // Record index before pushing glyphs so we can insert the background // before the text in painter's order. let glyph_idx = list.len(); list.push(PaintCommand::DrawGlyphs { line: line.clone(), font_size, color, }); // Draw underline as a 1px line below the baseline. if line.text_decoration == TextDecoration::Underline && line.width > 0.0 { let baseline_y = line.y + font_size; let underline_y = baseline_y + 2.0; list.push(PaintCommand::FillRect { x: line.x, y: underline_y, width: line.width, height: 1.0, color, }); } // Draw inline background if not transparent. if line.background_color.a > 0 && line.width > 0.0 { list.insert( glyph_idx, PaintCommand::FillRect { x: line.x, y: line.y, width: line.width, height: font_size * 1.2, color: line.background_color, }, ); } } } /// Software renderer that paints a display list into a BGRA pixel buffer. pub struct Renderer { width: u32, height: u32, /// BGRA pixel data, row-major, top-to-bottom. buffer: Vec, } impl Renderer { /// Create a new renderer with the given dimensions. /// The buffer is initialized to white. pub fn new(width: u32, height: u32) -> Renderer { let size = (width as usize) * (height as usize) * 4; let mut buffer = vec![0u8; size]; // Fill with white (BGRA: B=255, G=255, R=255, A=255). for pixel in buffer.chunks_exact_mut(4) { pixel[0] = 255; // B pixel[1] = 255; // G pixel[2] = 255; // R pixel[3] = 255; // A } Renderer { width, height, buffer, } } /// Paint a layout tree into the pixel buffer. pub fn paint(&mut self, layout_tree: &LayoutTree, font: &Font) { let display_list = build_display_list(layout_tree); for cmd in &display_list { match cmd { PaintCommand::FillRect { x, y, width, height, color, } => { self.fill_rect(*x, *y, *width, *height, *color); } PaintCommand::DrawGlyphs { line, font_size, color, } => { self.draw_text_line(line, *font_size, *color, font); } } } } /// Get the BGRA pixel data. pub fn pixels(&self) -> &[u8] { &self.buffer } /// Width in pixels. pub fn width(&self) -> u32 { self.width } /// Height in pixels. pub fn height(&self) -> u32 { self.height } /// Fill a rectangle with a solid color. pub fn fill_rect(&mut self, x: f32, y: f32, width: f32, height: f32, color: Color) { let x0 = (x as i32).max(0) as u32; let y0 = (y as i32).max(0) as u32; let x1 = ((x + width) as i32).max(0).min(self.width as i32) as u32; let y1 = ((y + height) as i32).max(0).min(self.height as i32) as u32; if color.a == 255 { // Fully opaque — direct write. for py in y0..y1 { for px in x0..x1 { self.set_pixel(px, py, color); } } } else if color.a > 0 { // Semi-transparent — alpha blend. let alpha = color.a as u32; let inv_alpha = 255 - alpha; for py in y0..y1 { for px in x0..x1 { let offset = ((py * self.width + px) * 4) as usize; let dst_b = self.buffer[offset] as u32; let dst_g = self.buffer[offset + 1] as u32; let dst_r = self.buffer[offset + 2] as u32; self.buffer[offset] = ((color.b as u32 * alpha + dst_b * inv_alpha) / 255) as u8; self.buffer[offset + 1] = ((color.g as u32 * alpha + dst_g * inv_alpha) / 255) as u8; self.buffer[offset + 2] = ((color.r as u32 * alpha + dst_r * inv_alpha) / 255) as u8; self.buffer[offset + 3] = 255; } } } } /// Draw a line of text using the font to render glyphs. fn draw_text_line(&mut self, line: &TextLine, font_size: f32, color: Color, font: &Font) { let positioned = font.render_text(&line.text, font_size); for glyph in &positioned { if let Some(ref bitmap) = glyph.bitmap { // Glyph origin: line position + glyph horizontal offset. let gx = line.x + glyph.x + bitmap.bearing_x as f32; // Baseline is at line.y + font_size (approximate: baseline at ~80% of font_size). // bearing_y is distance from baseline to top of glyph (positive = above baseline). let baseline_y = line.y + font_size; let gy = baseline_y - bitmap.bearing_y as f32; self.composite_glyph(gx, gy, bitmap, color); } } } /// Composite a grayscale glyph bitmap onto the buffer with anti-aliasing. fn composite_glyph( &mut self, x: f32, y: f32, bitmap: &we_text::font::rasterizer::GlyphBitmap, color: Color, ) { let x0 = x as i32; let y0 = y as i32; for by in 0..bitmap.height { for bx in 0..bitmap.width { let px = x0 + bx as i32; let py = y0 + by as i32; if px < 0 || py < 0 || px >= self.width as i32 || py >= self.height as i32 { continue; } let coverage = bitmap.data[(by * bitmap.width + bx) as usize]; if coverage == 0 { continue; } let px = px as u32; let py = py as u32; // Alpha-blend the glyph coverage with the text color onto the background. let alpha = (coverage as u32 * color.a as u32) / 255; if alpha == 0 { continue; } let offset = ((py * self.width + px) * 4) as usize; let dst_b = self.buffer[offset] as u32; let dst_g = self.buffer[offset + 1] as u32; let dst_r = self.buffer[offset + 2] as u32; // Source-over compositing. let inv_alpha = 255 - alpha; self.buffer[offset] = ((color.b as u32 * alpha + dst_b * inv_alpha) / 255) as u8; self.buffer[offset + 1] = ((color.g as u32 * alpha + dst_g * inv_alpha) / 255) as u8; self.buffer[offset + 2] = ((color.r as u32 * alpha + dst_r * inv_alpha) / 255) as u8; self.buffer[offset + 3] = 255; // Fully opaque. } } } /// Set a single pixel to the given color (no blending). fn set_pixel(&mut self, x: u32, y: u32, color: Color) { if x >= self.width || y >= self.height { return; } let offset = ((y * self.width + x) * 4) as usize; self.buffer[offset] = color.b; // B self.buffer[offset + 1] = color.g; // G self.buffer[offset + 2] = color.r; // R self.buffer[offset + 3] = color.a; // A } } #[cfg(test)] mod tests { use super::*; use we_dom::Document; use we_style::computed::{extract_stylesheets, resolve_styles}; use we_text::font::Font; fn test_font() -> Font { let paths = [ "/System/Library/Fonts/Geneva.ttf", "/System/Library/Fonts/Monaco.ttf", ]; for path in &paths { let p = std::path::Path::new(path); if p.exists() { return Font::from_file(p).expect("failed to parse font"); } } panic!("no test font found"); } fn layout_doc(doc: &Document) -> we_layout::LayoutTree { let font = test_font(); let sheets = extract_stylesheets(doc); let styled = resolve_styles(doc, &sheets).unwrap(); we_layout::layout(&styled, doc, 800.0, 600.0, &font) } #[test] fn renderer_new_white_background() { let r = Renderer::new(10, 10); assert_eq!(r.width(), 10); assert_eq!(r.height(), 10); assert_eq!(r.pixels().len(), 10 * 10 * 4); // All pixels should be white (BGRA: 255, 255, 255, 255). for pixel in r.pixels().chunks_exact(4) { assert_eq!(pixel, [255, 255, 255, 255]); } } #[test] fn fill_rect_basic() { let mut r = Renderer::new(20, 20); let red = Color::new(255, 0, 0, 255); r.fill_rect(5.0, 5.0, 10.0, 10.0, red); // Pixel at (7, 7) should be red (BGRA: 0, 0, 255, 255). let offset = ((7 * 20 + 7) * 4) as usize; assert_eq!(r.pixels()[offset], 0); // B assert_eq!(r.pixels()[offset + 1], 0); // G assert_eq!(r.pixels()[offset + 2], 255); // R assert_eq!(r.pixels()[offset + 3], 255); // A // Pixel at (0, 0) should still be white. assert_eq!(r.pixels()[0], 255); // B assert_eq!(r.pixels()[1], 255); // G assert_eq!(r.pixels()[2], 255); // R assert_eq!(r.pixels()[3], 255); // A } #[test] fn fill_rect_clipping() { let mut r = Renderer::new(10, 10); let blue = Color::new(0, 0, 255, 255); // Rect extends beyond the buffer — should not panic. r.fill_rect(-5.0, -5.0, 20.0, 20.0, blue); // All pixels should be blue (BGRA: 255, 0, 0, 255). for pixel in r.pixels().chunks_exact(4) { assert_eq!(pixel, [255, 0, 0, 255]); } } #[test] fn display_list_from_empty_layout() { let doc = Document::new(); let font = test_font(); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets); if let Some(styled) = styled { let tree = we_layout::layout(&styled, &doc, 800.0, 600.0, &font); let list = build_display_list(&tree); assert!(list.len() <= 1); } } #[test] fn display_list_has_background_and_text() { let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); let body = doc.create_element("body"); let p = doc.create_element("p"); let text = doc.create_text("Hello world"); doc.append_child(root, html); doc.append_child(html, body); doc.append_child(body, p); doc.append_child(p, text); let tree = layout_doc(&doc); let list = build_display_list(&tree); let has_text = list .iter() .any(|c| matches!(c, PaintCommand::DrawGlyphs { .. })); assert!(has_text, "should have at least one DrawGlyphs"); } #[test] fn paint_simple_page() { let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); let body = doc.create_element("body"); let p = doc.create_element("p"); let text = doc.create_text("Hello"); doc.append_child(root, html); doc.append_child(html, body); doc.append_child(body, p); doc.append_child(p, text); let font = test_font(); let tree = layout_doc(&doc); let mut renderer = Renderer::new(800, 600); renderer.paint(&tree, &font); let pixels = renderer.pixels(); // The buffer should have some non-white pixels (from text rendering). let has_non_white = pixels.chunks_exact(4).any(|p| p != [255, 255, 255, 255]); assert!( has_non_white, "rendered page should have non-white pixels from text" ); } #[test] fn bgra_format_correct() { let mut r = Renderer::new(1, 1); let color = Color::new(100, 150, 200, 255); r.set_pixel(0, 0, color); let pixels = r.pixels(); // BGRA format. assert_eq!(pixels[0], 200); // B assert_eq!(pixels[1], 150); // G assert_eq!(pixels[2], 100); // R assert_eq!(pixels[3], 255); // A } #[test] fn paint_heading_produces_larger_glyphs() { let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); let body = doc.create_element("body"); let h1 = doc.create_element("h1"); let h1_text = doc.create_text("Big"); let p = doc.create_element("p"); let p_text = doc.create_text("Small"); doc.append_child(root, html); doc.append_child(html, body); doc.append_child(body, h1); doc.append_child(h1, h1_text); doc.append_child(body, p); doc.append_child(p, p_text); let tree = layout_doc(&doc); let list = build_display_list(&tree); // There should be DrawGlyphs commands with different font sizes. let font_sizes: Vec = list .iter() .filter_map(|c| match c { PaintCommand::DrawGlyphs { font_size, .. } => Some(*font_size), _ => None, }) .collect(); assert!(font_sizes.len() >= 2, "should have at least 2 text lines"); // h1 has font_size 32, p has font_size 16. assert!( font_sizes.iter().any(|&s| s > 20.0), "should have a heading with large font size" ); assert!( font_sizes.iter().any(|&s| s < 20.0), "should have a paragraph with normal font size" ); } #[test] fn renderer_zero_size() { let r = Renderer::new(0, 0); assert_eq!(r.pixels().len(), 0); } #[test] fn glyph_compositing_anti_aliased() { // Render text and verify we get anti-aliased (partially transparent) pixels. let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); let body = doc.create_element("body"); let p = doc.create_element("p"); let text = doc.create_text("MMMMM"); doc.append_child(root, html); doc.append_child(html, body); doc.append_child(body, p); doc.append_child(p, text); let font = test_font(); let tree = layout_doc(&doc); let mut renderer = Renderer::new(800, 600); renderer.paint(&tree, &font); let pixels = renderer.pixels(); // Find pixels that are not pure white and not pure black. // These represent anti-aliased edges of glyphs. let mut has_intermediate = false; for pixel in pixels.chunks_exact(4) { let b = pixel[0]; let g = pixel[1]; let r = pixel[2]; // Anti-aliased pixel: gray between white and black. if r > 0 && r < 255 && r == g && g == b { has_intermediate = true; break; } } assert!( has_intermediate, "should have anti-aliased (gray) pixels from glyph compositing" ); } #[test] fn css_color_renders_correctly() { let html_str = r#"

Red text

"#; let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets).unwrap(); let tree = we_layout::layout(&styled, &doc, 800.0, 600.0, &font); let list = build_display_list(&tree); let text_colors: Vec<&Color> = list .iter() .filter_map(|c| match c { PaintCommand::DrawGlyphs { color, .. } => Some(color), _ => None, }) .collect(); assert!(!text_colors.is_empty()); // Text should be red. assert_eq!(*text_colors[0], Color::rgb(255, 0, 0)); } #[test] fn css_background_color_renders() { let html_str = r#"
Content
"#; let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets).unwrap(); let tree = we_layout::layout(&styled, &doc, 800.0, 600.0, &font); let list = build_display_list(&tree); let fill_colors: Vec<&Color> = list .iter() .filter_map(|c| match c { PaintCommand::FillRect { color, .. } => Some(color), _ => None, }) .collect(); // Should have a yellow fill rect for the div background. assert!(fill_colors.iter().any(|c| **c == Color::rgb(255, 255, 0))); } #[test] fn border_rendering() { let html_str = r#"
Bordered
"#; let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets).unwrap(); let tree = we_layout::layout(&styled, &doc, 800.0, 600.0, &font); let list = build_display_list(&tree); let red_fills: Vec<_> = list .iter() .filter(|c| matches!(c, PaintCommand::FillRect { color, .. } if *color == Color::rgb(255, 0, 0))) .collect(); // Should have 4 border fills (top, right, bottom, left). assert_eq!(red_fills.len(), 4, "should have 4 border edges"); } }