diff --git a/crates/obscura-render/src/dom.rs b/crates/obscura-render/src/dom.rs index 54d12a5..d412aab 100644 --- a/crates/obscura-render/src/dom.rs +++ b/crates/obscura-render/src/dom.rs @@ -12536,7 +12536,6 @@ fn can_use_native_float_band( || parent_style.internal_flex_container || parent_style.is_inline_block || parent_style.is_table_box - || parent_style.float.is_some() || matches!(parent_style.position, Some(taffy::Position::Absolute)) || !matches!( parent_style.width, @@ -12741,7 +12740,12 @@ fn build_children_with_float_zone( ifc_items: &mut IfcRegistry, styles: &HashMap, ) -> Vec { - let is_float = |cid: NodeId| styles.get(&cid).map(|s| s.float.is_some()).unwrap_or(false); + let is_float = |cid: NodeId| { + styles + .get(&cid) + .map(|style| style.display != crate::Display::None && style.float.is_some()) + .unwrap_or(false) + }; // A definite-height block whose only substantive contents are either one // full-width float or one percentage float followed by one percentage @@ -12785,7 +12789,13 @@ fn build_children_with_float_zone( .copied() .filter(|cid| { tree.get_node(*cid).is_some_and(|node| { - node.is_element() || !tree.text_content(*cid).trim().is_empty() + if node.is_element() { + styles + .get(cid) + .is_some_and(|style| style.display != crate::Display::None) + } else { + !tree.text_content(*cid).trim().is_empty() + } }) }) .collect(); @@ -13050,6 +13060,9 @@ fn build_children_with_float_zone( return tree.text_content(cid).trim().is_empty(); } let style = styles.get(&cid); + if style.is_some_and(|style| style.display == crate::Display::None) { + return true; + } let no_size = style .map(|style| { matches!(style.width, crate::Dimension::Auto) @@ -13075,7 +13088,8 @@ fn build_children_with_float_zone( let opposite_side = dom_children .get(opposite_idx) .and_then(|cid| styles.get(cid)) - .and_then(|style| style.float); + .and_then(|style| (style.display != crate::Display::None).then_some(style.float)) + .flatten(); if opposite_side.is_some() && opposite_side != float_side { let first = build( tree, @@ -13134,7 +13148,7 @@ fn build_children_with_float_zone( let mut float_count = 1usize; while run_end < dom_children.len() { let cid = dom_children[run_end]; - if styles.get(&cid).and_then(|s| s.float) == float_side { + if is_float(cid) && styles.get(&cid).and_then(|style| style.float) == float_side { float_count += 1; run_end += 1; } else if tree.get_node(cid).map_or(false, |n| !n.is_element()) @@ -13170,7 +13184,10 @@ fn build_children_with_float_zone( && dom_children .get(run_end) .and_then(|cid| styles.get(cid)) - .and_then(|style| style.float) + .and_then(|style| { + (style.display != crate::Display::None).then_some(style.float) + }) + .flatten() == Some(crate::Float::Right)) .then(|| dom_children[run_end]); if let Some(right_dom) = trailing_right { @@ -13260,7 +13277,11 @@ fn build_children_with_float_zone( // `clear` on a sibling ends the zone: the cleared element moves below the // float (the clearfix idiom), so it must not join the flow column beside it. let clears_this_float = |cid: NodeId| { - let Some(c) = styles.get(&cid).and_then(|s| s.clear) else { + let Some(c) = styles.get(&cid).and_then(|style| { + (style.display != crate::Display::None) + .then_some(style.clear) + .flatten() + }) else { return false; }; match (float_side, c) { @@ -13308,7 +13329,10 @@ fn build_children_with_float_zone( .get(&float_dom) .map(|s| matches!(s.width, crate::Dimension::Auto)) .unwrap_or(true); - if float_auto { + let float_is_table = styles + .get(&float_dom) + .is_some_and(|style| style.is_table_box); + if float_auto && float_is_table { if let Some(w) = max_definite_descendant_width(tree, float_dom, styles) { if let Ok(cur) = taffy_tree.style(float_id) { let mut st = cur.clone(); @@ -14260,6 +14284,100 @@ mod tests { ); } + #[test] + fn display_none_floats_do_not_hide_a_later_visible_float_from_the_band() { + // Bootstrap keeps its responsive toggle/cart floated even when a + // desktop media query makes them display:none. Non-generated boxes + // with display:none must not enter float selection or split the + // visible brand out of its float band. + let tree = parse_html( + r#" +
+ +
"#, + ); + let laid = layout_dom(&tree, (900.0, 300.0)); + let rect = |id: &str| laid.rects[&tree.get_element_by_id(id).unwrap()]; + let header = rect("header"); + let brand = rect("brand"); + let flow = rect("flow"); + let line = rect("line"); + + assert_eq!( + (brand.x, brand.y, brand.width, brand.height), + (0.0, 0.0, 208.0, 100.0) + ); + assert_eq!( + (flow.x, flow.y, flow.width, flow.height), + (0.0, 0.0, 780.0, 50.0), + "an ordinary flow block keeps its full border-box width beneath a float" + ); + assert_eq!( + (line.x, line.y, line.width, line.height), + (241.0, 0.0, 400.0, 20.0), + "display:none floats must not suppress the later visible float's line band" + ); + assert_eq!((header.width, header.height), (780.0, 100.0)); + } + + #[test] + fn auto_width_float_uses_its_own_bfc_for_nested_float_max_content() { + // A float establishes a BFC. Its auto inline size must therefore be + // measured from its nested float's max-content line, not through the + // cyclic width:100% synthetic row used for non-native float zones. + let tree = parse_html( + r#" + "#, + ); + let laid = layout_dom(&tree, (900.0, 300.0)); + let rect = |id: &str| laid.rects[&tree.get_element_by_id(id).unwrap()]; + let header = rect("header"); + let brand = rect("brand"); + let logo = rect("logo"); + + assert_eq!((logo.width, logo.height), (70.0, 70.0)); + assert!( + brand.width > 220.0, + "the nested float must aggregate the image and label on its max-content line: {brand:?}" + ); + assert!( + header.width > 255.0 && header.width < 259.0, + "the hidden 22px toggle icon must not cap the outer shrink-to-fit width: {header:?}" + ); + assert!( + (header.width - brand.width - 33.0).abs() < 0.01, + "the outer auto-width float must include the nested float's margin box: header={header:?}, brand={brand:?}" + ); + } + #[test] fn ineligible_nearest_inline_container_does_not_fall_through() { let tree = parse_html(