From 85613b741db4f6067cda2d595e8afb3cb8036669 Mon Sep 17 00:00:00 2001 From: SGavrl <277843151+SGavrl@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:03:50 +0000 Subject: [PATCH] render: place opposing floats in one band --- crates/obscura-render/src/dom.rs | 129 +++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/crates/obscura-render/src/dom.rs b/crates/obscura-render/src/dom.rs index c00117d..27ad5ae 100644 --- a/crates/obscura-render/src/dom.rs +++ b/crates/obscura-render/src/dom.rs @@ -1979,6 +1979,18 @@ fn build( // the CSS tie-break. Taffy has no CSS `order` style field, so feeding // it the correctly ordered item sequence is the missing translation. dom_children.sort_by_key(|cid| styles.get(cid).map(|style| style.order).unwrap_or(0)); + } else if style.display == crate::Display::Block { + // A float nested directly in a transparent inline wrapper (the common + // `` logo pattern) belongs to the + // ancestor block formatting context. Gecko reparents that out-of-flow + // frame to the BFC and leaves only a placeholder in the inline. Expose + // the same item to our float-band builder instead of charging the + // wrapper a full normal-flow row. + for child in &mut dom_children { + if let Some(float) = inline_wrapper_float(tree, *child, styles) { + *child = float; + } + } } // `float` has no effect on a flex or grid item. Legacy stylesheets often // leave floats on children after a newer rule turns their parent into a @@ -2073,6 +2085,39 @@ fn build( Some(taffy_id) } +fn inline_wrapper_float( + tree: &DomTree, + wrapper: NodeId, + styles: &HashMap, +) -> Option { + let wrapper_style = styles.get(&wrapper)?; + if wrapper_style.display != crate::Display::Inline + || wrapper_style.margin != crate::Edges::default() + || wrapper_style.padding != crate::Edges::default() + || wrapper_style.border != crate::Edges::default() + || wrapper_style.before_content.is_some() + || wrapper_style.after_content.is_some() + { + return None; + } + let visible: Vec = tree + .children(wrapper) + .into_iter() + .filter(|&child| { + let Some(node) = tree.get_node(child) else { return false }; + if !node.is_element() { + return !tree.text_content(child).trim().is_empty(); + } + styles + .get(&child) + .map(|style| style.display != crate::Display::None) + .unwrap_or(false) + }) + .collect(); + let [child] = visible.as_slice() else { return None }; + styles.get(child).and_then(|style| style.float).map(|_| *child) +} + /// Build a block container whose children mix inline-level and block-level /// content, preserving real block layout for the block children. /// @@ -2467,6 +2512,90 @@ fn build_children_with_float_zone( let float_side = styles.get(&dom_children[float_idx]).and_then(|s| s.float); + // Opposing header floats share one float band even when an empty legacy + // compatibility box sits between them. This is the classic left-logo / + // right-tagline header: serializing the two synthetic rows doubles the + // header height and pushes every later box down. Real float placement + // scans the same BFC band and puts the second float against the opposite + // edge when both margin boxes fit. + let is_empty_bridge = |cid: NodeId| { + let Some(node) = tree.get_node(cid) else { return true }; + if !node.is_element() { + return tree.text_content(cid).trim().is_empty(); + } + let style = styles.get(&cid); + let no_size = style + .map(|style| { + matches!(style.width, crate::Dimension::Auto) + && matches!(style.height, crate::Dimension::Auto) + && matches!(style.min_width, crate::Dimension::Auto) + && matches!(style.min_height, crate::Dimension::Auto) + && matches!(style.max_width, crate::Dimension::Auto) + && matches!(style.max_height, crate::Dimension::Auto) + && style.margin == crate::Edges::default() + && style.padding == crate::Edges::default() + && style.border == crate::Edges::default() + && style.before_content.is_none() + && style.after_content.is_none() + }) + .unwrap_or(true); + no_size && tree.text_content(cid).trim().is_empty() + }; + let mut opposite_idx = float_idx + 1; + while opposite_idx < dom_children.len() && is_empty_bridge(dom_children[opposite_idx]) { + opposite_idx += 1; + } + let opposite_side = dom_children + .get(opposite_idx) + .and_then(|cid| styles.get(cid)) + .and_then(|style| style.float); + if opposite_side.is_some() && opposite_side != float_side { + let first = build( + tree, + dom_children[float_idx], + taffy_tree, + id_map, + words, + engine, + ifc_items, + styles, + ); + let second = build( + tree, + dom_children[opposite_idx], + taffy_tree, + id_map, + words, + engine, + ifc_items, + styles, + ); + let row_children: Vec = match float_side { + Some(crate::Float::Left) => [first, second].into_iter().flatten().collect(), + _ => [second, first].into_iter().flatten().collect(), + }; + let row_style = taffy::Style { + display: taffy::style::Display::Flex, + flex_direction: taffy::FlexDirection::Row, + justify_content: Some(taffy::JustifyContent::SPACE_BETWEEN), + align_items: Some(taffy::AlignItems::FLEX_START), + size: taffy::Size { + width: taffy::Dimension::percent(1.0), + height: taffy::Dimension::auto(), + }, + ..Default::default() + }; + if let Ok(row) = taffy_tree.new_with_children(row_style, &row_children) { + result.push(row); + } + result.extend( + dom_children[opposite_idx + 1..] + .iter() + .flat_map(|&cid| build_any(tree, cid, taffy_tree, id_map, words, engine, ifc_items, styles)), + ); + return result; + } + // A run of two or more consecutively floated siblings (the classic // float-grid idiom: several `float:left; width:N%` boxes forming columns, // e.g. craigslist's `.sites .box{float:left;width:23%}` site directory) is