diff --git a/crates/obscura-render/src/dom.rs b/crates/obscura-render/src/dom.rs index 0caca20..ff5df14 100644 --- a/crates/obscura-render/src/dom.rs +++ b/crates/obscura-render/src/dom.rs @@ -83,6 +83,20 @@ struct IfcRegistry { /// table column-balancing pass in `layout_dom_with_images`, which pins /// specified columns instead of sizing them purely from content. table_cols: HashMap>, Vec>)>, + /// Floats whose direct block parent does not establish a block formatting + /// context and whose exclusion can therefore continue through later + /// descendant blocks of the same BFC. The first layout pass gives these + /// nodes real geometry; `apply_float_continuations` uses it to narrow the + /// later intersecting bands before the final layout. + float_continuations: Vec, +} + +#[derive(Clone, Copy)] +struct FloatContinuation { + owner: NodeId, + float: taffy::NodeId, + flow: taffy::NodeId, + side: crate::Float, } /// Walk the tree top-down accumulating the clip rect imposed by ancestor @@ -942,6 +956,19 @@ pub fn layout_dom_with_images( ); } let _ = taffy_tree.compute_layout_with_measure(taffy_root, available, &mut measure); + if apply_float_continuations( + tree, + &mut taffy_tree, + &id_map, + &styles, + &ifc_items, + ) { + let _ = taffy_tree.compute_layout_with_measure( + taffy_root, + available, + &mut measure, + ); + } } #[cfg(not(feature = "paint"))] { @@ -953,6 +980,15 @@ pub fn layout_dom_with_images( ); } let _ = taffy_tree.compute_layout(taffy_root, available); + if apply_float_continuations( + tree, + &mut taffy_tree, + &id_map, + &styles, + &ifc_items, + ) { + let _ = taffy_tree.compute_layout(taffy_root, available); + } } compute_absolute_rects(&taffy_tree, taffy_root, 0.0, 0.0, &id_map, &words, &mut rects, &mut text_runs, &mut anon_rects); synthesize_row_rects(tree, &mut rects); @@ -1384,6 +1420,307 @@ fn taffy_global_origin( Some((x, y)) } +fn collect_taffy_global_rects( + taffy_tree: &TaffyTree, + node: taffy::NodeId, + parent_x: f32, + parent_y: f32, + rects: &mut HashMap, +) { + let Ok(layout) = taffy_tree.layout(node) else { + return; + }; + let x = parent_x + layout.location.x; + let y = parent_y + layout.location.y; + rects.insert( + node, + Rect { + x, + y, + width: layout.size.width, + height: layout.size.height, + }, + ); + for child in taffy_tree.children(node).unwrap_or_default() { + collect_taffy_global_rects(taffy_tree, child, x, y, rects); + } +} + +#[derive(Clone, Copy)] +struct FloatBand { + top: f32, + bottom: f32, + left: f32, + right: f32, + side: crate::Float, +} + +fn narrow_node_to_float_band( + taffy_tree: &mut TaffyTree, + node: taffy::NodeId, + preliminary_rects: &HashMap, + band: FloatBand, +) -> bool { + let Some(&rect) = preliminary_rects.get(&node) else { + return false; + }; + if rect.y >= band.bottom + || rect.y + rect.height <= band.top + || rect.width <= 0.0 + { + return false; + } + let Ok(current) = taffy_tree.style(node) else { + return false; + }; + let Ok(layout) = taffy_tree.layout(node) else { + return false; + }; + let mut narrowed = current.clone(); + let (available, left_shift) = match band.side { + crate::Float::Right => { + if band.left >= rect.x + rect.width { + return false; + } + ((band.left - rect.x).max(0.0), 0.0) + } + crate::Float::Left => { + if band.right <= rect.x { + return false; + } + let shift = (band.right - rect.x).max(0.0); + ((rect.width - shift).max(0.0), shift) + } + }; + if available >= rect.width - 0.01 { + return false; + } + let specified = if current.box_sizing == taffy::BoxSizing::ContentBox { + (available + - layout.padding.left + - layout.padding.right + - layout.border.left + - layout.border.right) + .max(0.0) + } else { + available + }; + narrowed.size.width = taffy::Dimension::length(specified); + narrowed.max_size.width = taffy::Dimension::length(specified); + if left_shift > 0.0 { + narrowed.margin.left = + taffy::LengthPercentageAuto::length(layout.margin.left + left_shift); + } + taffy_tree.set_style(node, narrowed).is_ok() +} + +fn grow_bfc_to_float_bottom( + taffy_tree: &mut TaffyTree, + node: taffy::NodeId, + preliminary_rects: &HashMap, + float_bottom: f32, +) -> bool { + let Some(&rect) = preliminary_rects.get(&node) else { + return false; + }; + let desired_border_height = (float_bottom - rect.y).max(0.0); + if desired_border_height <= rect.height + 0.01 { + return false; + } + let Ok(current) = taffy_tree.style(node) else { + return false; + }; + let Ok(layout) = taffy_tree.layout(node) else { + return false; + }; + let specified = if current.box_sizing == taffy::BoxSizing::ContentBox { + (desired_border_height + - layout.padding.top + - layout.padding.bottom + - layout.border.top + - layout.border.bottom) + .max(0.0) + } else { + desired_border_height + }; + let mut grown = current.clone(); + grown.min_size.height = taffy::Dimension::length(specified); + taffy_tree.set_style(node, grown).is_ok() +} + +fn narrow_intersecting_descendants( + tree: &DomTree, + id: NodeId, + taffy_tree: &mut TaffyTree, + reverse: &HashMap, + styles: &HashMap, + ifc_items: &IfcRegistry, + preliminary_rects: &HashMap, + band: FloatBand, +) -> bool { + let Some(&node) = reverse.get(&id) else { + return false; + }; + let Some(&rect) = preliminary_rects.get(&node) else { + return false; + }; + if rect.y >= band.bottom || rect.y + rect.height <= band.top { + return false; + } + let Some(style) = styles.get(&id) else { + return false; + }; + if style.display == crate::Display::None + || style.float.is_some() + || matches!(style.position, Some(taffy::Position::Absolute)) + { + return false; + } + + // Float-avoiding formatting contexts move as one box. A shaped IFC or a + // leaf has no deeper block boundary at which its width can change, so it + // also consumes the current band as a unit. + let in_flow_element_children: Vec = tree + .children(id) + .into_iter() + .filter(|child| { + styles.get(child).map_or(false, |child_style| { + child_style.display != crate::Display::None + && child_style.float.is_none() + && !matches!( + child_style.position, + Some(taffy::Position::Absolute) + ) + }) + }) + .collect(); + if establishes_block_formatting_context(style) + || ifc_items.whole.contains_key(&id) + || in_flow_element_children.is_empty() + { + return narrow_node_to_float_band( + taffy_tree, + node, + preliminary_rects, + band, + ); + } + + let mut changed = false; + for child in in_flow_element_children { + changed |= narrow_intersecting_descendants( + tree, + child, + taffy_tree, + reverse, + styles, + ifc_items, + preliminary_rects, + band, + ); + } + changed +} + +fn apply_float_continuations( + tree: &DomTree, + taffy_tree: &mut TaffyTree, + id_map: &HashMap, + styles: &HashMap, + ifc_items: &IfcRegistry, +) -> bool { + let reverse: HashMap = + id_map.iter().map(|(&taffy, &dom)| (dom, taffy)).collect(); + let Some(root) = id_map + .keys() + .copied() + .find(|node| taffy_tree.parent(*node).is_none()) + else { + return false; + }; + let mut preliminary_rects = HashMap::with_capacity(id_map.len()); + collect_taffy_global_rects( + taffy_tree, + root, + 0.0, + 0.0, + &mut preliminary_rects, + ); + let mut changed = false; + for continuation in &ifc_items.float_continuations { + let Some(&float_rect) = preliminary_rects.get(&continuation.float) else { + continue; + }; + let Ok(float_layout) = taffy_tree.layout(continuation.float) else { + continue; + }; + let Some(&flow_rect) = preliminary_rects.get(&continuation.flow) else { + continue; + }; + let band = FloatBand { + top: float_rect.y - float_layout.margin.top, + bottom: float_rect.y + float_rect.height + float_layout.margin.bottom, + left: float_rect.x - float_layout.margin.left, + right: float_rect.x + float_rect.width + float_layout.margin.right, + side: continuation.side, + }; + if band.bottom <= flow_rect.y + flow_rect.height + 0.01 { + continue; + } + + // A non-BFC wrapper is transparent to the BFC's float manager. Visit + // the wrapper's following siblings, then repeat at each ancestor until + // (and including) the nearest ancestor BFC. Descend through ordinary + // blocks so only the leaf/block bands that actually intersect the + // float are narrowed; later siblings below the float stay full width. + let mut current = continuation.owner; + while let Some(parent) = tree.get_node(current).and_then(|node| node.parent) + { + let siblings = tree.children(parent); + let Some(index) = siblings.iter().position(|candidate| *candidate == current) + else { + break; + }; + for sibling in &siblings[index + 1..] { + changed |= narrow_intersecting_descendants( + tree, + *sibling, + taffy_tree, + &reverse, + styles, + ifc_items, + &preliminary_rects, + band, + ); + } + let reached_bfc = styles + .get(&parent) + .map(establishes_block_formatting_context) + .unwrap_or(false); + if reached_bfc { + if styles + .get(&parent) + .map(|style| matches!(style.height, crate::Dimension::Auto)) + .unwrap_or(false) + { + if let Some(&bfc_node) = reverse.get(&parent) { + changed |= + grow_bfc_to_float_bottom( + taffy_tree, + bfc_node, + &preliminary_rects, + band.bottom, + ); + } + } + break; + } + current = parent; + } + } + changed +} + fn resolve_static_positions_and_reparent( taffy_tree: &mut TaffyTree, candidates: &[StaticPositionCandidate], @@ -2757,6 +3094,15 @@ fn max_definite_descendant_width(tree: &DomTree, id: NodeId, styles: &HashMap bool { + matches!(style.display, crate::Display::Flex | crate::Display::Grid) + || style.flow_root + || style.overflow_hidden + || style.is_inline_block + || style.float.is_some() + || matches!(style.position, Some(taffy::Position::Absolute)) +} + fn build_children_with_float_zone( tree: &DomTree, parent_id: NodeId, @@ -3148,7 +3494,6 @@ fn build_children_with_float_zone( break; } } - // The float itself is always an element (only elements get style // entries, and `is_float` above required one), so a direct `build` call // is correct here; only its flow siblings need the word-splitting `build_any`. @@ -3227,12 +3572,52 @@ fn build_children_with_float_zone( }, ..Default::default() }; + // A float does not contribute to the height of a non-BFC block + // that contains it. Put an escaping float inside a zero-height, + // overflow-visible wrapper: its real width still reserves the + // current band, but its height can protrude into later descendant + // blocks of the ancestor BFC. A matching `clear` or any remaining + // direct full-width sibling keeps the old containing row, since + // that content explicitly terminates the local float zone. + let can_escape = zone_end == dom_children.len() + && styles + .get(&parent_id) + .map(|style| !establishes_block_formatting_context(style)) + .unwrap_or(false); + let row_float = if can_escape { + let wrapper_style = taffy::Style { + display: taffy::style::Display::Block, + flex_grow: 0.0, + flex_shrink: 0.0, + size: taffy::Size { + width: taffy::Dimension::auto(), + height: taffy::Dimension::length(0.0), + }, + ..Default::default() + }; + taffy_tree + .new_with_children(wrapper_style, &[float_id]) + .ok() + .unwrap_or(float_id) + } else { + float_id + }; let row_children: Vec = match float_side { - Some(crate::Float::Left) => [Some(float_id), flow_column].into_iter().flatten().collect(), - _ => [flow_column, Some(float_id)].into_iter().flatten().collect(), + Some(crate::Float::Left) => [Some(row_float), flow_column].into_iter().flatten().collect(), + _ => [flow_column, Some(row_float)].into_iter().flatten().collect(), }; if let Ok(row) = taffy_tree.new_with_children(row_style, &row_children) { result.push(row); + if can_escape { + if let (Some(flow), Some(side)) = (flow_column, float_side) { + ifc_items.float_continuations.push(FloatContinuation { + owner: parent_id, + float: float_id, + flow, + side, + }); + } + } } } // The float itself failed to build (e.g. display:none resolved for diff --git a/crates/obscura-render/src/lib.rs b/crates/obscura-render/src/lib.rs index fc3a0d9..4fc341a 100644 --- a/crates/obscura-render/src/lib.rs +++ b/crates/obscura-render/src/lib.rs @@ -413,6 +413,11 @@ pub struct LayoutStyle { /// own box (including any `::before`/`::after` content attached to it). pub is_inline_block: bool, + /// `display: flow-root`: generates a normal block box but establishes a + /// new block formatting context, containing descendant floats and stopping + /// their exclusion bands from propagating into outside siblings. + pub flow_root: bool, + /// `display: contents`: the element generates no box of its own; its /// children participate in the parent's formatting context directly /// (`dom::build_any` splices them into the parent's child list). Kept as a diff --git a/crates/obscura-render/src/style.rs b/crates/obscura-render/src/style.rs index 0be5e64..9a4992e 100644 --- a/crates/obscura-render/src/style.rs +++ b/crates/obscura-render/src/style.rs @@ -257,9 +257,12 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) { | "grid" | "inline-grid" | "block" + | "flow-root" | "contents" ) { style.internal_flex_container = false; + style.is_inline_block = false; + style.flow_root = false; } match value { "none" => style.display = crate::Display::None, @@ -273,6 +276,10 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) { "grid" => style.display = crate::Display::Grid, "inline-grid" => style.display = crate::Display::Grid, "block" => style.display = crate::Display::Block, + "flow-root" => { + style.display = crate::Display::Block; + style.flow_root = true; + } "contents" => style.display_contents = true, _ => {} } @@ -2742,6 +2749,10 @@ mod tests { assert_eq!(s.display, Display::Flex); assert_eq!(s.width, crate::Dimension::Px(200.0)); assert_eq!(s.height, crate::Dimension::Px(50.0)); + + let flow_root = compute_style("div", Some("display: flow-root")); + assert_eq!(flow_root.display, Display::Block); + assert!(flow_root.flow_root); } #[test]