gioser

tawasuyu/taffy

A high performance rust-powered UI layout library

Rust 497HTML 254C 19JavaScript 81329 Definitions

Fix clearance calculation and its interaction with margin collapsing

df46fcf3630e2e8f4ea2c99bbe261154383a4aab · by Nico Burns · (3 months ago) · parent 86d93d95b · browse the tree at this commit

Semantic delta · 21

src/compute/block.rs

src/compute/float.rs

tests/hand_written.rs

5 files · +478 −62

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a479e60ce..a981ea087 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,19 @@
 
 ## Unreleased
 
+### Changed
+
+- Block/float: `BlockContext::place_floated_box` takes an additional `adjoins_unresolved_strut: bool` parameter indicating whether the float is being placed while the position of the current margin-collapse strut is still unresolved
+
 ### Fixed
 
+- Block: clearance is now computed per CSS2.2 §9.5.2 from the hypothetical position of the cleared element (including its collapsed top margin), supporting negative clearance and suppressing clearance when a large top margin already places the element past the float
+- Block: `clear` on an element no longer has any effect when no float has been placed on the relevant side(s)
+- Block: clearance prevents the cleared element's top margin from collapsing with preceding margins and with the parent's top margin
+- Block: the top and bottom margins of a self-collapsing element with clearance collapse with each other and are applied inside the parent (they no longer collapse with the parent's bottom margin)
+- Block: floats placed while the position of the enclosing margin-collapse strut is unresolved force clearance on subsequent cleared elements whose margins adjoin the same strut
+- Block: an element containing only floated children can now be collapsed through
+- Block: a block establishing a new formatting context that does not fit beside a float is pushed below it and its top margin no longer collapses with preceding margins
 - Numeric style helpers (`length`, `percent`, `fr`, `flex`) now accept `Input: Into<f64>` instead of `Input: Into<f32>`. This allows bare float literals such as `length(800.0)` to be used without triggering the `float_literal_f32_fallback` future-compatibility lint, while widening the set of accepted numeric input types (#974)
 
 ## 0.12.2
diff --git a/src/compute/block.rs b/src/compute/block.rs
index 21c511eff..f437a145b 100644
--- a/src/compute/block.rs
+++ b/src/compute/block.rs
@@ -50,6 +50,10 @@ impl BlockFormattingContext {
             content_box_insets: [0.0, 0.0],
             float_content_contribution: 0.0,
             is_root: true,
+            #[cfg(feature = "float_layout")]
+            adjoining_floats: [false, false],
+            #[cfg(feature = "float_layout")]
+            top_adjoining_floats: None,
         }
     }
 }
@@ -71,6 +75,17 @@ pub struct BlockContext<'bfc> {
     float_content_contribution: f32,
     /// Whether the node is the root of the Block Formatting Context is belongs to.
     is_root: bool,
+    /// Whether a float has been placed (on each side) whose position adjoins the current
+    /// margin-collapse strut of this block (i.e. whose final position can still be moved by
+    /// margins that collapse into that strut). Such floats force clearance on cleared elements
+    /// whose margins adjoin the same strut.
+    #[cfg(feature = "float_layout")]
+    adjoining_floats: [bool; 2],
+    /// The value of `adjoining_floats` frozen at the first point at which in-flow content was
+    /// committed within this block (resolving the position of the block's top margin strut).
+    /// `None` if no in-flow content has been committed yet.
+    #[cfg(feature = "float_layout")]
+    top_adjoining_floats: Option<[bool; 2]>,
 }
 
 impl BlockContext<'_> {
@@ -84,6 +99,12 @@ impl BlockContext<'_> {
             content_box_insets: insets,
             float_content_contribution: 0.0,
             is_root: false,
+            // Floats adjoining the parent's current strut also adjoin this block's top strut
+            // (if this block's top margin collapses with its first child's, which is checked separately)
+            #[cfg(feature = "float_layout")]
+            adjoining_floats: self.adjoining_floats,
+            #[cfg(feature = "float_layout")]
+            top_adjoining_floats: None,
         }
     }
 
@@ -130,7 +151,11 @@ impl BlockContext<'_> {
         min_y: f32,
         direction: FloatDirection,
         clear: Clear,
