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