diff --git a/src/compute/block.rs b/src/compute/block.rs index 1f1fed86d..f01aa90ac 100644 --- a/src/compute/block.rs +++ b/src/compute/block.rs @@ -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) -> 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) -> 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 @@ -186,6 +191,7 @@ impl BlockContext<'_> { &self, min_y: f32, margins: [f32; 2], + height: f32, direction: Direction, clear: Clear, after: Option, @@ -194,6 +200,7 @@ impl BlockContext<'_> { min_y + self.y_offset, self.content_box_insets, margins, + height, direction, clear, after, @@ -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; @@ -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 @@ -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); @@ -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)); diff --git a/src/compute/float.rs b/src/compute/float.rs index 08ce35e49..a264c7c6a 100644 --- a/src/compute/float.rs +++ b/src/compute/float.rs @@ -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, ) -> ContentSlot { @@ -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, }, @@ -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, @@ -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], } diff --git a/tests/hand_written/floats.rs b/tests/hand_written/floats.rs index d299e96eb..2ccbae582 100644 --- a/tests/hand_written/floats.rs +++ b/tests/hand_written/floats.rs @@ -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 @@ -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 +/// +/// 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 }); +}