Semantic delta · 21
tests/hand_written/float_clearance.rs
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);
+}