we (web engine): Experimental web browser project to understand the limits of Claude
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement display:none and visibility:hidden rendering

Wire up visibility property through the layout and render pipeline:
- Add visibility field to LayoutBox, populated from ComputedStyle
- Renderer skips painting (background, borders, text, images) for
hidden/collapse elements but still recurses into children
- Children can override with visibility:visible per CSS spec
- display:none already worked (excluded from layout tree); verified

9 new tests: 5 layout tests (display:none exclusion, hidden preserves
space, inheritance, visible override, collapse behavior) and 4 render
tests (hidden not painted, visible child of hidden parent painted,
collapse not painted, display:none not in display list).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+276 -17
+153 -1
crates/layout/src/lib.rs
··· 9 9 use we_dom::{Document, NodeData, NodeId}; 10 10 use we_style::computed::{ 11 11 BorderStyle, BoxSizing, ComputedStyle, Display, LengthOrAuto, Overflow, Position, StyledNode, 12 - TextAlign, TextDecoration, 12 + TextAlign, TextDecoration, Visibility, 13 13 }; 14 14 use we_text::font::Font; 15 15 ··· 101 101 pub css_width: LengthOrAuto, 102 102 /// CSS `height` property (explicit or auto). 103 103 pub css_height: LengthOrAuto, 104 + /// CSS `visibility` property. 105 + pub visibility: Visibility, 104 106 } 105 107 106 108 impl LayoutBox { ··· 138 140 box_sizing: style.box_sizing, 139 141 css_width: style.width, 140 142 css_height: style.height, 143 + visibility: style.visibility, 141 144 } 142 145 } 143 146 ··· 2385 2388 2386 2389 // content-box: rect.height = 100 (specified height IS content height) 2387 2390 assert_eq!(div_box.rect.height, 100.0); 2391 + } 2392 + 2393 + // --- Visibility / display:none tests --- 2394 + 2395 + #[test] 2396 + fn display_none_excludes_from_layout_tree() { 2397 + let html_str = r#"<!DOCTYPE html> 2398 + <html> 2399 + <head><style> 2400 + body { margin: 0; } 2401 + .hidden { display: none; } 2402 + p { margin: 0; } 2403 + </style></head> 2404 + <body> 2405 + <p>First</p> 2406 + <div class="hidden">Hidden content</div> 2407 + <p>Second</p> 2408 + </body> 2409 + </html>"#; 2410 + let doc = we_html::parse_html(html_str); 2411 + let font = test_font(); 2412 + let sheets = extract_stylesheets(&doc); 2413 + let styled = resolve_styles(&doc, &sheets).unwrap(); 2414 + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 2415 + 2416 + let body_box = &tree.root.children[0]; 2417 + // display:none element is excluded — body should have only 2 children. 2418 + assert_eq!(body_box.children.len(), 2); 2419 + 2420 + let first = &body_box.children[0]; 2421 + let second = &body_box.children[1]; 2422 + // Second paragraph should be directly below first (no gap for hidden). 2423 + assert!( 2424 + second.rect.y == first.rect.y + first.rect.height, 2425 + "display:none should not occupy space" 2426 + ); 2427 + } 2428 + 2429 + #[test] 2430 + fn visibility_hidden_preserves_layout_space() { 2431 + let html_str = r#"<!DOCTYPE html> 2432 + <html> 2433 + <head><style> 2434 + body { margin: 0; } 2435 + .hidden { visibility: hidden; height: 50px; margin: 0; } 2436 + p { margin: 0; } 2437 + </style></head> 2438 + <body> 2439 + <p>First</p> 2440 + <div class="hidden">Hidden</div> 2441 + <p>Second</p> 2442 + </body> 2443 + </html>"#; 2444 + let doc = we_html::parse_html(html_str); 2445 + let font = test_font(); 2446 + let sheets = extract_stylesheets(&doc); 2447 + let styled = resolve_styles(&doc, &sheets).unwrap(); 2448 + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 2449 + 2450 + let body_box = &tree.root.children[0]; 2451 + // visibility:hidden still in layout tree — body has 3 children. 2452 + assert_eq!(body_box.children.len(), 3); 2453 + 2454 + let hidden_box = &body_box.children[1]; 2455 + assert_eq!(hidden_box.visibility, Visibility::Hidden); 2456 + assert_eq!(hidden_box.rect.height, 50.0); 2457 + 2458 + let second = &body_box.children[2]; 2459 + // Second paragraph should be below hidden div (it occupies 50px). 2460 + assert!( 2461 + second.rect.y >= hidden_box.rect.y + 50.0, 2462 + "visibility:hidden should preserve layout space" 2463 + ); 2464 + } 2465 + 2466 + #[test] 2467 + fn visibility_inherited_by_children() { 2468 + let html_str = r#"<!DOCTYPE html> 2469 + <html> 2470 + <head><style> 2471 + body { margin: 0; } 2472 + .parent { visibility: hidden; } 2473 + </style></head> 2474 + <body> 2475 + <div class="parent"><p>Child</p></div> 2476 + </body> 2477 + </html>"#; 2478 + let doc = we_html::parse_html(html_str); 2479 + let font = test_font(); 2480 + let sheets = extract_stylesheets(&doc); 2481 + let styled = resolve_styles(&doc, &sheets).unwrap(); 2482 + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 2483 + 2484 + let body_box = &tree.root.children[0]; 2485 + let parent_box = &body_box.children[0]; 2486 + let child_box = &parent_box.children[0]; 2487 + assert_eq!(parent_box.visibility, Visibility::Hidden); 2488 + assert_eq!(child_box.visibility, Visibility::Hidden); 2489 + } 2490 + 2491 + #[test] 2492 + fn visibility_visible_overrides_hidden_parent() { 2493 + let html_str = r#"<!DOCTYPE html> 2494 + <html> 2495 + <head><style> 2496 + body { margin: 0; } 2497 + .parent { visibility: hidden; } 2498 + .child { visibility: visible; } 2499 + </style></head> 2500 + <body> 2501 + <div class="parent"><p class="child">Visible child</p></div> 2502 + </body> 2503 + </html>"#; 2504 + let doc = we_html::parse_html(html_str); 2505 + let font = test_font(); 2506 + let sheets = extract_stylesheets(&doc); 2507 + let styled = resolve_styles(&doc, &sheets).unwrap(); 2508 + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 2509 + 2510 + let body_box = &tree.root.children[0]; 2511 + let parent_box = &body_box.children[0]; 2512 + let child_box = &parent_box.children[0]; 2513 + assert_eq!(parent_box.visibility, Visibility::Hidden); 2514 + assert_eq!(child_box.visibility, Visibility::Visible); 2515 + } 2516 + 2517 + #[test] 2518 + fn visibility_collapse_on_non_table_treated_as_hidden() { 2519 + let html_str = r#"<!DOCTYPE html> 2520 + <html> 2521 + <head><style> 2522 + body { margin: 0; } 2523 + div { visibility: collapse; height: 50px; } 2524 + </style></head> 2525 + <body> 2526 + <div>Collapsed</div> 2527 + </body> 2528 + </html>"#; 2529 + let doc = we_html::parse_html(html_str); 2530 + let font = test_font(); 2531 + let sheets = extract_stylesheets(&doc); 2532 + let styled = resolve_styles(&doc, &sheets).unwrap(); 2533 + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); 2534 + 2535 + let body_box = &tree.root.children[0]; 2536 + let div_box = &body_box.children[0]; 2537 + assert_eq!(div_box.visibility, Visibility::Collapse); 2538 + // Still occupies space (non-table collapse = hidden behavior). 2539 + assert_eq!(div_box.rect.height, 50.0); 2388 2540 } 2389 2541 }
+123 -16
crates/render/src/lib.rs
··· 9 9 use we_dom::NodeId; 10 10 use we_image::pixel::Image; 11 11 use we_layout::{BoxType, LayoutBox, LayoutTree, TextLine}; 12 - use we_style::computed::{BorderStyle, TextDecoration}; 12 + use we_style::computed::{BorderStyle, TextDecoration, Visibility}; 13 13 use we_text::font::Font; 14 14 15 15 /// A paint command in the display list. ··· 53 53 } 54 54 55 55 fn paint_box(layout_box: &LayoutBox, list: &mut DisplayList) { 56 - paint_background(layout_box, list); 57 - paint_borders(layout_box, list); 56 + let visible = layout_box.visibility == Visibility::Visible; 57 + 58 + if visible { 59 + paint_background(layout_box, list); 60 + paint_borders(layout_box, list); 58 61 59 - // Emit image paint command for replaced elements. 60 - if let Some((rw, rh)) = layout_box.replaced_size { 61 - if let Some(node_id) = node_id_from_box_type(&layout_box.box_type) { 62 - list.push(PaintCommand::DrawImage { 63 - x: layout_box.rect.x, 64 - y: layout_box.rect.y, 65 - width: rw, 66 - height: rh, 67 - node_id, 68 - }); 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 + } 69 73 } 70 - } 71 74 72 - paint_text(layout_box, list); 75 + paint_text(layout_box, list); 76 + } 73 77 74 - // Recurse into children. 78 + // Always recurse into children — they may override visibility. 75 79 for child in &layout_box.children { 76 80 paint_box(child, list); 77 81 } ··· 807 811 808 812 // Should have 4 border fills (top, right, bottom, left). 809 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> 822 + body { margin: 0; } 823 + div { 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> 851 + body { 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> 876 + body { margin: 0; } 877 + div { 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> 898 + body { 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 + ); 810 917 } 811 918 }