Use native floats for cleared bands

This commit is contained in:
SGavrl
2026-07-31 00:17:49 +00:00
parent 02f5d233d3
commit 8385f496ca
3 changed files with 388 additions and 28 deletions

View File

@@ -5912,6 +5912,8 @@ fn build(
// approximation corrupts flex sizing and percentage-margin placement.
let has_float_child = style.display == crate::Display::Block
&& dom_children.iter().any(|&cid| styles.get(&cid).map(|s| s.float.is_some()).unwrap_or(false));
let native_float_band =
has_float_child && can_use_native_float_band(tree, style, &dom_children, styles);
let has_in_flow_block_child = dom_children.iter().any(|cid| {
styles.get(cid).map_or(false, |child| {
child.display == crate::Display::Block
@@ -5961,6 +5963,7 @@ fn build(
if stacks_children_vertically
&& has_inline_ish_content
&& !native_float_band
&& !(has_float_child && has_in_flow_block_child)
{
taffy_style.display = taffy::style::Display::Flex;
@@ -5980,7 +5983,26 @@ fn build(
taffy_style.align_items = Some(taffy::AlignItems::FLEX_START);
}
let mut child_ids: Vec<taffy::NodeId> = if has_float_child {
if native_float_band {
// Native float placement is only selected for the guarded flat-band
// shape. It requires a real block formatting context on the parent.
taffy_style.display = taffy::style::Display::Block;
}
let mut child_ids: Vec<taffy::NodeId> = if native_float_band {
build_children_with_native_float_band(
tree,
id,
style,
&dom_children,
taffy_tree,
id_map,
words,
engine,
ifc_items,
styles,
)
} else if has_float_child {
build_children_with_float_zone(tree, id, &dom_children, taffy_tree, id_map, words, engine, ifc_items, styles)
} else if matches!(style.display, crate::Display::Flex | crate::Display::Grid)
&& !style.internal_flex_container
@@ -5999,26 +6021,28 @@ fn build(
} else {
dom_children.into_iter().flat_map(|cid| build_any(tree, cid, taffy_tree, id_map, words, engine, ifc_items, styles)).collect()
};
if let Some((mut before, _)) = build_in_flow_pseudo(
id,
GeneratedBoxKind::Before,
style.before_pseudo.as_deref(),
taffy_tree,
words,
ifc_items,
) {
before.append(&mut child_ids);
child_ids = before;
}
if let Some((mut after, _)) = build_in_flow_pseudo(
id,
GeneratedBoxKind::After,
style.after_pseudo.as_deref(),
taffy_tree,
words,
ifc_items,
) {
child_ids.append(&mut after);
if !native_float_band {
if let Some((mut before, _)) = build_in_flow_pseudo(
id,
GeneratedBoxKind::Before,
style.before_pseudo.as_deref(),
taffy_tree,
words,
ifc_items,
) {
before.append(&mut child_ids);
child_ids = before;
}
if let Some((mut after, _)) = build_in_flow_pseudo(
id,
GeneratedBoxKind::After,
style.after_pseudo.as_deref(),
taffy_tree,
words,
ifc_items,
) {
child_ids.append(&mut after);
}
}
if style.internal_flex_container
@@ -6812,6 +6836,222 @@ fn establishes_block_formatting_context(style: &crate::LayoutStyle) -> bool {
|| matches!(style.position, Some(taffy::Position::Absolute))
}
fn clear_matches_float_sides(clear: crate::Clear, has_left: bool, has_right: bool) -> bool {
(!has_left || matches!(clear, crate::Clear::Left | crate::Clear::Both))
&& (!has_right || matches!(clear, crate::Clear::Right | crate::Clear::Both))
}
fn has_deferred_or_auto_margin(style: &crate::LayoutStyle) -> bool {
style.margin_auto.iter().any(|value| *value)
|| style.margin_percent.iter().any(Option::is_some)
|| style.margin_relative.iter().any(Option::is_some)
|| style.margin_expressions.iter().any(Option::is_some)
}
fn is_structural_native_clear_box(
tree: &DomTree,
id: NodeId,
style: &crate::LayoutStyle,
) -> bool {
style.display == crate::Display::Block
&& !style.display_contents
&& !style.is_table_box
&& !style.is_inline_block
&& !style.flow_root
&& !style.overflow_hidden
&& effective_container_type(style) == crate::ContainerType::Normal
&& style.float.is_none()
&& style.clear.is_some()
&& !matches!(style.position, Some(taffy::Position::Absolute))
&& tree.text_content(id).trim().is_empty()
&& style.before_pseudo.is_none()
&& style.after_pseudo.is_none()
}
fn is_structural_native_float_pseudo(style: &crate::LayoutStyle) -> bool {
style.display != crate::Display::None
&& style.display != crate::Display::Inline
&& !style.display_contents
&& !style.is_inline_block
&& (!style.flow_root || style.is_table_box)
&& !style.overflow_hidden
&& effective_container_type(style) == crate::ContainerType::Normal
&& style.float.is_none()
&& !matches!(style.position, Some(taffy::Position::Absolute))
&& style
.before_content
.as_deref()
.map_or(true, |content| content.trim().is_empty())
}
/// Native taffy floats are currently sound for a deliberately small structural
/// subset: a flat block band made only of definite-width direct floats plus
/// empty generated/direct clearance boxes. Text flow, transparent wrappers,
/// shrink-to-fit floats, independent formatting contexts, and tables retain
/// the synthetic float-zone path.
fn can_use_native_float_band(
tree: &DomTree,
parent_style: &crate::LayoutStyle,
dom_children: &[NodeId],
styles: &HashMap<NodeId, crate::LayoutStyle>,
) -> bool {
if parent_style.display != crate::Display::Block
|| 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,
crate::Dimension::Px(_) | crate::Dimension::Percent(_)
)
|| parent_style.size_expressions[0].is_some()
{
return false;
}
let mut has_left = false;
let mut has_right = false;
let mut saw_float = false;
let mut saw_clear_after_floats = false;
if let Some(before) = parent_style.before_pseudo.as_deref() {
if !is_structural_native_float_pseudo(before) {
return false;
}
}
for &id in dom_children {
let Some(node) = tree.get_node(id) else { continue };
if !node.is_element() {
if !tree.text_content(id).trim().is_empty() {
return false;
}
continue;
}
let Some(style) = styles.get(&id) else {
return false;
};
if style.display == crate::Display::None {
continue;
}
if let Some(side) = style.float {
if saw_clear_after_floats
|| style.display_contents
|| style.is_table_box
|| matches!(style.position, Some(taffy::Position::Absolute))
|| !matches!(style.width, crate::Dimension::Px(_) | crate::Dimension::Percent(_))
|| style.size_expressions[0].is_some()
|| has_deferred_or_auto_margin(style)
{
return false;
}
saw_float = true;
has_left |= side == crate::Float::Left;
has_right |= side == crate::Float::Right;
} else if is_structural_native_clear_box(tree, id, style) {
if !saw_float {
return false;
}
saw_clear_after_floats |= clear_matches_float_sides(
style.clear.expect("structural clear has a side"),
has_left,
has_right,
);
} else {
return false;
}
}
if let Some(after) = parent_style.after_pseudo.as_deref() {
if !is_structural_native_float_pseudo(after) {
return false;
}
if let Some(clear) = after.clear {
saw_clear_after_floats |= clear_matches_float_sides(clear, has_left, has_right);
}
}
// Taffy represents overflow clipping as a real BFC root. Other Obscura
// BFC markers do not yet have a distinct taffy-side representation, so
// they are intentionally not used as an escape-safety signal here.
let parent_is_native_bfc = parent_style.overflow_hidden;
saw_float && (saw_clear_after_floats || parent_is_native_bfc)
}
fn set_native_float_clear(
taffy_tree: &mut TaffyTree<usize>,
node: taffy::NodeId,
style: &crate::LayoutStyle,
generated_pseudo: bool,
) {
let Ok(current) = taffy_tree.style(node) else { return };
let mut native = current.clone();
native.float = match style.float {
Some(crate::Float::Left) => taffy::style::Float::Left,
Some(crate::Float::Right) => taffy::style::Float::Right,
None => taffy::style::Float::None,
};
native.clear = match style.clear {
Some(crate::Clear::Left) => taffy::style::Clear::Left,
Some(crate::Clear::Right) => taffy::style::Clear::Right,
Some(crate::Clear::Both) => taffy::style::Clear::Both,
None => taffy::style::Clear::None,
};
if generated_pseudo && style.is_table_box {
// Bootstrap-style clearfix pseudos use display:table only to generate
// an empty block formatting box. Taffy's independent table-item clear
// path is incomplete, while its same-BFC block clear path is correct.
native.display = taffy::style::Display::Block;
native.item_is_table = false;
}
let _ = taffy_tree.set_style(node, native);
}
#[allow(clippy::too_many_arguments)]
fn build_children_with_native_float_band(
tree: &DomTree,
parent_id: NodeId,
parent_style: &crate::LayoutStyle,
dom_children: &[NodeId],
taffy_tree: &mut TaffyTree<usize>,
id_map: &mut HashMap<taffy::NodeId, NodeId>,
words: &mut HashMap<taffy::NodeId, (NodeId, String)>,
engine: &mut crate::inline::TextEngine,
ifc_items: &mut IfcRegistry,
styles: &HashMap<NodeId, crate::LayoutStyle>,
) -> Vec<taffy::NodeId> {
let mut result = Vec::new();
for (kind, pseudo) in [
(GeneratedBoxKind::Before, parent_style.before_pseudo.as_deref()),
(GeneratedBoxKind::After, parent_style.after_pseudo.as_deref()),
] {
if kind == GeneratedBoxKind::After {
for &id in dom_children {
let Some(style) = styles.get(&id) else { continue };
for node in build_any(
tree, id, taffy_tree, id_map, words, engine, ifc_items, styles,
) {
set_native_float_clear(taffy_tree, node, style, false);
result.push(node);
}
}
}
if let Some((nodes, _)) =
build_in_flow_pseudo(parent_id, kind, pseudo, taffy_tree, words, ifc_items)
{
if let Some(style) = pseudo {
for node in nodes {
set_native_float_clear(taffy_tree, node, style, true);
result.push(node);
}
}
}
}
result
}
fn build_children_with_float_zone(
tree: &DomTree,
parent_id: NodeId,

View File

@@ -533,6 +533,123 @@ fn percentage_flex_overlay_uses_auto_width_parent_content_box() {
assert!((sidebar.width - 320.0).abs() < 0.01, "sidebar: {sidebar:?}");
}
#[test]
fn native_float_band_matches_bootstrap_clearfix_geometry() {
let tree = parse_html(include_str!(
"../../../render-repros/bootstrap-float-clearfix.html"
));
let layout = layout_dom(&tree, (900.0, 1000.0));
let rect = |selector| {
let id = tree.query_selector(selector).unwrap().unwrap();
layout.rects[&id]
};
let row = rect(".row");
let column = rect(".column");
let centered = rect(".centered");
let card = rect(".card");
assert!(
(row.x - 0.0).abs() < 0.01
&& (row.y - 0.0).abs() < 0.01
&& (row.width - 900.0).abs() < 0.01
&& (row.height - 1000.0).abs() < 0.01,
"row: {row:?}"
);
assert!(
(column.x - 0.0).abs() < 0.01
&& (column.y - 0.0).abs() < 0.01
&& (column.width - 900.0).abs() < 0.01
&& (column.height - 1000.0).abs() < 0.01,
"column: {column:?}"
);
assert!(
(centered.x - 0.0).abs() < 0.01
&& (centered.y - 0.0).abs() < 0.01
&& (centered.width - 900.0).abs() < 0.01
&& (centered.height - 900.0).abs() < 0.01,
"centered: {centered:?}"
);
assert!(
(card.x - 250.0).abs() < 0.01
&& (card.y - 200.0).abs() < 0.01
&& (card.width - 400.0).abs() < 0.01
&& (card.height - 500.0).abs() < 0.01,
"card: {card:?}"
);
}
#[test]
fn native_percentage_float_uses_padded_containing_block_content_width() {
let tree = parse_html(
r#"<style>
html,body{margin:0}
#band{box-sizing:border-box;width:300px;padding:20px}
#float{float:left;width:50%;height:40px}
#clear{clear:left}
</style>
<div id="band"><div id="float"></div><div id="clear"></div></div>"#,
);
let layout = layout_dom(&tree, (600.0, 300.0));
let band = layout.rects[&tree.get_element_by_id("band").unwrap()];
let float = layout.rects[&tree.get_element_by_id("float").unwrap()];
let clear = layout.rects[&tree.get_element_by_id("clear").unwrap()];
assert!(
(band.width - 300.0).abs() < 0.01 && (band.height - 80.0).abs() < 0.01,
"band: {band:?}"
);
assert!(
(float.x - 20.0).abs() < 0.01
&& (float.y - 20.0).abs() < 0.01
&& (float.width - 130.0).abs() < 0.01
&& (float.height - 40.0).abs() < 0.01,
"float: {float:?}"
);
assert!(
(clear.x - 20.0).abs() < 0.01 && (clear.y - 60.0).abs() < 0.01,
"clear: {clear:?}"
);
}
#[test]
fn native_float_clearance_is_side_sensitive() {
let tree = parse_html(
r#"<style>
html,body{margin:0}
.band{width:200px;overflow:hidden}
.float{float:right;width:60px;height:50px}
.clear{height:10px}
#matching .clear{clear:right}
#other-side .clear{clear:left}
</style>
<div id="matching" class="band"><div class="float"></div><div class="clear"></div></div>
<div id="other-side" class="band"><div class="float"></div><div class="clear"></div></div>"#,
);
let layout = layout_dom(&tree, (600.0, 300.0));
let matching = tree.get_element_by_id("matching").unwrap();
let other = tree.get_element_by_id("other-side").unwrap();
let matching_clear = tree
.query_selector("#matching .clear")
.unwrap()
.unwrap();
let other_clear = tree
.query_selector("#other-side .clear")
.unwrap()
.unwrap();
let matching_rect = layout.rects[&matching];
let other_rect = layout.rects[&other];
let matching_clear_rect = layout.rects[&matching_clear];
let other_clear_rect = layout.rects[&other_clear];
assert!(
(matching_clear_rect.y - 50.0).abs() < 0.01
&& (matching_rect.height - 60.0).abs() < 0.01,
"matching: {matching_rect:?} clear: {matching_clear_rect:?}"
);
assert!(
(other_clear_rect.y - 60.0).abs() < 0.01
&& (other_rect.height - 50.0).abs() < 0.01,
"other: {other_rect:?} clear: {other_clear_rect:?}"
);
}
#[test]
fn float_flow_zone_preserves_blocks_inline_runs_and_clearance() {
let tree = parse_html(include_str!("../../../render-repros/float-flow-bands.html"));

View File

@@ -452,14 +452,17 @@ impl FloatContext {
/// Get the end segment of the last float on side(s) specified by the clear parameter (if any)
fn cleared_segment(&self, clear: Clear) -> Option<usize> {
let placed_end = |slot: usize| {
let placed = &self.last_placed_floats[slot];
(placed.start < placed.end).then_some(placed.end)
};
match clear {
Clear::Left => Some(self.last_placed_floats[0].end),
Clear::Right => Some(self.last_placed_floats[1].end),
Clear::Both => {
let left_end = self.last_placed_floats[0].end;
let right_end = self.last_placed_floats[1].end;
Some(left_end.max(right_end))
}
Clear::Left => placed_end(0),
Clear::Right => placed_end(1),
Clear::Both => match (placed_end(0), placed_end(1)) {
(Some(left), Some(right)) => Some(left.max(right)),
(left, right) => left.or(right),
},
Clear::None => None,
}
}