diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 7636f94..2b72121 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -9,7 +9,7 @@ use we_css::values::Color; use we_dom::{Document, NodeData, NodeId}; use we_style::computed::{ BorderStyle, BoxSizing, ComputedStyle, Display, LengthOrAuto, Overflow, Position, StyledNode, - TextAlign, TextDecoration, + TextAlign, TextDecoration, Visibility, }; use we_text::font::Font; @@ -101,6 +101,8 @@ pub struct LayoutBox { pub css_width: LengthOrAuto, /// CSS `height` property (explicit or auto). pub css_height: LengthOrAuto, + /// CSS `visibility` property. + pub visibility: Visibility, } impl LayoutBox { @@ -138,6 +140,7 @@ impl LayoutBox { box_sizing: style.box_sizing, css_width: style.width, css_height: style.height, + visibility: style.visibility, } } @@ -2386,4 +2389,153 @@ div { height: 100px; padding: 10px; border: 5px solid black; } // content-box: rect.height = 100 (specified height IS content height) assert_eq!(div_box.rect.height, 100.0); } + + // --- Visibility / display:none tests --- + + #[test] + fn display_none_excludes_from_layout_tree() { + let html_str = r#" + + + +

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, &HashMap::new()); + + let body_box = &tree.root.children[0]; + // display:none element is excluded — body should have only 2 children. + assert_eq!(body_box.children.len(), 2); + + let first = &body_box.children[0]; + let second = &body_box.children[1]; + // Second paragraph should be directly below first (no gap for hidden). + assert!( + second.rect.y == first.rect.y + first.rect.height, + "display:none should not occupy space" + ); + } + + #[test] + fn visibility_hidden_preserves_layout_space() { + let html_str = r#" + + + +

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, &HashMap::new()); + + let body_box = &tree.root.children[0]; + // visibility:hidden still in layout tree — body has 3 children. + assert_eq!(body_box.children.len(), 3); + + let hidden_box = &body_box.children[1]; + assert_eq!(hidden_box.visibility, Visibility::Hidden); + assert_eq!(hidden_box.rect.height, 50.0); + + let second = &body_box.children[2]; + // Second paragraph should be below hidden div (it occupies 50px). + assert!( + second.rect.y >= hidden_box.rect.y + 50.0, + "visibility:hidden should preserve layout space" + ); + } + + #[test] + fn visibility_inherited_by_children() { + let html_str = r#" + + + +

Child

+ +"#; + 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, &HashMap::new()); + + let body_box = &tree.root.children[0]; + let parent_box = &body_box.children[0]; + let child_box = &parent_box.children[0]; + assert_eq!(parent_box.visibility, Visibility::Hidden); + assert_eq!(child_box.visibility, Visibility::Hidden); + } + + #[test] + fn visibility_visible_overrides_hidden_parent() { + let html_str = r#" + + + +

Visible child

