layout: Support min/max main keyword sizes in flexbox (#35961)

Adds support for min-content, max-content, fit-content and stretch on
the min and max main size properties of a flex item.

I'm removing `automatic_min_size()` and `flex_base_size()` because they
would need to share so much code among themselves and their one caller
that it's simpler to just inline the code.

Signed-off-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
Oriol Brufau 2025-03-19 10:03:49 +01:00 committed by GitHub
parent 2362e4c134
commit ba6c3916fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 160 additions and 444 deletions

View file

@ -21,7 +21,7 @@ use style::properties::longhands::flex_direction::computed_value::T as FlexDirec
use style::properties::longhands::flex_wrap::computed_value::T as FlexWrap;
use style::values::computed::LengthPercentage;
use style::values::generics::flex::GenericFlexBasis as FlexBasis;
use style::values::generics::length::{GenericLengthPercentageOrAuto, LengthPercentageOrNormal};
use style::values::generics::length::LengthPercentageOrNormal;
use style::values::specified::align::AlignFlags;
use super::geom::{FlexAxis, FlexRelativeRect, FlexRelativeSides, FlexRelativeVec2};
@ -2203,6 +2203,11 @@ impl FlexItemBox {
style.writing_mode.is_horizontal(),
flex_axis,
);
let main_axis = if cross_axis_is_item_block_axis {
Direction::Inline
} else {
Direction::Block
};
let ContentBoxSizesAndPBM {
content_box_sizes,
@ -2210,11 +2215,6 @@ impl FlexItemBox {
depends_on_block_constraints,
} = content_box_sizes_and_pbm;
let content_box_size = content_box_sizes.map(|size| size.preferred);
// TODO(#32853): handle size keywords.
let content_min_box_size = content_box_sizes.map(|size| size.min.to_auto_or());
let content_max_box_size = content_box_sizes.map(|size| size.max.to_numeric());
let preferred_aspect_ratio = self
.independent_formatting_context
.preferred_aspect_ratio(&pbm.padding_border_sums);
@ -2229,90 +2229,177 @@ impl FlexItemBox {
} + margin_auto_is_zero.sum_by_axis();
let auto_cross_size_stretches_to_container_size =
config.item_with_auto_cross_size_stretches_to_container_size(style, &margin);
let flex_relative_content_box_size = flex_axis.vec2_to_flex_relative(content_box_size);
let flex_relative_content_max_size = flex_axis.vec2_to_flex_relative(content_max_box_size);
let flex_relative_content_min_size = flex_axis.vec2_to_flex_relative(content_min_box_size);
let containing_block_size = flex_axis.vec2_to_flex_relative(containing_block.size);
let flex_relative_content_min_size = FlexRelativeVec2 {
main: flex_relative_content_min_size.main.auto_is(|| {
self.automatic_min_size(
layout_context,
containing_block_size,
cross_axis_is_item_block_axis,
flex_relative_content_box_size,
flex_relative_content_min_size,
flex_relative_content_max_size,
preferred_aspect_ratio,
&pbm_auto_is_zero,
auto_cross_size_stretches_to_container_size,
|item| {
let min_size_auto_is_zero = content_min_box_size.auto_is(Au::zero);
item.layout_for_block_content_size(
flex_context_getter(),
pbm,
content_box_size,
min_size_auto_is_zero,
content_max_box_size,
preferred_aspect_ratio,
auto_cross_size_stretches_to_container_size,
IntrinsicSizingMode::Size,
)
},
)
}),
cross: flex_relative_content_min_size.cross.auto_is(Au::zero),
let (content_main_sizes, content_cross_sizes) = match flex_axis {
FlexAxis::Row => (&content_box_sizes.inline, &content_box_sizes.block),
FlexAxis::Column => (&content_box_sizes.block, &content_box_sizes.inline),
};
let align_self = AlignItems(config.resolve_align_self_for_child(style));
let (flex_base_size, flex_base_size_is_definite) = self.flex_base_size(
layout_context,
containing_block_size,
cross_axis_is_item_block_axis,
flex_relative_content_box_size,
flex_relative_content_min_size,
flex_relative_content_max_size,
preferred_aspect_ratio,
padding_border,
&pbm_auto_is_zero,
auto_cross_size_stretches_to_container_size,
|item| {
let min_size = flex_axis.vec2_to_flow_relative(flex_relative_content_min_size);
item.layout_for_block_content_size(
let containing_block_size = flex_axis.vec2_to_flex_relative(containing_block.size);
let stretch_size = FlexRelativeVec2 {
main: containing_block_size
.main
.map(|v| Au::zero().max(v - pbm_auto_is_zero.main)),
cross: containing_block_size
.cross
.map(|v| Au::zero().max(v - pbm_auto_is_zero.cross)),
};
// <https://drafts.csswg.org/css-flexbox/#definite-sizes>
// > If a single-line flex container has a definite cross size, the automatic preferred
// > outer cross size of any stretched flex items is the flex containers inner cross size
// > (clamped to the flex items min and max cross size) and is considered definite.
let (preferred_cross_size, min_cross_size, max_cross_size) = content_cross_sizes
.resolve_each_extrinsic(
if auto_cross_size_stretches_to_container_size {
Size::Stretch
} else {
Size::FitContent
},
Au::zero(),
stretch_size.cross,
);
let cross_size = SizeConstraint::new(preferred_cross_size, min_cross_size, max_cross_size);
// <https://drafts.csswg.org/css-flexbox/#transferred-size-suggestion>
// > If the item has a preferred aspect ratio and its preferred cross size is definite, then the
// > transferred size suggestion is that size (clamped by its minimum and maximum cross sizes if they
// > are definite), converted through the aspect ratio. It is otherwise undefined.
let transferred_size_suggestion =
LazyCell::new(|| match (preferred_aspect_ratio, cross_size) {
(Some(ratio), SizeConstraint::Definite(cross_size)) => {
Some(ratio.compute_dependent_size(main_axis, cross_size))
},
_ => None,
});
// <https://drafts.csswg.org/css-flexbox/#algo-main-item>
let flex_base_size_is_definite = Cell::new(true);
let main_content_sizes = LazyCell::new(|| {
let flex_item = &self.independent_formatting_context;
// > B: If the flex item has ...
// > - a preferred aspect ratio,
// > - a used flex basis of content, and
// > - a definite cross size,
// > then the flex base size is calculated from its used cross size and the flex items aspect ratio.
if let Some(transferred_size_suggestion) = *transferred_size_suggestion {
return transferred_size_suggestion.into();
}
flex_base_size_is_definite.set(false);
// FIXME: implement cases C, D.
// > E. Otherwise, size the item into the available space using its used flex basis in place of
// > its main size, treating a value of content as max-content. If a cross size is needed to
// > determine the main size (e.g. when the flex items main size is in its block axis, or when
// > it has a preferred aspect ratio) and the flex items cross size is auto and not definite,
// > in this calculation use fit-content as the flex items cross size. The flex base size is
// > the items resulting main size.
if cross_axis_is_item_block_axis {
// The main axis is the inline axis, so we can get the content size from the normal
// preferred widths calculation.
let constraint_space =
ConstraintSpace::new(cross_size, style.writing_mode, preferred_aspect_ratio);
let content_sizes = flex_item
.inline_content_sizes(layout_context, &constraint_space)
.sizes;
if let Some(ratio) = preferred_aspect_ratio {
let transferred_min = ratio.compute_dependent_size(main_axis, min_cross_size);
let transferred_max =
max_cross_size.map(|v| ratio.compute_dependent_size(main_axis, v));
content_sizes
.map(|size| size.clamp_between_extremums(transferred_min, transferred_max))
} else {
content_sizes
}
} else {
self.layout_for_block_content_size(
flex_context_getter(),
pbm,
content_box_size,
min_size,
content_max_box_size,
content_box_sizes.map(|size| size.preferred),
// TODO(#32853): handle size keywords.
content_box_sizes.map(|size| size.min.to_numeric().unwrap_or_default()),
content_box_sizes.map(|size| size.max.to_numeric()),
preferred_aspect_ratio,
auto_cross_size_stretches_to_container_size,
IntrinsicSizingMode::Size,
)
},
);
.into()
}
});
let hypothetical_main_size = flex_base_size.clamp_between_extremums(
flex_relative_content_min_size.main,
flex_relative_content_max_size.main,
let flex_base_size = self
.flex_basis(
containing_block_size.main,
content_main_sizes.preferred,
padding_border.main,
)
.resolve_for_preferred(Size::MaxContent, stretch_size.main, &main_content_sizes);
let flex_base_size_is_definite = flex_base_size_is_definite.take();
let content_max_main_size = content_main_sizes
.max
.resolve_for_max(stretch_size.main, &main_content_sizes);
let get_automatic_minimum_size = || {
// This is an implementation of <https://drafts.csswg.org/css-flexbox/#min-size-auto>.
if style.establishes_scroll_container(self.base_fragment_info().flags) {
return Au::zero();
}
// > **specified size suggestion**
// > If the items preferred main size is definite and not automatic, then the specified
// > size suggestion is that size. It is otherwise undefined.
let specified_size_suggestion = content_main_sizes
.preferred
.maybe_resolve_extrinsic(stretch_size.main);
let is_replaced = self.independent_formatting_context.is_replaced();
// > **content size suggestion**
// > The content size suggestion is the min-content size in the main axis, clamped, if it has a
// > preferred aspect ratio, by any definite minimum and maximum cross sizes converted through the
// > aspect ratio.
let content_size_suggestion = match preferred_aspect_ratio {
Some(ratio) => main_content_sizes.min_content.clamp_between_extremums(
ratio.compute_dependent_size(main_axis, min_cross_size),
max_cross_size.map(|l| ratio.compute_dependent_size(main_axis, l)),
),
None => main_content_sizes.min_content,
};
// > The content-based minimum size of a flex item is the smaller of its specified size
// > suggestion and its content size suggestion if its specified size suggestion exists;
// > otherwise, the smaller of its transferred size suggestion and its content size
// > suggestion if the element is replaced and its transferred size suggestion exists;
// > otherwise its content size suggestion. In all cases, the size is clamped by the maximum
// > main size if its definite.
match (specified_size_suggestion, *transferred_size_suggestion) {
(Some(specified), _) => specified.min(content_size_suggestion),
(_, Some(transferred)) if is_replaced => transferred.min(content_size_suggestion),
_ => content_size_suggestion,
}
.clamp_below_max(content_max_main_size)
};
let content_min_main_size = content_main_sizes.min.resolve_for_min(
get_automatic_minimum_size,
stretch_size.main,
&main_content_sizes,
);
let margin: FlexRelativeSides<AuOrAuto> = config.sides_to_flex_relative(pbm.margin);
FlexItem {
box_: self,
content_cross_sizes: match flex_axis {
FlexAxis::Row => content_box_sizes.block.clone(),
FlexAxis::Column => content_box_sizes.inline.clone(),
},
content_cross_sizes: content_cross_sizes.clone(),
padding,
border,
margin,
margin: config.sides_to_flex_relative(pbm.margin),
pbm_auto_is_zero,
flex_base_size,
flex_base_size_is_definite,
hypothetical_main_size,
content_min_main_size: flex_relative_content_min_size.main,
content_max_main_size: flex_relative_content_max_size.main,
align_self,
hypothetical_main_size: flex_base_size
.clamp_between_extremums(content_min_main_size, content_max_main_size),
content_min_main_size,
content_max_main_size,
align_self: AlignItems(config.resolve_align_self_for_child(style)),
depends_on_block_constraints: *depends_on_block_constraints,
preferred_aspect_ratio,
auto_cross_size_stretches_to_container_size,
@ -2494,109 +2581,6 @@ impl FlexItemBox {
}
}
/// This is an implementation of <https://drafts.csswg.org/css-flexbox/#min-size-auto>.
#[allow(clippy::too_many_arguments)]
fn automatic_min_size(
&self,
layout_context: &LayoutContext,
containing_block_size: FlexRelativeVec2<Option<Au>>,
cross_axis_is_item_block_axis: bool,
content_box_size: FlexRelativeVec2<Size<Au>>,
min_size: FlexRelativeVec2<GenericLengthPercentageOrAuto<Au>>,
max_size: FlexRelativeVec2<Option<Au>>,
preferred_aspect_ratio: Option<AspectRatio>,
pbm_auto_is_zero: &FlexRelativeVec2<Au>,
auto_cross_size_stretches_to_container_size: bool,
block_content_size_callback: impl FnOnce(&FlexItemBox) -> Au,
) -> Au {
// FIXME(stshine): Consider more situations when auto min size is not needed.
let style = &self.independent_formatting_context.style();
if style.establishes_scroll_container(self.base_fragment_info().flags) {
return Au::zero();
}
// > **specified size suggestion**
// > If the items preferred main size is definite and not automatic, then the specified
// > size suggestion is that size. It is otherwise undefined.
let specified_size_suggestion = content_box_size.main.maybe_resolve_extrinsic(
containing_block_size
.main
.map(|v| v - pbm_auto_is_zero.main),
);
let is_replaced = self.independent_formatting_context.is_replaced();
let main_axis = if cross_axis_is_item_block_axis {
Direction::Inline
} else {
Direction::Block
};
let cross_stretch_size = containing_block_size
.cross
.map(|v| v - pbm_auto_is_zero.cross);
let cross_size = SizeConstraint::new(
if content_box_size.cross.is_initial() && auto_cross_size_stretches_to_container_size {
cross_stretch_size
} else {
content_box_size
.cross
.maybe_resolve_extrinsic(cross_stretch_size)
},
min_size.cross.auto_is(Au::zero),
max_size.cross,
);
// > **transferred size suggestion**
// > If the item has a preferred aspect ratio and its preferred cross size is definite, then the
// > transferred size suggestion is that size (clamped by its minimum and maximum cross sizes if they
// > are definite), converted through the aspect ratio. It is otherwise undefined.
let transferred_size_suggestion = match (preferred_aspect_ratio, cross_size) {
(Some(ratio), SizeConstraint::Definite(cross_size)) => {
Some(ratio.compute_dependent_size(main_axis, cross_size))
},
_ => None,
};
// > **content size suggestion**
// > The content size suggestion is the min-content size in the main axis, clamped, if it has a
// > preferred aspect ratio, by any definite minimum and maximum cross sizes converted through the
// > aspect ratio.
let main_content_size = if cross_axis_is_item_block_axis {
let writing_mode = style.writing_mode;
let constraint_space =
ConstraintSpace::new(cross_size, writing_mode, preferred_aspect_ratio);
self.independent_formatting_context
.inline_content_sizes(layout_context, &constraint_space)
.sizes
.min_content
} else {
block_content_size_callback(self)
};
let content_size_suggestion = preferred_aspect_ratio
.map(|ratio| {
main_content_size.clamp_between_extremums(
ratio.compute_dependent_size(main_axis, min_size.cross.auto_is(Au::zero)),
max_size
.cross
.map(|l| ratio.compute_dependent_size(main_axis, l)),
)
})
.unwrap_or(main_content_size);
// > The content-based minimum size of a flex item is the smaller of its specified size
// > suggestion and its content size suggestion if its specified size suggestion exists;
// > otherwise, the smaller of its transferred size suggestion and its content size
// > suggestion if the element is replaced and its transferred size suggestion exists;
// > otherwise its content size suggestion. In all cases, the size is clamped by the maximum
// > main size if its definite.
match (specified_size_suggestion, transferred_size_suggestion) {
(Some(specified), _) => specified.min(content_size_suggestion),
(_, Some(transferred)) if is_replaced => transferred.min(content_size_suggestion),
_ => content_size_suggestion,
}
.clamp_below_max(max_size.main)
}
/// <https://drafts.csswg.org/css-flexbox-1/#flex-basis-property>
/// Returns the used value of the `flex-basis` property, after resolving percentages,
/// resolving `auto`, and taking `box-sizing` into account.
@ -2648,114 +2632,6 @@ impl FlexItemBox {
}
}
/// <https://drafts.csswg.org/css-flexbox/#algo-main-item>
#[allow(clippy::too_many_arguments)]
fn flex_base_size(
&self,
layout_context: &LayoutContext,
container_definite_inner_size: FlexRelativeVec2<Option<Au>>,
cross_axis_is_item_block_axis: bool,
content_box_size: FlexRelativeVec2<Size<Au>>,
content_min_box_size: FlexRelativeVec2<Au>,
content_max_box_size: FlexRelativeVec2<Option<Au>>,
preferred_aspect_ratio: Option<AspectRatio>,
padding_border_sums: FlexRelativeVec2<Au>,
pbm_auto_is_zero: &FlexRelativeVec2<Au>,
auto_cross_size_stretches_to_container_size: bool,
block_content_size_callback: impl FnOnce(&FlexItemBox) -> Au,
) -> (Au, bool) {
let used_flex_basis = self.flex_basis(
container_definite_inner_size.main,
content_box_size.main,
padding_border_sums.main,
);
let mut flex_base_size_is_definite = true;
let content_size = LazyCell::new(|| {
let flex_item = &self.independent_formatting_context;
let main_axis = if cross_axis_is_item_block_axis {
Direction::Inline
} else {
Direction::Block
};
// > If a single-line flex container has a definite cross size, the automatic preferred
// > outer cross size of any stretched flex items is the flex containers inner cross size
// > (clamped to the flex items min and max cross size) and is considered definite.
let cross_stretch_size = container_definite_inner_size
.cross
.map(|v| v - pbm_auto_is_zero.cross);
let cross_size = SizeConstraint::new(
if content_box_size.cross.is_initial() &&
auto_cross_size_stretches_to_container_size
{
cross_stretch_size
} else {
content_box_size
.cross
.maybe_resolve_extrinsic(cross_stretch_size)
},
content_min_box_size.cross,
content_max_box_size.cross,
);
// > B: If the flex item has ...
// > - a preferred aspect ratio,
// > - a used flex basis of content, and
// > - a definite cross size,
// > then the flex base size is calculated from its used cross size and the flex items aspect ratio.
if let (Some(ratio), SizeConstraint::Definite(cross_size)) =
(preferred_aspect_ratio, cross_size)
{
return ratio.compute_dependent_size(main_axis, cross_size).into();
}
flex_base_size_is_definite = false;
// FIXME: implement cases C, D.
// > E. Otherwise, size the item into the available space using its used flex basis in place of
// > its main size, treating a value of content as max-content. If a cross size is needed to
// > determine the main size (e.g. when the flex items main size is in its block axis, or when
// > it has a preferred aspect ratio) and the flex items cross size is auto and not definite,
// > in this calculation use fit-content as the flex items cross size. The flex base size is
// > the items resulting main size.
if cross_axis_is_item_block_axis {
// The main axis is the inline axis, so we can get the content size from the normal
// preferred widths calculation.
let constraint_space = ConstraintSpace::new(
cross_size,
flex_item.style().writing_mode,
preferred_aspect_ratio,
);
let content_sizes = flex_item
.inline_content_sizes(layout_context, &constraint_space)
.sizes;
if let Some(ratio) = preferred_aspect_ratio {
let transferred_min =
ratio.compute_dependent_size(main_axis, content_min_box_size.cross);
let transferred_max = content_max_box_size
.cross
.map(|v| ratio.compute_dependent_size(main_axis, v));
content_sizes
.map(|size| size.clamp_between_extremums(transferred_min, transferred_max))
} else {
content_sizes
}
} else {
block_content_size_callback(self).into()
}
});
let stretch_size = container_definite_inner_size
.main
.map(|container_size| Au::zero().max(container_size - pbm_auto_is_zero.main));
let flex_base_size =
used_flex_basis.resolve_for_preferred(Size::MaxContent, stretch_size, &content_size);
(flex_base_size, flex_base_size_is_definite)
}
#[allow(clippy::too_many_arguments)]
#[cfg_attr(
feature = "tracing",

View file

@ -729,12 +729,6 @@ impl<T: Clone> Size<T> {
}
}
#[inline]
pub(crate) fn to_auto_or(&self) -> AutoOr<T> {
self.to_numeric()
.map_or(AutoOr::Auto, AutoOr::LengthPercentage)
}
#[inline]
pub fn map<U>(&self, f: impl FnOnce(T) -> U) -> Size<U> {
match self {

View file

@ -1,4 +0,0 @@
[canvas-dynamic-change-001.html]
[.flexbox 1]
expected: FAIL

View file

@ -1,3 +0,0 @@
[flex-aspect-ratio-img-column-011.html]
[.flexbox 7]
expected: FAIL

View file

@ -1,2 +0,0 @@
[flex-item-max-height-min-content.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[flex-item-max-width-min-content.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[flex-minimum-height-flex-items-023.html]
expected: FAIL

View file

@ -1,72 +0,0 @@
[keyword-sizes-on-flex-item-001.html]
[.test 10]
expected: FAIL
[.test 11]
expected: FAIL
[.test 12]
expected: FAIL
[.test 13]
expected: FAIL
[.test 14]
expected: FAIL
[.test 15]
expected: FAIL
[.test 16]
expected: FAIL
[.test 17]
expected: FAIL
[.test 18]
expected: FAIL
[.test 19]
expected: FAIL
[.test 20]
expected: FAIL
[.test 21]
expected: FAIL
[.test 22]
expected: FAIL
[.test 23]
expected: FAIL
[.test 24]
expected: FAIL
[.test 25]
expected: FAIL
[.test 26]
expected: FAIL
[.test 27]
expected: FAIL
[.test 40]
expected: FAIL
[.test 41]
expected: FAIL
[.test 42]
expected: FAIL
[.test 43]
expected: FAIL
[.test 44]
expected: FAIL
[.test 45]
expected: FAIL

View file

@ -1,54 +0,0 @@
[keyword-sizes-on-flex-item-002.html]
[.test 31]
expected: FAIL
[.test 32]
expected: FAIL
[.test 33]
expected: FAIL
[.test 34]
expected: FAIL
[.test 35]
expected: FAIL
[.test 36]
expected: FAIL
[.test 49]
expected: FAIL
[.test 50]
expected: FAIL
[.test 51]
expected: FAIL
[.test 52]
expected: FAIL
[.test 53]
expected: FAIL
[.test 54]
expected: FAIL
[.test 69]
expected: FAIL
[.test 70]
expected: FAIL
[.test 71]
expected: FAIL
[.test 72]
expected: FAIL
[.test 73]
expected: FAIL
[.test 74]
expected: FAIL

View file

@ -1,3 +0,0 @@
[indefinite-4.html]
[[data-expected-client-height\] 1]
expected: FAIL

View file

@ -2,17 +2,11 @@
[[data-expected-height\] 8]
expected: FAIL
[[data-expected-height\] 20]
expected: FAIL
[[data-expected-height\] 22]
expected: FAIL
[[data-expected-height\] 31]
expected: FAIL
[[data-expected-height\] 43]
expected: FAIL
[[data-expected-height\] 45]
expected: FAIL

View file

@ -2,17 +2,11 @@
[[data-expected-width\] 8]
expected: FAIL
[[data-expected-width\] 19]
expected: FAIL
[[data-expected-width\] 22]
expected: FAIL
[[data-expected-width\] 31]
expected: FAIL
[[data-expected-width\] 42]
expected: FAIL
[[data-expected-width\] 45]
expected: FAIL