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
+Hidden content
+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
+Hidden
+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#"
+
+
+
+
+
+"#;
+ 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#"
+
+
+
+
+
+"#;
+ 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#"
+
+
+
+"#;
+ 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"
+ );
+ }
}