Delta semántico · 19
tests/hand_written/bfc_beside_floats.rs
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);
+}