Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 99 additions & 6 deletions src/compute/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,14 @@ impl BlockContext<'_> {
}

/// Search a space suitable for laying out non-floated content into
pub fn find_content_slot(&self, min_y: f32, clear: Clear, after: Option<usize>) -> ContentSlot {
let mut slot =
self.bfc.float_context.find_content_slot(min_y + self.y_offset, self.content_box_insets, clear, after);
pub fn find_content_slot(&self, min_y: f32, height: f32, clear: Clear, after: Option<usize>) -> ContentSlot {
let mut slot = self.bfc.float_context.find_content_slot(
min_y + self.y_offset,
self.content_box_insets,
height,
clear,
after,
);
slot.y -= self.y_offset;
slot.x -= self.insets[0];
slot
Expand All @@ -186,6 +191,7 @@ impl BlockContext<'_> {
&self,
min_y: f32,
margins: [f32; 2],
height: f32,
direction: Direction,
clear: Clear,
after: Option<usize>,
Expand All @@ -194,6 +200,7 @@ impl BlockContext<'_> {
min_y + self.y_offset,
self.content_box_insets,
margins,
height,
direction,
clear,
after,
Expand Down Expand Up @@ -1135,7 +1142,8 @@ fn perform_final_layout_on_in_flow_children(
#[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 {
#[cfg_attr(not(feature = "float_layout"), allow(unused_mut))]
let (mut stretch_width, mut float_avoiding_position, mut float_avoiding_width) = if item.is_in_same_bfc {
let stretch_width = container_inner_width - item_non_auto_x_margin_sum;
let position = Point { x: 0.0, y: 0.0 };
let width = 0.0;
Expand All @@ -1160,11 +1168,24 @@ fn perform_final_layout_on_in_flow_children(
// (so that the margin box width is non-negative, per CSS2 §10.3.3)
let min_auto_width = -item_non_auto_x_margin_sum;

// If the item's height is known up-front then it can be accounted for in
// the slot search (the item's border box must not overlap floats over its
// entire height). Otherwise the slot is re-checked after layout below.
let known_height =
item.size.height.maybe_clamp(item.min_size.height, item.max_size.height).unwrap_or(0.0);

// Find the highest slot (at or below `min_y`) with enough horizontal space
// for the item's border box, which must not overlap any float
let mut slot_segment = None;
let slot = loop {
let slot = block_ctx.find_bfc_slot(min_y, x_margins, direction, item.clear, slot_segment);
let slot = block_ctx.find_bfc_slot(
min_y,
x_margins,
known_height,
direction,
item.clear,
slot_segment,
);
let Some(segment_id) = slot.segment_id else { break slot };
let width = item
.size
Expand Down Expand Up @@ -1266,7 +1287,8 @@ fn perform_final_layout_on_in_flow_children(
#[cfg(not(feature = "float_layout"))]
let clear_pos = f32::NEG_INFINITY;

let item_layout = if item.is_in_same_bfc {
#[cfg_attr(not(feature = "float_layout"), allow(unused_mut))]
let mut item_layout = if item.is_in_same_bfc {
// Replaced elements may not have a known width (they are sized by their
// measure function rather than stretch-sized)
let width = known_dimensions.width.unwrap_or(stretch_width);
Expand Down Expand Up @@ -1296,6 +1318,77 @@ fn perform_final_layout_on_in_flow_children(
} else {
tree.compute_child_layout(item.node_id, inputs)
};

// The initial slot search only accounts for the item's height if it is known up-front.
// Now that the item has been laid out its height is known: re-run the slot search with
// the actual height (the item's border box must not overlap floats over its entire
// height), re-laying the item out if this changes the slot.
#[cfg(feature = "float_layout")]
if item_avoids_floats {
let min_y = committed_y_offset + y_margin_offset;
let x_margins = [item_non_auto_margin.left, item_non_auto_margin.right];
let min_auto_width = -item_non_auto_x_margin_sum;

for _ in 0..4 {
let height = item_layout.size.height;
let mut slot_segment = None;
let slot = loop {
let slot =
block_ctx.find_bfc_slot(min_y, x_margins, height, direction, item.clear, slot_segment);
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;
}
slot_segment = Some(segment_id);
};

let slot_unchanged = (slot.y - float_avoiding_position.y).abs() <= 0.001
&& (slot.x - float_avoiding_position.x).abs() <= 0.001
&& (slot.border_width - float_avoiding_width).abs() <= 0.001;
if slot_unchanged {
break;
}

if slot.y > min_y {
item_pushed_below_float = true;
}
has_active_floats = slot.segment_id.is_some();
stretch_width = slot.stretch_width.max(min_auto_width);
float_avoiding_position = Point { x: slot.x, y: slot.y };
float_avoiding_width = slot.border_width;

let known_dimensions = if item.is_table || item.is_replaced {
Size::NONE
} else {
item.size
.map_width(|width| {
Some(
width
.unwrap_or(stretch_width)
.maybe_clamp(item.min_size.width, item.max_size.width),
)
})
.maybe_clamp(item.min_size, item.max_size)
};
let inputs = LayoutInput {
run_mode,
sizing_mode: SizingMode::InherentSize,
axis: RequestedAxis::Both,
known_dimensions,
known_dimensions_are_definite: Size { width: true, height: true },
parent_size,
available_space: available_space.map_width(|_| AvailableSpace::Definite(stretch_width)),
vertical_margins_are_collapsible: Line::FALSE,
};
item_layout = tree.compute_child_layout(item.node_id, inputs);
}
}

let final_size = item_layout.size;

let top_margin_set = item_layout.top_margin.collapse_with_margin(item_margin.top.unwrap_or(0.0));
Expand Down
75 changes: 60 additions & 15 deletions src/compute/float.rs
Original file line number Diff line number Diff line change
Expand Up @@ -584,10 +584,18 @@ impl FloatContext {
}

/// Search for a space suitable for laying out non-floated content into
///
/// If `height` is non-zero then the returned slot accounts for all floats that a box of that
/// height starting at the slot's y position would be adjacent to (a line box is shortened by
/// any float that its vertical extent intersects). The returned slot's `height` is the
/// vertical extent (from the slot's y position) over which the slot's width is valid: content
/// taller than this may be adjacent to further floats and should be re-placed with its actual
/// height.
pub fn find_content_slot(
&self,
min_y: f32,
containing_block_insets: [f32; 2],
height: f32,
clear: Clear,
after: Option<usize>,
) -> ContentSlot {
Expand Down Expand Up @@ -617,20 +625,39 @@ impl FloatContext {
let segment = self.segments.get(start_idx);
match segment {
Some(segment) => {
let inset_left = segment.insets[0].max(containing_block_insets[0]);
let inset_right = segment.insets[1].max(containing_block_insets[1]);
let y = segment.y.start.max(min_y);
let end_y = y + height;

// Union the float insets of all segments that the box's vertical extent
// intersects, then extend the slot's height over any further segments whose
// insets do not exceed that union (the slot's width remains valid there).
let mut float_insets = segment.insets;
let mut slot_height = f32::INFINITY;
for seg in &self.segments[(start_idx + 1)..] {
if seg.y.start < end_y {
float_insets[0] = float_insets[0].max(seg.insets[0]);
float_insets[1] = float_insets[1].max(seg.insets[1]);
} else if seg.insets[0] > float_insets[0] || seg.insets[1] > float_insets[1] {
slot_height = seg.y.start - y;
break;
}
}

let inset_left = float_insets[0].max(containing_block_insets[0]);
let inset_right = float_insets[1].max(containing_block_insets[1]);
ContentSlot {
segment_id: Some(start_idx),
x: inset_left,
y: segment.y.start.max(min_y),
y,
width: self.available_width - inset_left - inset_right,
height: f32::INFINITY,
height: slot_height,
}
}
// Below all floats
None => ContentSlot {
segment_id: None,
x: containing_block_insets[0],
y: min_y,
y: self.segments.last().map(|segment| segment.y.end).unwrap_or(min_y).max(min_y),
width: self.available_width - containing_block_insets[0] - containing_block_insets[1],
height: f32::INFINITY,
},
Expand All @@ -657,11 +684,13 @@ impl FloatContext {
/// negative margin lets the border box extend outside the containing block.
///
/// When there are no floats beside the box, its (possibly negative) margins apply as usual.
#[allow(clippy::too_many_arguments)]
pub fn find_bfc_slot(
&self,
min_y: f32,
containing_block_insets: [f32; 2],
margins: [f32; 2],
height: f32,
direction: Direction,
clear: Clear,
after: Option<usize>,
Expand Down Expand Up @@ -695,34 +724,50 @@ impl FloatContext {
let start_idx = start_idx.unwrap_or(self.segments.len());
match self.segments.get(start_idx) {
Some(segment) => {
let y = segment.y.start.max(min_y);

// The box's border box must not overlap floats over its entire height, so union
// the float insets of all segments that the box's vertical extent intersects
let mut float_insets = segment.insets;
let mut has_float = segment.has_float;
if height > 0.0 {
let end_y = y + height;
for seg in &self.segments[(start_idx + 1)..] {
if seg.y.start >= end_y {
break;
}
float_insets[0] = float_insets[0].max(seg.insets[0]);
float_insets[1] = float_insets[1].max(seg.insets[1]);
has_float[0] |= seg.has_float[0];
has_float[1] |= seg.has_float[1];
}
}

let lead = match direction {
Direction::Ltr => 0,
Direction::Rtl => 1,
};
let trail = 1 - lead;
let has_lead_float = segment.has_float[lead];
let has_trail_float = segment.has_float[trail];
let has_lead_float = has_float[lead];
let has_trail_float = has_float[trail];
let mut fit_insets = [0.0; 2];
let mut stretch_insets = [0.0; 2];
fit_insets[lead] =
if has_lead_float { segment.insets[lead].max(margin_insets[lead]) } else { margin_insets[lead] };
if has_lead_float { float_insets[lead].max(margin_insets[lead]) } else { margin_insets[lead] };
stretch_insets[lead] = fit_insets[lead];
fit_insets[trail] = if has_trail_float {
segment.insets[trail].max(containing_block_insets[trail])
float_insets[trail].max(containing_block_insets[trail])
} else {
// A positive trailing margin may overflow the containing block edge (it does
// not affect fit), but a negative one widens the space for the border box
margin_insets[trail].min(containing_block_insets[trail])
};
stretch_insets[trail] = if has_trail_float {
segment.insets[trail].max(margin_insets[trail])
} else {
margin_insets[trail]
};
stretch_insets[trail] =
if has_trail_float { float_insets[trail].max(margin_insets[trail]) } else { margin_insets[trail] };
BfcSlot {
segment_id: Some(start_idx),
x: fit_insets[0],
y: segment.y.start.max(min_y),
y,
border_width: self.available_width - fit_insets[0] - fit_insets[1],
stretch_width: self.available_width - stretch_insets[0] - stretch_insets[1],
}
Expand Down
43 changes: 42 additions & 1 deletion tests/hand_written/floats.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#![cfg(feature = "float_layout")]
use taffy::geometry::Point;
use taffy::prelude::*;
use taffy::style::{Clear, Float};
use taffy::style::{Clear, Float, Overflow};
use taffy_test_helpers::new_test_tree;

/// Regression test for <https://wpt.live/css/CSS2/floats-clear/floats-146.xht>
Expand Down Expand Up @@ -149,3 +149,44 @@ fn float_beside_existing_float_moves_down_instead_of_overflowing() {
assert_eq!(taffy.layout(right).unwrap().location, Point { x: 40.0, y: 50.0 });
assert_eq!(taffy.layout(left2).unwrap().location, Point { x: 0.0, y: 100.0 });
}

/// Regression test for <https://wpt.live/css/CSS2/floats/floats-wrap-top-below-bfc-001l.xht>
///
/// A box establishing a new block formatting context must not overlap floats over its full
/// vertical extent: if it would intersect a float lower down, it moves below that float.
#[test]
fn bfc_avoids_floats_over_full_height() {
let mut taffy = new_test_tree();

let spacer = taffy.new_leaf(float_block(100.0, 30.0, Float::None)).unwrap();
// A float placed below the spacer
let float_a = taffy.new_leaf(float_block(80.0, 30.0, Float::Left)).unwrap();
// A BFC box: too wide to fit beside the float, so it must be placed below it even though
// there is float-free space beside the spacer above the float
let bfc = taffy
.new_leaf(Style {
display: Display::Block,
overflow: Point { x: Overflow::Hidden, y: Overflow::Hidden },
size: Size { width: length(40.0), height: length(60.0) },
..Default::default()
})
.unwrap();

let inner = taffy
.new_with_children(
Style {
display: Display::Block,
size: Size { width: length(100.0), height: auto() },
..Default::default()
},
&[float_a],
)
.unwrap();
let root = taffy.new_with_children(root_style(100.0), &[spacer, inner, bfc]).unwrap();

taffy.compute_layout(root, Size::MAX_CONTENT).unwrap();

assert_eq!(taffy.layout(float_a).unwrap().location, Point { x: 0.0, y: 0.0 });
// The BFC box starts below the float (y = 30 + 30 = 60), not beside the spacer
assert_eq!(taffy.layout(bfc).unwrap().location, Point { x: 0.0, y: 60.0 });
}