+ +"#; + 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, &HashMap::new()); + + let body_box = &tree.root.children[0]; + let parent_box = &body_box.children[0]; + let child_box = &parent_box.children[0]; + assert_eq!(parent_box.visibility, Visibility::Hidden); + assert_eq!(child_box.visibility, Visibility::Visible); + } + + #[test] + fn visibility_collapse_on_non_table_treated_as_hidden() { + let html_str = r#" + + + +
Collapsed
+ +"#; + 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, &HashMap::new()); + + let body_box = &tree.root.children[0]; + let div_box = &body_box.children[0]; + assert_eq!(div_box.visibility, Visibility::Collapse); + // Still occupies space (non-table collapse = hidden behavior). + assert_eq!(div_box.rect.height, 50.0); + } } diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index d019502..8641992 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -9,7 +9,7 @@ use we_css::values::Color; use we_dom::NodeId; use we_image::pixel::Image; use we_layout::{BoxType, LayoutBox, LayoutTree, TextLine}; -use we_style::computed::{BorderStyle, TextDecoration}; +use we_style::computed::{BorderStyle, TextDecoration, Visibility}; use we_text::font::Font; /// A paint command in the display list. @@ -53,25 +53,29 @@ pub fn build_display_list(tree: &LayoutTree) -> DisplayList { } fn paint_box(layout_box: &LayoutBox, list: &mut DisplayList) { - paint_background(layout_box, list); - paint_borders(layout_box, list); - - // Emit image paint command for replaced elements. - if let Some((rw, rh)) = layout_box.replaced_size { - if let Some(node_id) = node_id_from_box_type(&layout_box.box_type) { - list.push(PaintCommand::DrawImage { - x: layout_box.rect.x, - y: layout_box.rect.y, - width: rw, - height: rh, - node_id, - }); + let visible = layout_box.visibility == Visibility::Visible; + + if visible { + paint_background(layout_box, list); + paint_borders(layout_box, list); + + // Emit image paint command for replaced elements. + if let Some((rw, rh)) = layout_box.replaced_size { + if let Some(node_id) = node_id_from_box_type(&layout_box.box_type) { + list.push(PaintCommand::DrawImage { + x: layout_box.rect.x, + y: layout_box.rect.y, + width: rw, + height: rh, + node_id, + }); + } } - } - paint_text(layout_box, list); + paint_text(layout_box, list); + } - // Recurse into children. + // Always recurse into children — they may override visibility. for child in &layout_box.children { paint_box(child, list); } @@ -808,4 +812,107 @@ mod tests { // Should have 4 border fills (top, right, bottom, left). assert_eq!(red_fills.len(), 4, "should have 4 border edges"); } + + // --- Visibility painting tests --- + + #[test] + fn visibility_hidden_not_painted() { + let html_str = r#" + +
Hidden
"#; + let doc = we_html::parse_html(html_str); + let tree = layout_doc(&doc); + let list = build_display_list(&tree); + + // No red background should be in the display list. + let red_fills = list.iter().filter(|c| { + matches!(c, PaintCommand::FillRect { color, .. } if *color == Color::rgb(255, 0, 0)) + }); + assert_eq!(red_fills.count(), 0, "hidden element should not be painted"); + + // No text should be rendered for the hidden element. + let hidden_text = list.iter().filter( + |c| matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text == "Hidden"), + ); + assert_eq!( + hidden_text.count(), + 0, + "hidden element text should not be painted" + ); + } + + #[test] + fn visibility_visible_child_of_hidden_parent_is_painted() { + let html_str = r#" + + +

Visible

+"#; + let doc = we_html::parse_html(html_str); + let tree = layout_doc(&doc); + let list = build_display_list(&tree); + + // The visible child's text should appear in the display list. + let visible_text = list.iter().filter( + |c| matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text.contains("Visible")), + ); + assert!( + visible_text.count() > 0, + "visible child of hidden parent should be painted" + ); + } + + #[test] + fn visibility_collapse_not_painted() { + let html_str = r#" + +
Collapsed
"#; + let doc = we_html::parse_html(html_str); + let tree = layout_doc(&doc); + let list = build_display_list(&tree); + + let blue_fills = list.iter().filter(|c| { + matches!(c, PaintCommand::FillRect { color, .. } if *color == Color::rgb(0, 0, 255)) + }); + assert_eq!( + blue_fills.count(), + 0, + "collapse element should not be painted" + ); + } + + #[test] + fn display_none_not_in_display_list() { + let html_str = r#" + + +

Visible

+
Gone
+"#; + let doc = we_html::parse_html(html_str); + let tree = layout_doc(&doc); + let list = build_display_list(&tree); + + let green_fills = list.iter().filter(|c| { + matches!(c, PaintCommand::FillRect { color, .. } if *color == Color::rgb(0, 128, 0)) + }); + assert_eq!( + green_fills.count(), + 0, + "display:none element should not be in display list" + ); + } }