gioser

tawasuyu/taffy

A high performance rust-powered UI layout library

Rust 1061HTML 997Markdown 12TOML 83120 Definiciones

Fix layout of new formatting contexts beside floats (CSS2 §9.5)

98253f1c532de8fed1b4f0365aa088453e0ef617 · por Nico Burns · (hace 3 meses) · padre c3d5a7964 · ver el árbol en este commit

Delta semántico · 19

src/compute/block.rs

src/compute/float.rs

tests/hand_written.rs

4 archivos · +388 −7

diff --git a/src/compute/block.rs b/src/compute/block.rs
index 4029a2680..3c7741693 100644
--- a/src/compute/block.rs
+++ b/src/compute/block.rs
@@ -15,7 +15,7 @@ use crate::{
 };
 
 #[cfg(feature = "float_layout")]
-use super::float::{ContentSlot, FloatContext, FloatIntrinsicWidthCalculator};
+use super::float::{BfcSlot, ContentSlot, FloatContext, FloatIntrinsicWidthCalculator};
 #[cfg(feature = "float_layout")]
 use crate::{Clear, Float, FloatDirection};
 
@@ -155,6 +155,29 @@ impl BlockContext<'_> {
         slot
     }
 
+    /// Search for a space suitable for laying out a box that establishes an independent
+    /// formatting context (whose border box must not overlap floats)
+    pub fn find_bfc_slot(
+        &self,
+        min_y: f32,
+        margins: [f32; 2],
+        direction: Direction,
+        clear: Clear,
+        after: Option<usize>,
+    ) -> BfcSlot {
+        let mut slot = self.bfc.float_context.find_bfc_slot(
+            min_y + self.y_offset,
+            self.content_box_insets,
+            margins,
+            direction,
+            clear,
+            after,
+        );
+        slot.y -= self.y_offset;
+        slot.x -= self.insets[0];
+        slot
+    }
+
     /// Get the bottom of lowest relevant float for the specific clear property
     pub fn cleared_threshold(&self, clear: Clear) -> Option<f32> {
         self.bfc.float_context.cleared_threshold(clear).map(|threshold| threshold - self.y_offset)
@@ -885,6 +908,10 @@ fn perform_final_layout_on_in_flow_children(
             // Handle non-floated boxes
 
             let mut y_margin_offset: f32 = 0.0;
+            #[cfg(feature = "float_layout")]
+            let mut item_avoids_floats = false;
+            #[cfg(feature = "float_layout")]
+            let mut item_pushed_below_float = false;
 
             let (stretch_width, float_avoiding_position, float_avoiding_width) = if item.is_in_same_bfc {
                 let stretch_width = container_inner_width - item_non_auto_x_margin_sum;
@@ -903,10 +930,44 @@ fn perform_final_layout_on_in_flow_children(
 
                     #[cfg(feature = "float_layout")]
                     if has_active_floats {
-                        let slot = block_ctx.find_content_slot(min_y, item.clear, None);
+                        let x_margins = [item_non_auto_margin.left, item_non_auto_margin.right];
+                        // An auto width resolves to at least the negation of the margin sum
+                        // (so that the margin box width is non-negative, per CSS2 §10.3.3)
+                        let min_auto_width = -item_non_auto_x_margin_sum;
+
+                        // Find the highest slot (at or below `search_y`) with enough horizontal space
+                        // for the item's border box, which must not overlap any float
+                        let find_fitting_slot = |block_ctx: &BlockContext, search_y: f32| {
+                            let mut after = None;
+                            loop {
+                                let slot = block_ctx.find_bfc_slot(search_y, x_margins, direction, item.clear, after);
+                                let Some(segment_id) = slot.segment_id else { break slot };
+                                let width = item
+                                    .size
+                                    .width
+                                    .unwrap_or(slot.stretch_width.max(min_auto_width))
+                                    .maybe_clamp(item.min_size.width, item.max_size.width);
+                                if width <= slot.border_width + 0.001 {
+                                    break slot;
+                                }
+                                after = Some(segment_id);
+                            }
+                        };
+
+                        let mut slot = find_fitting_slot(block_ctx, min_y);
+
+                        // If the item had to move down to avoid floats then it "separates from the
+                        // float": similarly to clearance, its top margin no longer collapses with
+                        // the parent's margins and does not push the item below the float's edge.
+                        if slot.y > min_y {
+                            item_pushed_below_float = true;
+                            slot = find_fitting_slot(block_ctx, committed_y_offset);
+                        }
+
                         has_active_floats = slot.segment_id.is_some();
-                        let stretch_width = slot.width - item_non_auto_x_margin_sum;
-                        break 'block (stretch_width, Point { x: slot.x, y: slot.y }, slot.width);
+                        item_avoids_floats = true;
+                        let stretch_width = slot.stretch_width.max(min_auto_width);
+                        break 'block (stretch_width, Point { x: slot.x, y: slot.y }, slot.border_width);
                     }
 
                     if !has_active_floats {
@@ -1062,12 +1123,27 @@ fn perform_final_layout_on_in_flow_children(
                     y: committed_y_offset.max(clear_pos) + y_margin_offset + inset_offset.y,
                 }
             } else {
+                // When the item avoids floats, its non-auto margins are already accounted for in the
+                // slot's border-box position/width (margins may overlap floats), so only the auto
+                // portion of the resolved margin is added here.
+                #[cfg(feature = "float_layout")]
+                let (extra_margin_left, extra_margin_right) = if item_avoids_floats {
+                    (
+                        resolved_margin.left - item_non_auto_margin.left,
+                        resolved_margin.right - item_non_auto_margin.right,
+                    )
+                } else {
+                    (resolved_margin.left, resolved_margin.right)
+                };
+                #[cfg(not(feature = "float_layout"))]
+                let (extra_margin_left, extra_margin_right) = (resolved_margin.left, resolved_margin.right);
+
                 // TODO: handle inset and margins
                 Point {
                     x: match direction {
-                        Direction::Ltr => float_avoiding_position.x + resolved_margin.left + inset_offset.x,
+                        Direction::Ltr => float_avoiding_position.x + extra_margin_left + inset_offset.x,
                         Direction::Rtl => {
-                            float_avoiding_position.x + float_avoiding_width - final_size.width - resolved_margin.right
+                            float_avoiding_position.x + float_avoiding_width - final_size.width - extra_margin_right
                                 + inset_offset.x
                         }
                     },
@@ -1121,6 +1197,12 @@ fn perform_final_layout_on_in_flow_children(
             }
 
             // Update first_child_top_margin_set
+            #[cfg(feature = "float_layout")]
+            if is_collapsing_with_first_margin_set && item_pushed_below_float {
+                // The item's top margin "separated from the float" and must not
+                // propagate to the parent
+                is_collapsing_with_first_margin_set = false;
+            }
             if is_collapsing_with_first_margin_set {
                 if item.can_be_collapsed_through {
                     first_child_top_margin_set = first_child_top_margin_set
diff --git a/src/compute/float.rs b/src/compute/float.rs
index e565ac0e5..f44edfb28 100644
--- a/src/compute/float.rs
+++ b/src/compute/float.rs
@@ -28,7 +28,7 @@
 
 use core::ops::Range;
 
-use crate::{debug::debug_log, sys::Vec, AvailableSpace, Clear, FloatDirection, Point, Size};
+use crate::{debug::debug_log, sys::Vec, AvailableSpace, Clear, Direction, FloatDirection, Point, Size};
 
 /// An empty "slot" that avoids floats that is suitable for non-floated content
 /// to be laid out into
@@ -46,6 +46,29 @@ pub struct ContentSlot {
     pub height: f32,
 }
 
+/// An empty "slot" that avoids floats that is suitable for a box that establishes
+/// an independent formatting context (and therefore must not overlap floats) to be
+/// laid out into. Unlike [`ContentSlot`], this accounts for the box's own margins,
+/// which are resolved against the containing block's edges (not float edges) and
+/// may therefore overlap floats.
+#[derive(Debug, Clone, Copy, Default)]
+pub struct BfcSlot {
+    /// The id of the segment that the slot starts in
+    pub segment_id: Option<usize>,
+    /// The x position of the start of the slot (border box edge)
+    pub x: f32,
+    /// The y position of the start of the slot
+    pub y: f32,
+    /// The space available for the box's border box: the space between float edges
+    /// and the margin-inset containing block edges. The box fits in the slot if its
+    /// border box width does not exceed this.
+    pub border_width: f32,
+    /// The width that an auto width resolves to (before applying min/max constraints and
+    /// the negative-margin lower bound). This differs from `border_width` in that the
+    /// trailing margin is subtracted (except for any part of it that overlaps a float).
+    pub stretch_width: f32,
+}
+
 /// A floated box
 #[derive(Debug, Clone, Default)]
 pub struct PlacedFloatedBox {
@@ -518,6 +541,84 @@ impl FloatContext {
             },
         }
     }
+
+    /// Search for a space suitable for laying out a box that establishes an independent
+    /// formatting context (whose border box must not overlap floats).
+    ///
+    /// The box's margins are resolved against the containing block's content edges. When there
+    /// are floats beside the box:
+    ///
+    ///   - The leading margin (in flow direction) positions the border box's leading edge, but
+    ///     may be partially or fully "absorbed" by a float on the leading side (the border edge
+    ///     is the further-in of the float edge and the margin-inset containing block edge), and
+    ///     a negative leading margin does not move the border edge past the float edge or the
+    ///     containing block edge.
+    ///   - The trailing margin is subtracted from the width an auto width resolves to (except
+    ///     for any part of it that overlaps a float on the trailing side), but does not affect
+    ///     whether a box of a given width fits in the slot: the trailing margin may overflow
+    ///     the containing block edge.
+    ///
+    /// When there are no floats beside the box, its (possibly negative) margins apply as usual.
+    pub fn find_bfc_slot(
+        &self,
+        min_y: f32,
+        containing_block_insets: [f32; 2],
+        margins: [f32; 2],
+        direction: Direction,
+        clear: Clear,
+        after: Option<usize>,
+    ) -> BfcSlot {
+        let margin_insets = [containing_block_insets[0] + margins[0], containing_block_insets[1] + margins[1]];
+        let no_float_width = self.available_width - margin_insets[0] - margin_insets[1];
+        let no_float_slot = BfcSlot {
+            segment_id: None,
+            x: margin_insets[0],
+            y: min_y,
+            border_width: no_float_width,
+            stretch_width: no_float_width,
+        };
+
+        if !self.has_active_floats(min_y) {
+            return no_float_slot;
+        }
+
+        // The min starting segment index
+        let at_least = after.map(|idx| idx + 1).unwrap_or(0);
+        let hwm = at_least.max(self.cleared_segment(clear).map(|idx| idx + 1).unwrap_or(0));
+
+        let start_idx = self
+            .segments
+            .get(hwm..)
+            .and_then(|segments| segments.iter().position(|segment| segment.y.end > min_y).map(|idx| idx + hwm));
+        let start_idx = start_idx.unwrap_or(self.segments.len());
+        match self.segments.get(start_idx) {
+            Some(segment) => {
+                let lead = match direction {
+                    Direction::Ltr => 0,
+                    Direction::Rtl => 1,
+                };
+                let trail = 1 - lead;
+                let mut fit_insets = [0.0; 2];
+                let mut stretch_insets = [0.0; 2];
+                fit_insets[lead] = segment.insets[lead].max(containing_block_insets[lead]).max(margin_insets[lead]);
+                stretch_insets[lead] = fit_insets[lead];
+                fit_insets[trail] = segment.insets[trail].max(containing_block_insets[trail]);
+                stretch_insets[trail] = fit_insets[trail].max(containing_block_insets[trail] + margins[trail].max(0.0));
+                BfcSlot {
+                    segment_id: Some(start_idx),
+                    x: fit_insets[0],
+                    y: segment.y.start.max(min_y),
+                    border_width: self.available_width - fit_insets[0] - fit_insets[1],
+                    stretch_width: self.available_width - stretch_insets[0] - stretch_insets[1],
+                }
+            }
+            // Below all floats
+            None => BfcSlot {
+                y: self.segments.last().map(|segment| segment.y.end).unwrap_or(min_y).max(min_y),
+                ..no_float_slot
+            },
+        }
+    }
 }
 
 /// Context for computing the intrinsic width contribution of a set of floats
diff --git a/tests/hand_written.rs b/tests/hand_written.rs
index 150f7845f..f84008e8a 100644
--- a/tests/hand_written.rs
+++ b/tests/hand_written.rs
@@ -1,4 +1,5 @@
 mod hand_written {
+    mod bfc_beside_floats;
     mod border_and_padding;
     mod caching;
     mod measure;
diff --git a/tests/hand_written/bfc_beside_floats.rs b/tests/hand_written/bfc_beside_floats.rs
new file mode 100644
index 000000000..a2588def8
--- /dev/null
+++ b/tests/hand_written/bfc_beside_floats.rs
@@ -0,0 +1,197 @@
+//! Tests for boxes that establish an independent formatting context (e.g. `overflow: hidden`)
+//! laid out beside floats. Per CSS2 §9.5 the border box of such a box must not overlap any
+//! floats in the same block formatting context: it either narrows to fit beside the float or
+//! moves down below it.
+#![cfg(feature = "float_layout")]
+
+use taffy::prelude::*;
+use taffy::{Direction, Float, Overflow, Point, Rect};
+
+fn block() -> Style {
+    Style { display: taffy::Display::Block, ..Default::default() }
+}
+
+fn bfc_root() -> Style {
+    Style { overflow: Point { x: Overflow::Hidden, y: Overflow::Hidden }, ..block() }
+}
+
+fn container(taffy: &mut TaffyTree<()>, width: f32, direction: Direction, children: &[NodeId]) -> NodeId {
+    taffy
+        .new_with_children(
+            Style { size: Size { width: length(width), height: auto() }, direction, ..block() },
+            children,
+        )
+        .unwrap()
+}
+
+fn float_left(taffy: &mut TaffyTree<()>, width: f32, height: f32) -> NodeId {
+    taffy
+        .new_leaf(Style { float: Float::Left, size: Size { width: length(width), height: length(height) }, ..block() })
+        .unwrap()
+}
+
+fn compute(taffy: &mut TaffyTree<()>, root: NodeId) {
+    taffy.compute_layout(root, Size::MAX_CONTENT).unwrap();
+}
+
+fn assert_rect(taffy: &TaffyTree<()>, node: NodeId, x: f32, y: f32, width: f32) {
+    let layout = taffy.layout(node).unwrap();
+    assert_eq!(layout.location.x, x, "x of {node:?}");
+    assert_eq!(layout.location.y, y, "y of {node:?}");
+    assert_eq!(layout.size.width, width, "width of {node:?}");
+}
+
+/// An auto-width BFC root fits beside the float and stretches to fill the remaining space
+#[test]
+fn bfc_narrows_beside_float() {
+    let mut taffy: TaffyTree<()> = TaffyTree::new();
+    let float = float_left(&mut taffy, 50.0, 100.0);
+    let bfc = taffy.new_leaf(Style { size: Size { width: auto(), height: length(50.0) }, ..bfc_root() }).unwrap();
+    let root = container(&mut taffy, 100.0, Direction::Ltr, &[float, bfc]);
+    compute(&mut taffy, root);
+    assert_rect(&taffy, bfc, 50.0, 0.0, 50.0);
+}
+
+/// A fixed-width BFC root that is too wide to fit beside the float moves below it
+#[test]
+fn bfc_moves_below_float() {
+    let mut taffy: TaffyTree<()> = TaffyTree::new();
+    let float = float_left(&mut taffy, 150.0, 150.0);
+    let bfc =
+        taffy.new_leaf(Style { size: Size { width: length(200.0), height: length(150.0) }, ..bfc_root() }).unwrap();
+    let root = container(&mut taffy, 300.0, Direction::Ltr, &[float, bfc]);
+    compute(&mut taffy, root);
+    assert_rect(&taffy, bfc, 0.0, 150.0, 200.0);
+    assert_eq!(taffy.layout(root).unwrap().size.height, 300.0);
+}
+
+/// A BFC root with a large top margin separates from an adjoining float (similar to clearance):
+/// the margin positions the top of the float, and the BFC is placed below the float
+#[test]
+fn bfc_separates_from_adjoining_float() {
+    let mut taffy: TaffyTree<()> = TaffyTree::new();
+    let float = float_left(&mut taffy, 200.0, 200.0);
+    let inner = taffy.new_with_children(block(), &[float]).unwrap();
+    let bfc = taffy
+        .new_leaf(Style {
+            size: Size { width: length(200.0), height: length(1.0) },
+            margin: Rect { top: length(200.0), left: zero(), right: zero(), bottom: zero() },
+            ..bfc_root()
+        })
+        .unwrap();
+    let wrapper = taffy.new_with_children(block(), &[inner, bfc]).unwrap();
+    let root = taffy
+        .new_with_children(Style { size: Size { width: length(200.0), height: auto() }, ..bfc_root() }, &[wrapper])
+        .unwrap();
+    compute(&mut taffy, root);
+    assert_rect(&taffy, float, 0.0, 0.0, 200.0);
+    assert_rect(&taffy, bfc, 0.0, 200.0, 200.0);
+    assert_eq!(taffy.layout(root).unwrap().size.height, 201.0);
+}
+
+/// A small negative margin on the float side does not pull an auto-width BFC root under the
+/// float: the border edge does not move past the float edge
+#[test]
+fn bfc_negative_margin_beside_float_is_clamped() {
+    let mut taffy: TaffyTree<()> = TaffyTree::new();
+    let float = float_left(&mut taffy, 50.0, 100.0);
+    let bfc = taffy
+        .new_leaf(Style {
+            size: Size { width: auto(), height: length(100.0) },
+            margin: Rect { left: length(-20.0), right: zero(), top: zero(), bottom: zero() },
+            ..bfc_root()
+        })
+        .unwrap();
+    let root = container(&mut taffy, 100.0, Direction::Ltr, &[float, bfc]);
+    compute(&mut taffy, root);
+    assert_rect(&taffy, bfc, 50.0, 0.0, 50.0);
+}
+
+/// A large negative margin makes an auto-width BFC root too wide to fit beside the float
+/// (its width must be at least the negation of its margins), so it moves below the float
+/// and resolves its width against the full containing block
+#[test]
+fn bfc_large_negative_margin_moves_below_float() {
+    let mut taffy: TaffyTree<()> = TaffyTree::new();
+    let float = float_left(&mut taffy, 50.0, 100.0);
+    let bfc = taffy
+        .new_leaf(Style {
+            size: Size { width: auto(), height: length(50.0) },
+            margin: Rect { left: length(-60.0), right: zero(), top: zero(), bottom: zero() },
+            ..bfc_root()
+        })
+        .unwrap();
+    let root = container(&mut taffy, 100.0, Direction::Ltr, &[float, bfc]);
+    compute(&mut taffy, root);
+    assert_rect(&taffy, bfc, -60.0, 100.0, 160.0);
+}
+
+/// A positive margin on the float side is "absorbed" by the float: the border edge is the
+/// further-in of the float edge and the margin-inset containing block edge
+#[test]
+fn bfc_positive_margin_absorbed_by_float() {
+    let mut taffy: TaffyTree<()> = TaffyTree::new();
+    let float = float_left(&mut taffy, 50.0, 100.0);
+    let bfc = taffy
+        .new_leaf(Style {
+            size: Size { width: auto(), height: length(50.0) },
+            margin: Rect { left: length(20.0), right: zero(), top: zero(), bottom: zero() },
+            ..bfc_root()
+        })
+        .unwrap();
+    let root = container(&mut taffy, 100.0, Direction::Ltr, &[float, bfc]);
+    compute(&mut taffy, root);
+    assert_rect(&taffy, bfc, 50.0, 0.0, 50.0);
+}
+
+/// A trailing margin reduces the width an auto width resolves to, but does not prevent a
+/// fixed-width box from fitting beside the float (it may overflow the containing block)
+#[test]
+fn bfc_trailing_margin() {
+    let mut taffy: TaffyTree<()> = TaffyTree::new();
+
+    // Auto width: trailing margin is subtracted from the stretch width
+    let float = float_left(&mut taffy, 50.0, 100.0);
+    let bfc = taffy
+        .new_leaf(Style {
+            size: Size { width: auto(), height: length(50.0) },
+            margin: Rect { right: length(10.0), left: zero(), top: zero(), bottom: zero() },
+            ..bfc_root()
+        })
+        .unwrap();
+    let root = container(&mut taffy, 100.0, Direction::Ltr, &[float, bfc]);
+    compute(&mut taffy, root);
+    assert_rect(&taffy, bfc, 50.0, 0.0, 40.0);
+
+    // Fixed width filling the whole space beside the float: the trailing margin
+    // does not force the box below the float
+    let float = float_left(&mut taffy, 50.0, 100.0);
+    let bfc = taffy
+        .new_leaf(Style {
+            size: Size { width: length(50.0), height: length(50.0) },
+            margin: Rect { right: length(10.0), left: zero(), top: zero(), bottom: zero() },
+            ..bfc_root()
+        })
+        .unwrap();
+    let root = container(&mut taffy, 100.0, Direction::Ltr, &[float, bfc]);
+    compute(&mut taffy, root);
+    assert_rect(&taffy, bfc, 50.0, 0.0, 50.0);
+}
+
+/// In an RTL container, a negative margin on the trailing (left) side with a left float does
+/// not prevent the BFC root from fitting beside the float
+#[test]
+fn bfc_beside_float_rtl() {
+    let mut taffy: TaffyTree<()> = TaffyTree::new();
+    let float = float_left(&mut taffy, 50.0, 100.0);
+    let bfc = taffy
+        .new_leaf(Style {
+            size: Size { width: auto(), height: length(100.0) },
+            margin: Rect { left: length(-20.0), right: zero(), top: zero(), bottom: zero() },
+            ..bfc_root()
+        })
+        .unwrap();
+    let root = container(&mut taffy, 100.0, Direction::Rtl, &[float, bfc]);
+    compute(&mut taffy, root);
+    assert_rect(&taffy, bfc, 50.0, 0.0, 50.0);
+}