Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

### Fixed

- Block: a block container's content width and the stretch width / available width handed to its in-flow and floated children are floored at zero when padding/border or the child's margins exceed the container width. Children (and measure functions) could previously receive negative widths.
- Flexbox/Grid: the stretch size and available space derived from the container/grid area minus an item's margins are likewise floored at zero. Stretched flex items whose cross-axis margins exceeded the line previously ended up with a negative used cross size (Chrome gives 0).
- Grid: items with an `auto` start line and a definite end line (e.g. `grid-column: auto / 1`) no longer cause a phantom zero-sized positive implicit track to be created. This previously caused `grid_template_columns()`/`grid_template_rows()` to serialize an extra `0px` track (e.g. `10px 0px` instead of `10px`)
- Grid: fixed `DetailedGridTracksInfo::resolve_absolute_grid_area` edge cases: placements are now normalized (lines sorted, named lines resolved) *before* out-of-range lines are treated as `auto`, and axes with no tracks resolve against the axis' single content-aligned grid line instead of falling back to the padding edge
- Block/float: absorb `f32` rounding errors in horizontal fit checks, preventing floats from spuriously wrapping when percentage widths and margins sum to exactly 100% of the container (#1161).
Expand Down
12 changes: 6 additions & 6 deletions src/compute/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -973,7 +973,7 @@ fn perform_final_layout_on_in_flow_children(
block_ctx: &mut BlockContext<'_>,
) -> (Rect<f32>, f32, CollapsibleMarginSet, CollapsibleMarginSet, Option<f32>) {
// Resolve container_inner_width for sizing child nodes using initial content_box_inset
let container_inner_width = container_outer_width - resolved_content_box_inset.horizontal_axis_sum();
let container_inner_width = (container_outer_width - resolved_content_box_inset.horizontal_axis_sum()).max(0.0);
let container_percentage_resolution_height =
container_percentage_resolution_height.maybe_sub(resolved_content_box_inset.vertical_axis_sum());
let parent_size = Size { width: Some(container_inner_width), height: container_percentage_resolution_height };
Expand Down Expand Up @@ -1046,7 +1046,7 @@ fn perform_final_layout_on_in_flow_children(

// A float with `width: auto` is shrink-to-fit (fit-content) sized: the available
// space clamped between its min-content and max-content sizes.
let available_width = container_inner_width - item_non_auto_x_margin_sum;
let available_width = (container_inner_width - item_non_auto_x_margin_sum).max(0.0);
let (item_known_width, item_available_width) = match resolve_sizing_keyword(
item.size_style.width,
Some(available_width),
Expand Down Expand Up @@ -1160,7 +1160,7 @@ fn perform_final_layout_on_in_flow_children(
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;
let stretch_width = (container_inner_width - item_non_auto_x_margin_sum).max(0.0);
let position = Point { x: 0.0, y: 0.0 };
let width = 0.0;

Expand Down Expand Up @@ -1193,7 +1193,7 @@ fn perform_final_layout_on_in_flow_children(
let width = item
.size
.width
.unwrap_or(slot.stretch_width.max(min_auto_width))
.unwrap_or(slot.stretch_width.max(min_auto_width).max(0.0))
.maybe_clamp(item.min_size.width, item.max_size.width);
if width <= slot.border_width + FIT_TOLERANCE {
break slot;
Expand All @@ -1210,12 +1210,12 @@ fn perform_final_layout_on_in_flow_children(

has_active_floats = slot.segment_id.is_some();
item_avoids_floats = true;
let stretch_width = slot.stretch_width.max(min_auto_width);
let stretch_width = slot.stretch_width.max(min_auto_width).max(0.0);
break 'block (stretch_width, Point { x: slot.x, y: slot.y }, slot.border_width);
}

if !has_active_floats {
let stretch_width = container_inner_width - item_non_auto_x_margin_sum;
let stretch_width = (container_inner_width - item_non_auto_x_margin_sum).max(0.0);
break 'block (
stretch_width,
Point { x: resolved_content_box_inset.left, y: min_y },
Expand Down
27 changes: 18 additions & 9 deletions src/compute/flexbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -757,19 +757,25 @@ fn determine_available_space(
) -> Size<AvailableSpace> {
// Note: min/max/preferred size styles have already been applied to known_dimensions in the `compute` function above
let width = match known_dimensions.width {
Some(node_width) => AvailableSpace::Definite(node_width - constants.content_box_inset.horizontal_axis_sum()),
Some(node_width) => {
AvailableSpace::Definite((node_width - constants.content_box_inset.horizontal_axis_sum()).max(0.0))
}
None => outer_available_space
.width
.maybe_sub(constants.margin.horizontal_axis_sum())
.maybe_sub(constants.content_box_inset.horizontal_axis_sum()),
.maybe_sub(constants.content_box_inset.horizontal_axis_sum())
.maybe_max(0.0),
};

let height = match known_dimensions.height {
Some(node_height) => AvailableSpace::Definite(node_height - constants.content_box_inset.vertical_axis_sum()),
Some(node_height) => {
AvailableSpace::Definite((node_height - constants.content_box_inset.vertical_axis_sum()).max(0.0))
}
None => outer_available_space
.height
.maybe_sub(constants.margin.vertical_axis_sum())
.maybe_sub(constants.content_box_inset.vertical_axis_sum()),
.maybe_sub(constants.content_box_inset.vertical_axis_sum())
.maybe_max(0.0),
};

Size { width, height }
Expand Down Expand Up @@ -862,7 +868,7 @@ fn determine_flex_base_size(
{
ckd.set_cross(
dir,
cross_axis_available_space.into_option().maybe_sub(child.margin.cross_axis_sum(dir)),
cross_axis_available_space.into_option().maybe_sub(child.margin.cross_axis_sum(dir)).maybe_max(0.0),
);
// The cross size of a stretched item is definite if the container has a definite
// cross size (https://www.w3.org/TR/css-flexbox-1/#definite-sizes)
Expand Down Expand Up @@ -904,7 +910,8 @@ fn determine_flex_base_size(
// Note: `child.size` has already been resolved against aspect_ratio in generate_anonymous_flex_items
// So B will just work here by using main_size without special handling for aspect_ratio
let main_size = child.size.main(dir);
let main_stretch_size = percent_resolution_main_size.maybe_sub(child.margin.main_axis_sum(dir));
let main_stretch_size =
percent_resolution_main_size.maybe_sub(child.margin.main_axis_sum(dir)).maybe_max(0.0);

// A flex basis that is a sizing keyword (min-content, max-content, fit-content,
// fit-content(...), stretch) is used in place of the main size property: `stretch`
Expand Down Expand Up @@ -1458,7 +1465,8 @@ fn determine_container_main_size(
dir,
cross_axis_available_space
.into_option()
.maybe_sub(item.margin.cross_axis_sum(dir)),
.maybe_sub(item.margin.cross_axis_sum(dir))
.maybe_max(0.0),
);
}
ckd
Expand Down Expand Up @@ -1821,7 +1829,8 @@ fn determine_hypothetical_cross_size(
.node_inner_size
.cross(constants.dir)
.map(|val| constants.divided_cross_space(val))
.maybe_sub(child.margin.cross_axis_sum(constants.dir));
.maybe_sub(child.margin.cross_axis_sum(constants.dir))
.maybe_max(0.0);
let child_available_cross = match resolve_sizing_keyword(
child.size_style.cross(constants.dir),
cross_stretch_size,
Expand Down Expand Up @@ -2091,7 +2100,7 @@ fn determine_used_cross_size(
.maybe_resolve(constants.node_inner_size, |val, basis| tree.calc(val, basis))
.maybe_add(box_sizing_adjustment);

(line_cross_size - child.margin.cross_axis_sum(constants.dir)).maybe_clamp(
(line_cross_size - child.margin.cross_axis_sum(constants.dir)).max(0.0).maybe_clamp(
child.min_size.cross(constants.dir),
max_size_ignoring_aspect_ratio.cross(constants.dir),
)
Expand Down
4 changes: 2 additions & 2 deletions src/compute/grid/alignment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ pub(super) fn align_and_position_item(
drop(style);

let grid_area_minus_item_margins_size = Size {
width: grid_area_size.width.maybe_sub(margin.left).maybe_sub(margin.right),
height: grid_area_size.height.maybe_sub(margin.top).maybe_sub(margin.bottom) - baseline_shim,
width: grid_area_size.width.maybe_sub(margin.left).maybe_sub(margin.right).max(0.0),
height: (grid_area_size.height.maybe_sub(margin.top).maybe_sub(margin.bottom) - baseline_shim).max(0.0),
};

// A size that is a sizing keyword (min-content, max-content, fit-content,
Expand Down
4 changes: 2 additions & 2 deletions src/compute/grid/types/grid_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ impl GridItem {
.maybe_apply_aspect_ratio(aspect_ratio)
.maybe_add(box_sizing_adjustment);

let grid_area_minus_item_margins_size = grid_area_size.maybe_sub(margins);
let grid_area_minus_item_margins_size = grid_area_size.maybe_sub(margins).maybe_max(Size::ZERO);

// If node is absolutely positioned and width is not set explicitly, then deduce it
// from left, right and container_content_box if both are set.
Expand Down Expand Up @@ -575,7 +575,7 @@ impl GridItem {
if !size_style.is_sizing_keyword() {
continue;
}
let stretch_size = grid_area_size.get(axis).maybe_sub(margins.get(axis));
let stretch_size = grid_area_size.get(axis).maybe_sub(margins.get(axis)).maybe_max(0.0);
if let Some(SizingKeywordResolution::Measure(available)) =
resolve_sizing_keyword(size_style, stretch_size, grid_area_size.get(axis))
{
Expand Down
18 changes: 18 additions & 0 deletions test_fixtures/block/block_margin_x_exceeds_container_width.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script src="../../scripts/gentest/test_helper.js"></script>
<link rel="stylesheet" type="text/css" href="../../scripts/gentest/test_base_style.css">
<title>
Test description
</title>
</head>
<body>

<div id="test-root" style="display: block; width: 20px;">
<div style="height: 10px; margin-left: 30px; margin-right: 10px;"></div>
<div style="height: 10px; margin-left: 30px; margin-right: 10px;"><div style="height: 10px;"></div></div>
</div>

</body>
</html>
18 changes: 18 additions & 0 deletions test_fixtures/block/block_padding_exceeds_container_width.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script src="../../scripts/gentest/test_helper.js"></script>
<link rel="stylesheet" type="text/css" href="../../scripts/gentest/test_base_style.css">
<title>
Test description
</title>
</head>
<body>

<div id="test-root" style="display: block; width: 10px; padding-left: 20px; padding-right: 20px; box-sizing: border-box;">
<div style="height: 10px; margin-left: 5px;"></div>
<div style="height: 10px; width: 50%;"></div>
</div>

</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script src="../../scripts/gentest/test_helper.js"></script>
<link rel="stylesheet" type="text/css" href="../../scripts/gentest/test_base_style.css">
<title>Test description</title>
</head>
<body>
<div id="test-root" style="display: flex; flex-direction: column; width: 20px; height: 100px;">
<div style="margin-left: 30px; margin-right: 10px; height: 20px; display: flex;">
<div style="flex-grow: 1; flex-basis: 10px; height: 20px;"></div>
<div style="flex-grow: 1; flex-basis: 10px; height: 20px;"></div>
</div>
<div style="margin-left: 30px; margin-right: 10px; height: 20px;"></div>
<div style="margin-left: 30px; margin-right: 10px; display: flex; flex-wrap: wrap;">
<div style="width: 5px; height: 10px;"></div>
<div style="width: 5px; height: 10px;"></div>
</div>
</div>
</body>
</html>
1 change: 1 addition & 0 deletions tests/hand_written.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod hand_written {
mod floats;
mod measure;
mod min_max_overrides;
mod negative_available_space;
mod relayout;
mod root_constraints;
mod rounding;
Expand Down
88 changes: 88 additions & 0 deletions tests/hand_written/negative_available_space.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#[cfg(test)]
mod negative_available_space {
use taffy::prelude::*;
use taffy::{AvailableSpace, LayoutInput, LayoutOutput, NodeId};
use taffy_test_helpers::{new_test_tree, test_measure_function, TestNodeContext};

/// Lays out a 20px wide container of the given display type with a single leaf whose
/// horizontal margins (40px) exceed the container width. Returns the leaf's final width
/// and every `LayoutInput` with a negative known dimension or available space that reached
/// the leaf's measure function.
fn layout_with_oversized_margins(display: Display, flex_direction: FlexDirection) -> (f32, Vec<String>) {
let mut taffy = new_test_tree();
let leaf = taffy
.new_leaf_with_context(
Style {
margin: Rect {
left: LengthPercentageAuto::length(30.0),
right: LengthPercentageAuto::length(10.0),
top: zero(),
bottom: zero(),
},
size: Size { width: auto(), height: auto() },
..Default::default()
},
TestNodeContext::zero(),
)
.unwrap();
let root = taffy
.new_with_children(
Style {
display,
flex_direction,
size: Size { width: length(20.0), height: auto() },
grid_template_columns: vec![length(20.0)],
..Default::default()
},
&[leaf],
)
.unwrap();

let mut negative_inputs = Vec::new();
taffy
.compute_layout_with_measure(
root,
Size::MAX_CONTENT,
|inputs: LayoutInput, node_id: NodeId, context: Option<&mut TestNodeContext>, style: &Style| {
let known = inputs.known_dimensions;
let avail = inputs.available_space;
let is_negative = |value: Option<f32>| value.is_some_and(|v| v < 0.0);
let is_negative_avail =
|value: AvailableSpace| matches!(value, AvailableSpace::Definite(v) if v < 0.0);
if is_negative(known.width)
|| is_negative(known.height)
|| is_negative_avail(avail.width)
|| is_negative_avail(avail.height)
{
negative_inputs.push(format!("known={known:?} available={avail:?}"));
}
let output: LayoutOutput = test_measure_function(inputs, node_id, context, style);
output
},
)
.unwrap();

(taffy.layout(leaf).unwrap().size.width, negative_inputs)
}

fn assert_floored_at_zero(display: Display, flex_direction: FlexDirection) {
let (leaf_width, negative_inputs) = layout_with_oversized_margins(display, flex_direction);
assert!(negative_inputs.is_empty(), "leaf received negative layout inputs: {negative_inputs:?}");
assert_eq!(leaf_width, 0.0);
}

#[test]
fn block_child_margins_exceed_container_width() {
assert_floored_at_zero(Display::Block, FlexDirection::Row);
}

#[test]
fn flex_column_child_margins_exceed_container_width() {
assert_floored_at_zero(Display::Flex, FlexDirection::Column);
}

#[test]
fn grid_child_margins_exceed_container_width() {
assert_floored_at_zero(Display::Grid, FlexDirection::Row);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<test name="block_margin_x_exceeds_container_width__border_box_ltr" use-rounding="true">
<viewport width="max-content" height="max-content"/>
<input>
<div display="block" direction="ltr" width="20px">
<div direction="ltr" height="10px" margin-left="30px" margin-right="10px"/>
<div direction="ltr" height="10px" margin-left="30px" margin-right="10px">
<div direction="ltr" height="10px"/>
</div>
</div>
</input>
<expectations>
<node x="0" y="0" width="20" height="20">
<node x="30" y="0" width="0" height="10"/>
<node x="30" y="10" width="0" height="10">
<node x="0" y="0" width="0" height="10"/>
</node>
</node>
</expectations>
</test>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<test name="block_margin_x_exceeds_container_width__border_box_rtl" use-rounding="true">
<viewport width="max-content" height="max-content"/>
<input>
<div display="block" direction="rtl" width="20px">
<div direction="rtl" height="10px" margin-left="30px" margin-right="10px"/>
<div direction="rtl" height="10px" margin-left="30px" margin-right="10px">
<div direction="rtl" height="10px"/>
</div>
</div>
</input>
<expectations>
<node x="0" y="0" width="20" height="20">
<node x="10" y="0" width="0" height="10"/>
<node x="10" y="10" width="0" height="10">
<node x="0" y="0" width="0" height="10"/>
</node>
</node>
</expectations>
</test>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<test name="block_margin_x_exceeds_container_width__content_box_ltr" use-rounding="true">
<viewport width="max-content" height="max-content"/>
<input>
<div display="block" box-sizing="content-box" direction="ltr" width="20px">
<div box-sizing="content-box" direction="ltr" height="10px" margin-left="30px" margin-right="10px"/>
<div box-sizing="content-box" direction="ltr" height="10px" margin-left="30px" margin-right="10px">
<div box-sizing="content-box" direction="ltr" height="10px"/>
</div>
</div>
</input>
<expectations>
<node x="0" y="0" width="20" height="20">
<node x="30" y="0" width="0" height="10"/>
<node x="30" y="10" width="0" height="10">
<node x="0" y="0" width="0" height="10"/>
</node>
</node>
</expectations>
</test>
Loading