+        adjoins_unresolved_strut: bool,
     ) -> Point<f32> {
+        if adjoins_unresolved_strut {
+            self.adjoining_floats[direction as usize] = true;
+        }
         let mut pos = self.bfc.float_context.place_floated_box(
             floated_box,
             min_y + self.y_offset,
@@ -160,6 +185,39 @@ impl BlockContext<'_> {
         self.bfc.float_context.cleared_threshold(clear).map(|threshold| threshold - self.y_offset)
     }
 
+    /// Whether a float that is adjoining the current margin-collapse strut has been placed
+    /// on the side(s) relevant to the passed clear property
+    pub fn has_adjoining_float(&self, clear: Clear) -> bool {
+        match clear {
+            Clear::Left => self.adjoining_floats[0],
+            Clear::Right => self.adjoining_floats[1],
+            Clear::Both => self.adjoining_floats[0] || self.adjoining_floats[1],
+            Clear::None => false,
+        }
+    }
+
+    /// Merge adjoining float flags propagated from a child block into this block's flags
+    fn merge_adjoining_floats(&mut self, flags: [bool; 2]) {
+        self.adjoining_floats[0] |= flags[0];
+        self.adjoining_floats[1] |= flags[1];
+    }
+
+    /// Record that in-flow content has been committed within this block, resolving the position of
+    /// the current margin-collapse strut. Floats placed before this point no longer adjoin the
+    /// current strut. The flags for the block's top strut are frozen at the first commit.
+    fn commit_strut(&mut self) {
+        if self.top_adjoining_floats.is_none() {
+            self.top_adjoining_floats = Some(self.adjoining_floats);
+        }
+        self.adjoining_floats = [false, false];
+    }
+
+    /// The adjoining float flags for this block's top margin strut: floats placed while the
+    /// position of the block's top strut was still unresolved
+    fn top_adjoining_floats(&self) -> [bool; 2] {
+        self.top_adjoining_floats.unwrap_or(self.adjoining_floats)
+    }
+
     /// Update the height that descendent floats with the height that floats consume
     /// within a particular child
     fn add_child_floated_content_height_contribution(&mut self, child_contribution: f32) {
@@ -535,8 +593,13 @@ fn compute_inner(
     }
 
     // Determine whether this node can be collapsed through
-    let all_in_flow_children_can_be_collapsed_through =
-        items.iter().all(|item| item.position == Position::Absolute || item.can_be_collapsed_through);
+    let all_in_flow_children_can_be_collapsed_through = items.iter().all(|item| {
+        #[cfg(feature = "float_layout")]
+        if item.float.is_floated() {
+            return true;
+        }
+        item.position == Position::Absolute || item.can_be_collapsed_through
+    });
     let can_be_collapsed_through =
         !has_styles_preventing_being_collapsed_through && all_in_flow_children_can_be_collapsed_through;
 
@@ -783,6 +846,14 @@ fn perform_final_layout_on_in_flow_children(
         block_ctx.apply_content_box_inset([resolved_content_box_inset.left, resolved_content_box_inset.right]);
     }
 
+    // If this block's top margin does not collapse with its children's then the position of its
+    // top margin strut is resolved relative to it, and floats adjoining ancestor struts do not
+    // adjoin this block's strut.
+    #[cfg(feature = "float_layout")]
+    if !own_margins_collapse_with_children.start {
+        block_ctx.commit_strut();
+    }
+
     #[cfg_attr(not(feature = "content_size"), allow(unused_mut))]
     let mut inflow_content_size = Size::ZERO;
     let mut committed_y_offset = resolved_content_box_inset.top;
@@ -790,13 +861,10 @@ fn perform_final_layout_on_in_flow_children(
     let mut first_child_top_margin_set = CollapsibleMarginSet::ZERO;
     let mut active_collapsible_margin_set = CollapsibleMarginSet::ZERO;
     let mut is_collapsing_with_first_margin_set = true;
-
-    #[cfg(feature = "float_layout")]
-    let mut has_active_floats = block_ctx.has_active_floats(committed_y_offset);
-    #[cfg(not(feature = "float_layout"))]
-    let has_active_floats = false;
-    #[cfg(feature = "float_layout")]
-    let mut y_offset_for_float = resolved_content_box_inset.top;
+    // Whether the active margin set contains the margins of a self-collapsing element with
+    // clearance. Such margins collapse with the margins of following siblings but the resulting
+    // margin does not collapse with the bottom margin of the parent block.
+    let mut active_margin_set_has_clearance = false;
 
     for item in items.iter_mut() {
         if item.position == Position::Absolute {
@@ -820,8 +888,6 @@ fn perform_final_layout_on_in_flow_children(
             // Handle floated boxes
             #[cfg(feature = "float_layout")]
             if let Some(float_direction) = item.float.float_direction() {
-                has_active_floats = true;
-
                 let item_layout = tree.perform_child_layout(
                     item.node_id,
                     Size::NONE,
@@ -833,8 +899,29 @@ fn perform_final_layout_on_in_flow_children(
                 );
                 let margin_box = item_layout.size + item_non_auto_margin.sum_axes();
 
-                let mut location =
-                    block_ctx.place_floated_box(margin_box, y_offset_for_float, float_direction, item.clear);
+                // Floats that occur between collapsing margins are positioned as if they had an otherwise
+                // empty anonymous block parent taking part in the flow, so the pending collapsible margins
+                // contribute to the float's minimum y position (unless those margins collapse with the
+                // container's own top margin, in which case they are applied outside the container).
+                //
+                // In the latter case the position of the float is not fully resolved: margins contributed
+                // by later siblings can still collapse into the strut and move the container (and float).
+                // Such floats force clearance on cleared elements whose margins adjoin the same strut.
+                let adjoins_unresolved_strut =
+                    is_collapsing_with_first_margin_set && own_margins_collapse_with_children.start;
+                let y_offset_for_float = if adjoins_unresolved_strut {
+                    committed_y_offset
+                } else {
+                    committed_y_offset + active_collapsible_margin_set.resolve()
+                };
+
+                let mut location = block_ctx.place_floated_box(
+                    margin_box,
+                    y_offset_for_float,
+                    float_direction,
+                    item.clear,
+                    adjoins_unresolved_strut,
+                );
 
                 // Ensure that content that appears after a float does not get positioned before/above the float
                 //
@@ -842,7 +929,6 @@ fn perform_final_layout_on_in_flow_children(
                 // shouldn't cause content to push down to it's level
                 // committed_y_offset = committed_y_offset.max(location.y);
                 // y_offset_for_absolute = y_offset_for_absolute.max(location.y);
-                // y_offset_for_float = y_offset_for_float.max(location.y);
 
                 // Convert the margin-box location returned by float placement into a border-box location
                 // for the output Layout
@@ -885,6 +971,8 @@ 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 bfc_margin_separated = 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;
@@ -895,30 +983,67 @@ fn perform_final_layout_on_in_flow_children(
             } else {
                 'block: {
                     // Set y_margin_offset (different bfc child)
-                    if !is_collapsing_with_first_margin_set || !own_margins_collapse_with_children.start {
+                    let margins_escape =
+                        is_collapsing_with_first_margin_set && own_margins_collapse_with_children.start;
+                    if !margins_escape {
                         y_margin_offset =
                             active_collapsible_margin_set.collapse_with_margin(item_non_auto_margin.top).resolve();
                     };
                     let min_y = committed_y_offset + y_margin_offset;
 
                     #[cfg(feature = "float_layout")]
-                    if has_active_floats {
-                        let slot = block_ctx.find_content_slot(min_y, item.clear, None);
-                        has_active_floats = slot.segment_id.is_some();
+                    if block_ctx.has_active_floats(committed_y_offset.min(min_y)) {
+                        let mut slot = block_ctx.find_content_slot(min_y, item.clear, None);
+
+                        // A new formatting context whose border box does not fit beside the floats at its
+                        // hypothetical position (with margins collapsed) must be pushed below the floats,
+                        // separating its margin from them (similar to clearance): the margin no longer
+                        // collapses with preceding margins and does not move the floats.
+                        if margins_escape {
+                            if let Some(item_width) = item.size.width {
+                                // Only the item's border box must not overlap the floats:
+                                // its margins may overflow the available space
+                                let outer_width = item_width;
+                                // If a float adjoins the strut that this item's margin would collapse into,
+                                // the collapsed margin would move the float together with the item, so the
+                                // fit check is made at the strut's unresolved position (where the float is).
+                                let hypothetical_min_y = if block_ctx.has_adjoining_float(Clear::Both) {
+                                    committed_y_offset
+                                } else {
+                                    committed_y_offset
+                                        + active_collapsible_margin_set
+                                            .collapse_with_margin(item_non_auto_margin.top)
+                                            .resolve()
+                                };
+                                let mut candidate = block_ctx.find_content_slot(hypothetical_min_y, item.clear, None);
+                                if candidate.segment_id.is_some() && outer_width > candidate.width {
+                                    while candidate.segment_id.is_some() && outer_width > candidate.width {
+                                        candidate = block_ctx.find_content_slot(
+                                            hypothetical_min_y,
+                                            item.clear,
+                                            candidate.segment_id,
+                                        );
+                                    }
+                                    bfc_margin_separated = true;
+                                    // The preceding margins (excluding this item's, which is now separated)
+                                    // still escape and move the container; compensate so that the item's
+                                    // absolute position stays at the found slot.
+                                    slot = candidate;
+                                    slot.y -= active_collapsible_margin_set.resolve();
+                                }
+                            }
+                        }
+
                         let stretch_width = slot.width - item_non_auto_x_margin_sum;
                         break 'block (stretch_width, Point { x: slot.x, y: slot.y }, slot.width);
                     }
 
-                    if !has_active_floats {
-                        let stretch_width = container_inner_width - item_non_auto_x_margin_sum;
-                        break 'block (
-                            stretch_width,
-                            Point { x: resolved_content_box_inset.left, y: min_y },
-                            container_inner_width,
-                        );
-                    }
-
-                    unreachable!("One of the above cases will always be hit");
+                    let stretch_width = container_inner_width - item_non_auto_x_margin_sum;
+                    break 'block (
+                        stretch_width,
+                        Point { x: resolved_content_box_inset.left, y: min_y },
+                        container_inner_width,
+                    );
                 }
             };
 
@@ -947,7 +1072,9 @@ fn perform_final_layout_on_in_flow_children(
             };
 
             #[cfg(feature = "float_layout")]
-            let clear_pos = block_ctx.cleared_threshold(item.clear).unwrap_or(f32::NEG_INFINITY);
+            let clear_threshold = block_ctx.cleared_threshold(item.clear);
+            #[cfg(feature = "float_layout")]
+            let clear_pos = clear_threshold.unwrap_or(f32::NEG_INFINITY);
             #[cfg(not(feature = "float_layout"))]
             let clear_pos = f32::NEG_INFINITY;
 
@@ -970,7 +1097,11 @@ fn perform_final_layout_on_in_flow_children(
                 #[cfg(feature = "float_layout")]
                 {
                     let child_contribution = child_block_ctx.floated_content_height_contribution();
+                    let child_top_adjoining_floats = child_block_ctx.top_adjoining_floats();
                     block_ctx.add_child_floated_content_height_contribution(y_offset_for_absolute + child_contribution);
+                    // Floats placed while the position of the child's top margin strut was unresolved
+                    // also adjoin this block's current strut
+                    block_ctx.merge_adjoining_floats(child_top_adjoining_floats);
                 }
 
                 output
@@ -1018,16 +1149,56 @@ fn perform_final_layout_on_in_flow_children(
             if item.is_in_same_bfc
                 && (!is_collapsing_with_first_margin_set || !own_margins_collapse_with_children.start)
             {
-                y_margin_offset = active_collapsible_margin_set.collapse_with_margin(resolved_margin.top).resolve()
+                y_margin_offset = active_collapsible_margin_set.collapse_with_set(top_margin_set).resolve()
             };
 
+            // Compute clearance (CSS2.2 9.5.2). Clearance is introduced if the hypothetical position of the
+            // item's top border edge (the position it would have with normal margin collapsing) is not past
+            // the bottom of the relevant floats. When clearance is introduced the item's top margin no longer
+            // collapses with preceding margins, and the item's border edge is placed at
+            // `max(float bottom, hypothetical position)`.
             #[cfg(feature = "float_layout")]
-            let float_or_not_clear = item.float.is_floated() || item.clear == Clear::None;
+            let mut has_clearance = false;
             #[cfg(not(feature = "float_layout"))]
-            let float_or_not_clear = true;
+            let has_clearance = false;
+            #[cfg(feature = "float_layout")]
+            if item.is_in_same_bfc {
+                if let Some(threshold) = clear_threshold {
+                    // The hypothetical position always includes the item's collapsed top margin set, even
+                    // when those margins collapse with the container's own top margin (and are thus applied
+                    // outside the container): in that case they still move the container (and hence the item)
+                    // relative to the floats.
+                    let hypothetical_y =
+                        committed_y_offset + active_collapsible_margin_set.collapse_with_set(top_margin_set).resolve();
+                    // Clearance is forced (regardless of the hypothetical position) if a relevant float is
+                    // adjoining the margin-collapse strut that the item's top margin would collapse into:
+                    // if the margins were allowed to collapse they would pull the float down with the item,
+                    // so clearance is inserted to separate the two, placing the item just below the float.
+                    let forced_clearance = block_ctx.has_adjoining_float(item.clear);
+                    if forced_clearance || hypothetical_y < threshold {
+                        has_clearance = true;
+                        // Clearance stops the item's top margin collapsing with preceding margins. If those
+                        // preceding margins collapse with the container's own top margin they are applied
+                        // outside the container (moving it down), so the item's cleared position within the
+                        // container must be reduced by that amount to keep its absolute position correct.
+                        let escaped_margin =
+                            if is_collapsing_with_first_margin_set && own_margins_collapse_with_children.start {
+                                active_collapsible_margin_set.resolve()
+                            } else {
+                                0.0
+                            };
+                        y_margin_offset = threshold - committed_y_offset - escaped_margin;
+                    }
+                }
+            }
+            #[cfg(feature = "float_layout")]
+            {
+                // A new-formatting-context item pushed below floats separates its margin like clearance does
+                has_clearance |= bfc_margin_separated;
+            }
 
             item.computed_size = item_layout.size;
-            item.can_be_collapsed_through = item_layout.margins_can_collapse_through && float_or_not_clear;
+            item.can_be_collapsed_through = item_layout.margins_can_collapse_through && !has_clearance;
             item.static_position = if item.is_in_same_bfc {
                 let uncleared_y = committed_y_offset + active_collapsible_margin_set.resolve();
                 Point {
@@ -1059,7 +1230,7 @@ fn perform_final_layout_on_in_flow_children(
                                 + inset_offset.x
                         }
                     },
-                    y: (committed_y_offset + y_margin_offset).max(clear_pos) + inset_offset.y,
+                    y: committed_y_offset + y_margin_offset + inset_offset.y,
                 }
             } else {
                 // TODO: handle inset and margins
@@ -1121,7 +1292,12 @@ fn perform_final_layout_on_in_flow_children(
             }
 
             // Update first_child_top_margin_set
-            if is_collapsing_with_first_margin_set {
+            //
+            // The top margin of an item with clearance does not collapse with the container's top margin,
+            // so clearance terminates collapsing without contributing the item's own margins.
+            if is_collapsing_with_first_margin_set && has_clearance {
+                is_collapsing_with_first_margin_set = false;
+            } else if is_collapsing_with_first_margin_set {
                 if item.can_be_collapsed_through {
                     first_child_top_margin_set = first_child_top_margin_set
                         .collapse_with_set(top_margin_set)
@@ -1138,25 +1314,42 @@ fn perform_final_layout_on_in_flow_children(
                     .collapse_with_set(top_margin_set)
                     .collapse_with_set(bottom_margin_set);
                 y_offset_for_absolute = committed_y_offset + item_layout.size.height + y_margin_offset;
-                #[cfg(feature = "float_layout")]
-                {
-                    y_offset_for_float = committed_y_offset + item_layout.size.height + y_margin_offset;
-                }
             } else {
                 committed_y_offset = location.y - inset_offset.y + item_layout.size.height;
-                active_collapsible_margin_set = bottom_margin_set;
+                // A self-collapsing item with clearance is not collapsed through (its margins do not collapse
+                // with margins of preceding siblings), but its top and bottom margins still collapse with each
+                // other and with the margins of following siblings.
+                if has_clearance && item_layout.margins_can_collapse_through {
+                    // The element's border edge stays at the cleared position, but its collapsed margin
+                    // extends below it: the border edge sits `top margin` inside the collapsed margin, so
+                    // following content is offset by `collapsed margin - top margin` from the border edge.
+                    committed_y_offset -= top_margin_set.resolve();
+                    active_collapsible_margin_set = top_margin_set.collapse_with_set(bottom_margin_set);
+                    active_margin_set_has_clearance = true;
+                } else {
+                    active_collapsible_margin_set = bottom_margin_set;
+                    active_margin_set_has_clearance = false;
+                }
                 y_offset_for_absolute = committed_y_offset + active_collapsible_margin_set.resolve();
+                // Committing in-flow content resolves the position of the current margin-collapse strut,
+                // so floats placed before this point no longer force clearance
                 #[cfg(feature = "float_layout")]
-                {
-                    y_offset_for_float = committed_y_offset + active_collapsible_margin_set.resolve();
-                }
+                block_ctx.commit_strut();
             }
         }
     }
 
-    let last_child_bottom_margin_set = active_collapsible_margin_set;
-    let bottom_y_margin_offset =
-        if own_margins_collapse_with_children.end { 0.0 } else { last_child_bottom_margin_set.resolve() };
+    // The margins of a self-collapsing element with clearance do not collapse with the bottom
+    // margin of the parent block: they extend the parent's content height instead of escaping it
+    let last_child_bottom_margin_set =
+        if active_margin_set_has_clearance { CollapsibleMarginSet::ZERO } else { active_collapsible_margin_set };
+    let bottom_y_margin_offset = if active_margin_set_has_clearance {
+        active_collapsible_margin_set.resolve()
+    } else if own_margins_collapse_with_children.end {
+        0.0
+    } else {
+        last_child_bottom_margin_set.resolve()
+    };
 
     committed_y_offset += resolved_content_box_inset.bottom + bottom_y_margin_offset;
     let content_height = f32_max(0.0, committed_y_offset);
diff --git a/src/compute/float.rs b/src/compute/float.rs
index 107670780..594533006 100644
--- a/src/compute/float.rs
+++ b/src/compute/float.rs
@@ -462,22 +462,22 @@ impl FloatContext {
     }
 
     /// Get the end segment of the last float on side(s) specified by the clear parameter (if any)
+    ///
+    /// Returns `None` if no float has been placed on the relevant side(s)
     fn cleared_segment(&self, clear: Clear) -> Option<usize> {
+        let left_end = self.last_placed_floats[0].end;
+        let right_end = self.last_placed_floats[1].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::None => None,
+            Clear::Left if left_end > 0 => Some(left_end),
+            Clear::Right if right_end > 0 => Some(right_end),
+            Clear::Both if left_end > 0 || right_end > 0 => Some(left_end.max(right_end)),
+            _ => None,
         }
     }
 
     /// Get the bottom of lowest relevant float for the specific clear property
     pub fn cleared_threshold(&self, clear: Clear) -> Option<f32> {
-        self.cleared_segment(clear).and_then(|idx| self.segments.get(idx.max(1) - 1)).map(|seg| seg.y.end)
+        self.cleared_segment(clear).and_then(|idx| self.segments.get(idx - 1)).map(|seg| seg.y.end)
     }
 
     /// Search for a space suitable for laying out non-floated content into
@@ -520,13 +520,19 @@ impl FloatContext {
                     height: f32::INFINITY,
                 }
             }
-            None => ContentSlot {
-                segment_id: None,
-                x: containing_block_insets[0],
-                y: min_y,
-                width: self.available_width - containing_block_insets[0] - containing_block_insets[1],
-                height: f32::INFINITY,
-            },
+            None => {
+                // There are no float-affected segments at or below `min_y` past `hwm`: the slot
+                // starts below the last skipped segment (or at `min_y` if that is lower down)
+                let below_segments_y =
+                    if hwm > 0 { self.segments.get(hwm - 1).map(|s| s.y.end).unwrap_or(min_y) } else { min_y };
+                ContentSlot {
+                    segment_id: None,
+                    x: containing_block_insets[0],
+                    y: min_y.max(below_segments_y),
+                    width: self.available_width - containing_block_insets[0] - containing_block_insets[1],
+                    height: f32::INFINITY,
+                }
+            }
         }
     }
 }
diff --git a/tests/hand_written.rs b/tests/hand_written.rs
index 8f1b996ee..fc7cbcfe8 100644
--- a/tests/hand_written.rs
+++ b/tests/hand_written.rs
@@ -1,6 +1,8 @@
 mod hand_written {
     mod border_and_padding;
     mod caching;
+    #[cfg(feature = "float_layout")]
+    mod float_clearance;
     mod floats;
     mod measure;
     mod min_max_overrides;
diff --git a/tests/hand_written/float_clearance.rs b/tests/hand_written/float_clearance.rs
new file mode 100644
index 000000000..b89e82b74
--- /dev/null
+++ b/tests/hand_written/float_clearance.rs
@@ -0,0 +1,204 @@
+//! Regression tests for clearance calculation and its interaction with margin collapsing.
+//!
+//! Scenarios are reduced from WPT tests in css/CSS2/floats-clear and css/CSS2/floats.
+use taffy::prelude::*;
+use taffy::{Clear, Float, Overflow, Point};
+
+fn block(tree: &mut TaffyTree<()>, style: Style, children: &[NodeId]) -> NodeId {
+    tree.new_with_children(Style { display: Display::Block, ..style }, children).unwrap()
+}
+
+fn assert_layout(tree: &TaffyTree<()>, node: NodeId, x: f32, y: f32, width: f32, height: f32) {
+    let layout = tree.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:?}");
+    assert_eq!(layout.size.height, height, "height of {node:?}");
+}
+
+/// Reduced from WPT css/CSS2/floats-clear/clear-002.xht
+///
+/// A cleared float should clear past prior floats, but `clear` on the first float
+/// (with no prior floats on the relevant side) should not move it down.
+#[test]
+fn clear_on_float_with_and_without_preceding_float() {
+    let t = &mut TaffyTree::new();
+    let f1 = block(
+        t,
+        Style { float: Float::Right, size: Size { width: length(96.0), height: length(96.0) }, ..Default::default() },
+        &[],
+    );
+    let f2 = block(
+        t,
+        Style {
+            float: Float::Right,
+            clear: Clear::Right,
+            size: Size { width: length(96.0), height: length(96.0) },
+            ..Default::default()
+        },
+        &[],
+    );
+    let container =
+        block(t, Style { size: Size { width: length(500.0), height: auto() }, ..Default::default() }, &[f1, f2]);
+    t.compute_layout(container, Size::MAX_CONTENT).unwrap();
+
+    assert_layout(t, f1, 404.0, 0.0, 96.0, 96.0);
+    assert_layout(t, f2, 404.0, 96.0, 96.0, 96.0);
+}
+
+/// Reduced from WPT css/CSS2/floats-clear/clear-clearance-calculation-004.xht
+///
+/// When the hypothetical position of a cleared block (with its top margin) is already
+/// past the bottom of the float, negative clearance keeps it at the float's bottom edge.
+#[test]
+fn negative_clearance_from_large_top_margin() {
+    let t = &mut TaffyTree::new();
+    let first = block(
+        t,
+        Style {
+            size: Size { width: auto(), height: length(25.0) },
+            margin: Rect { bottom: length(25.0), ..Rect::zero() },
+            ..Default::default()
+        },
+        &[],
+    );
+    let float = block(
+        t,
+        Style { float: Float::Left, size: Size { width: length(50.0), height: length(50.0) }, ..Default::default() },
+        &[],
+    );
+    let last = block(
+        t,
+        Style {
+            clear: Clear::Left,
+            margin: Rect { top: length(75.0), ..Rect::zero() },
+            size: Size { width: auto(), height: length(50.0) },
+            ..Default::default()
+        },
+        &[],
+    );
+    let container = block(
+        t,
+        Style { size: Size { width: length(300.0), height: auto() }, ..Default::default() },
+        &[first, float, last],
+    );
+    t.compute_layout(container, Size::MAX_CONTENT).unwrap();
+
+    assert_layout(t, float, 0.0, 50.0, 50.0, 50.0);
+    assert_layout(t, last, 0.0, 100.0, 300.0, 50.0);
+    assert_layout(t, container, 0.0, 0.0, 300.0, 150.0);
+}
+
+/// Reduced from WPT css/CSS2/floats-clear/clear-with-top-margin-after-cleared-empty-block.html
+///
+/// Clearance on a self-collapsing block suppresses margin collapsing with following
+/// siblings, and a following cleared sibling with a large top margin gets no clearance.
+#[test]
+fn clear_with_top_margin_after_cleared_empty_block() {
+    let t = &mut TaffyTree::new();
+    let float = block(
+        t,
+        Style { float: Float::Left, size: Size { width: length(100.0), height: length(20.0) }, ..Default::default() },
+        &[],
+    );
+    let c1 = block(t, Style { clear: Clear::Both, ..Default::default() }, &[]);
+    let c2 = block(
+        t,
+        Style {
+            clear: Clear::Both,
+            margin: Rect { top: length(100.0), ..Rect::zero() },
+            size: Size { width: auto(), height: length(20.0) },
+            ..Default::default()
+        },
+        &[],
+    );
+    let container = block(
+        t,
+        Style {
+            size: Size { width: length(100.0), height: auto() },
+            border: Rect { top: length(1.0), ..Rect::zero() },
+            ..Default::default()
+        },
+        &[float, c1, c2],
+    );
+    t.compute_layout(container, Size::MAX_CONTENT).unwrap();
+
+    assert_layout(t, c1, 0.0, 21.0, 100.0, 0.0);
+    assert_layout(t, c2, 0.0, 121.0, 100.0, 20.0);
+    assert_layout(t, container, 0.0, 0.0, 100.0, 141.0);
+}
+
+/// Reduced from WPT css/CSS2/floats/new-fc-separates-from-float.html
+///
+/// A block establishing a new formatting context that does not fit beside a float is
+/// pushed below it, and its top margin no longer collapses with preceding margins
+/// (so the float, which adjoins the unresolved margin strut, does not move down).
+#[test]
+fn new_fc_separates_from_float() {
+    let t = &mut TaffyTree::new();
+    let float = block(
+        t,
+        Style { float: Float::Left, size: Size { width: length(200.0), height: length(200.0) }, ..Default::default() },
+        &[],
+    );
+    let inner = block(t, Style::default(), &[float]);
+    let bfc = block(
+        t,
+        Style {
+            overflow: Point { x: Overflow::Hidden, y: Overflow::Hidden },
+            margin: Rect { top: length(200.0), ..Rect::zero() },
+            size: Size { width: length(200.0), height: length(1.0) },
+            ..Default::default()
+        },
+        &[],
+    );
+    let mid = block(t, Style::default(), &[inner, bfc]);
+    let container = block(
+        t,
+        Style {
+            overflow: Point { x: Overflow::Hidden, y: Overflow::Hidden },
+            size: Size { width: length(200.0), height: auto() },
+            ..Default::default()
+        },
+        &[mid],
+    );
+    t.compute_layout(container, Size::MAX_CONTENT).unwrap();
+
+    assert_layout(t, mid, 0.0, 0.0, 200.0, 201.0);
+    assert_layout(t, float, 0.0, 0.0, 200.0, 200.0);
+    assert_layout(t, bfc, 0.0, 200.0, 200.0, 1.0);
+    assert_layout(t, container, 0.0, 0.0, 200.0, 201.0);
+}
+
+/// Reduced from WPT css/CSS2/floats-clear/margin-collapse-clear-013.xht
+///
+/// The top and bottom margins of a self-collapsing block with clearance collapse with
+/// each other and are applied inside the parent (they do not collapse with the parent's
+/// bottom margin).
+#[test]
+fn self_collapsing_block_with_clearance_margins() {
+    let t = &mut TaffyTree::new();
+    let float = block(
+        t,
+        Style { float: Float::Left, size: Size { width: length(100.0), height: length(100.0) }, ..Default::default() },
+        &[],
+    );
+    let cleared = block(
+        t,
+        Style {
+            clear: Clear::Left,
+            margin: Rect { top: length(40.0), bottom: length(140.0), ..Rect::zero() },
+            ..Default::default()
+        },
+        &[],
+    );
+    let parent = block(t, Style::default(), &[float, cleared]);
+    let next = block(t, Style { size: Size { width: auto(), height: length(100.0) }, ..Default::default() }, &[]);
+    let root =
+        block(t, Style { size: Size { width: length(300.0), height: auto() }, ..Default::default() }, &[parent, next]);
+    t.compute_layout(root, Size::MAX_CONTENT).unwrap();
+
+    assert_layout(t, cleared, 0.0, 100.0, 300.0, 0.0);
+    assert_layout(t, parent, 0.0, 0.0, 300.0, 200.0);
+    assert_layout(t, next, 0.0, 200.0, 300.0, 100.0);
+}