diff --git a/Cargo.lock b/Cargo.lock index 6f76422..d54ece9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,6 +62,7 @@ version = "0.1.0" dependencies = [ "we-css", "we-dom", + "we-html", "we-style", "we-text", ] @@ -84,9 +85,11 @@ version = "0.1.0" dependencies = [ "we-css", "we-dom", + "we-html", "we-image", "we-layout", "we-platform", + "we-style", "we-text", ] diff --git a/crates/browser/src/main.rs b/crates/browser/src/main.rs index 018b972..a087c2e 100644 --- a/crates/browser/src/main.rs +++ b/crates/browser/src/main.rs @@ -54,10 +54,13 @@ fn render_page(html: &str, font: &Font, bitmap: &mut BitmapContext) { // Extract CSS from + + +

First

+

Second

+ +"#; + 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 = layout(&styled, &doc, 800.0, 600.0, &font); + + let body_box = &tree.root.children[0]; + let first = &body_box.children[0]; + let second = &body_box.children[1]; + + // p has 50px top margin from CSS. + assert_eq!(first.margin.top, 50.0); + assert_eq!(first.margin.bottom, 50.0); + + // Second p should be well below first. + assert!(second.rect.y > first.rect.y + 100.0); + } + + #[test] + fn inline_style_affects_layout() { + 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 = layout(&styled, &doc, 800.0, 600.0, &font); + + let body_box = &tree.root.children[0]; + let div_box = &body_box.children[0]; + + assert_eq!(div_box.padding.top, 20.0); + assert_eq!(div_box.padding.bottom, 20.0); + } + + #[test] + fn css_color_propagates_to_layout() { + let html_str = r#" + + +

Colored

+"#; + 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 = layout(&styled, &doc, 800.0, 600.0, &font); + + let body_box = &tree.root.children[0]; + let p_box = &body_box.children[0]; + + assert_eq!(p_box.color, Color::rgb(255, 0, 0)); + assert_eq!(p_box.background_color, Color::rgb(0, 0, 255)); + } } diff --git a/crates/render/Cargo.toml b/crates/render/Cargo.toml index 0355b32..3ae24be 100644 --- a/crates/render/Cargo.toml +++ b/crates/render/Cargo.toml @@ -12,5 +12,9 @@ we-platform = { path = "../platform" } we-layout = { path = "../layout" } we-dom = { path = "../dom" } we-css = { path = "../css" } +we-style = { path = "../style" } we-text = { path = "../text" } we-image = { path = "../image" } + +[dev-dependencies] +we-html = { path = "../html" } diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index d7af917..3e89149 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -3,33 +3,11 @@ //! Walks a layout tree, generates paint commands, and rasterizes them //! into a BGRA pixel buffer suitable for display via CoreGraphics. -use we_layout::{BoxType, LayoutBox, LayoutTree, TextLine}; +use we_css::values::Color; +use we_layout::{LayoutBox, LayoutTree, TextLine}; +use we_style::computed::{BorderStyle, TextDecoration}; use we_text::font::Font; -/// An RGBA color with 8-bit components. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Color { - pub r: u8, - pub g: u8, - pub b: u8, - pub a: u8, -} - -impl Color { - pub const BLACK: Color = Color { - r: 0, - g: 0, - b: 0, - a: 255, - }; - pub const WHITE: Color = Color { - r: 255, - g: 255, - b: 255, - a: 255, - }; -} - /// A paint command in the display list. #[derive(Debug)] pub enum PaintCommand { @@ -55,7 +33,7 @@ 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 text on top. +/// 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); @@ -63,10 +41,8 @@ pub fn build_display_list(tree: &LayoutTree) -> DisplayList { } fn paint_box(layout_box: &LayoutBox, list: &mut DisplayList) { - // Paint background for block-level boxes. paint_background(layout_box, list); - - // Paint text lines (inline content). + paint_borders(layout_box, list); paint_text(layout_box, list); // Recurse into children. @@ -76,31 +52,100 @@ fn paint_box(layout_box: &LayoutBox, list: &mut DisplayList) { } fn paint_background(layout_box: &LayoutBox, list: &mut DisplayList) { - match &layout_box.box_type { - BoxType::Block(_) | BoxType::Anonymous => { - // Paint a white background for block boxes. - // Only emit background if the box has non-zero area. - if layout_box.rect.width > 0.0 && layout_box.rect.height > 0.0 { - 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: Color::WHITE, - }); - } - } - BoxType::Inline(_) | BoxType::TextRun { .. } => {} + 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) { + let color = layout_box.color; + let underline = layout_box.text_decoration == TextDecoration::Underline; + for line in &layout_box.lines { list.push(PaintCommand::DrawGlyphs { line: line.clone(), font_size: layout_box.font_size, - color: Color::BLACK, + color, }); + + // Draw underline as a 1px line below the baseline. + if underline && line.width > 0.0 { + let baseline_y = line.y + layout_box.font_size; + let underline_y = baseline_y + 2.0; // 2px below baseline + list.push(PaintCommand::FillRect { + x: line.x, + y: underline_y, + width: line.width, + height: 1.0, + color, + }); + } } } @@ -173,15 +218,37 @@ impl Renderer { } /// Fill a rectangle with a solid color. - fn fill_rect(&mut self, x: f32, y: f32, width: f32, height: f32, color: 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; - for py in y0..y1 { - for px in x0..x1 { - self.set_pixel(px, py, color); + 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; + } } } } @@ -272,6 +339,7 @@ impl Renderer { 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 { @@ -288,6 +356,13 @@ mod tests { 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); @@ -303,12 +378,7 @@ mod tests { #[test] fn fill_rect_basic() { let mut r = Renderer::new(20, 20); - let red = Color { - r: 255, - g: 0, - b: 0, - a: 255, - }; + 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). @@ -328,12 +398,7 @@ mod tests { #[test] fn fill_rect_clipping() { let mut r = Renderer::new(10, 10); - let blue = Color { - r: 0, - g: 0, - b: 255, - a: 255, - }; + 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); @@ -343,42 +408,21 @@ mod tests { } } - #[test] - fn color_constants() { - assert_eq!( - Color::BLACK, - Color { - r: 0, - g: 0, - b: 0, - a: 255 - } - ); - assert_eq!( - Color::WHITE, - Color { - r: 255, - g: 255, - b: 255, - a: 255 - } - ); - } - #[test] fn display_list_from_empty_layout() { - let font = test_font(); let doc = Document::new(); - let tree = we_layout::layout(&doc, 800.0, 600.0, &font); - let list = build_display_list(&tree); - // Empty document should produce no paint commands (or just a background). - // Just check it doesn't panic. - assert!(list.len() <= 1); + 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 font = test_font(); let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); @@ -390,23 +434,18 @@ mod tests { doc.append_child(body, p); doc.append_child(p, text); - let tree = we_layout::layout(&doc, 800.0, 600.0, &font); + let tree = layout_doc(&doc); let list = build_display_list(&tree); - let has_fill = list - .iter() - .any(|c| matches!(c, PaintCommand::FillRect { .. })); let has_text = list .iter() .any(|c| matches!(c, PaintCommand::DrawGlyphs { .. })); - assert!(has_fill, "should have at least one FillRect"); assert!(has_text, "should have at least one DrawGlyphs"); } #[test] fn paint_simple_page() { - let font = test_font(); let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); @@ -418,7 +457,8 @@ mod tests { doc.append_child(body, p); doc.append_child(p, text); - let tree = we_layout::layout(&doc, 800.0, 600.0, &font); + let font = test_font(); + let tree = layout_doc(&doc); let mut renderer = Renderer::new(800, 600); renderer.paint(&tree, &font); @@ -435,12 +475,7 @@ mod tests { #[test] fn bgra_format_correct() { let mut r = Renderer::new(1, 1); - let color = Color { - r: 100, - g: 150, - b: 200, - a: 255, - }; + let color = Color::new(100, 150, 200, 255); r.set_pixel(0, 0, color); let pixels = r.pixels(); // BGRA format. @@ -452,7 +487,6 @@ mod tests { #[test] fn paint_heading_produces_larger_glyphs() { - let font = test_font(); let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); @@ -468,7 +502,7 @@ mod tests { doc.append_child(body, p); doc.append_child(p, p_text); - let tree = we_layout::layout(&doc, 800.0, 600.0, &font); + let tree = layout_doc(&doc); let list = build_display_list(&tree); // There should be DrawGlyphs commands with different font sizes. @@ -501,7 +535,6 @@ mod tests { #[test] fn glyph_compositing_anti_aliased() { // Render text and verify we get anti-aliased (partially transparent) pixels. - let font = test_font(); let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); @@ -513,7 +546,8 @@ mod tests { doc.append_child(body, p); doc.append_child(p, text); - let tree = we_layout::layout(&doc, 800.0, 600.0, &font); + let font = test_font(); + let tree = layout_doc(&doc); let mut renderer = Renderer::new(800, 600); renderer.paint(&tree, &font); @@ -536,4 +570,80 @@ mod tests { "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"); + } }