layout: Support min/max cross keywords sizes in flexbox (#35860)

Adds support for min-content, max-content, fit-content and stretch on
the min and max cross size properties of a flex item.
With one exception: when resolving the main sizes, transferred cross
minimums and maximums will still ignore keywords.

Signed-off-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
Oriol Brufau 2025-03-10 15:25:56 +01:00 committed by GitHub
parent 56da4ad959
commit c6527c4118
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 79 additions and 214 deletions

View file

@ -62,9 +62,7 @@ struct FlexContext<'a> {
/// A flex item with some intermediate results
struct FlexItem<'a> {
box_: &'a FlexItemBox,
content_cross_size: Size<Au>,
content_min_size: FlexRelativeVec2<Au>,
content_max_size: FlexRelativeVec2<Option<Au>>,
content_cross_sizes: Sizes,
padding: FlexRelativeSides<Au>,
border: FlexRelativeSides<Au>,
margin: FlexRelativeSides<AuOrAuto>,
@ -84,6 +82,14 @@ struct FlexItem<'a> {
/// <https://drafts.csswg.org/css-flexbox/#algo-main-item>
hypothetical_main_size: Au,
/// The used min main size of the flex item.
/// <https://drafts.csswg.org/css-flexbox/#min-main-size-property>
content_min_main_size: Au,
/// The used max main size of the flex item.
/// <https://drafts.csswg.org/css-flexbox/#max-main-size-property>
content_max_main_size: Option<Au>,
/// This is `align-self`, defaulting to `align-items` if `auto`
align_self: AlignItems,
@ -1222,9 +1228,10 @@ impl<'a> FlexItem<'a> {
Self {
box_,
content_cross_size: flex_relative_content_box_size.cross,
content_min_size: flex_relative_content_min_size,
content_max_size: flex_relative_content_max_size,
content_cross_sizes: match config.flex_axis {
FlexAxis::Row => content_box_sizes.block,
FlexAxis::Column => content_box_sizes.inline,
},
padding,
border,
margin,
@ -1232,6 +1239,8 @@ impl<'a> FlexItem<'a> {
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,
depends_on_block_constraints,
preferred_aspect_ratio,
@ -1242,7 +1251,7 @@ impl<'a> FlexItem<'a> {
// Note this returns false for a `stretch` size, because that stretches to the
// containing block, not to the flex line.
// To be discussed in https://github.com/w3c/csswg-drafts/issues/11784.
self.content_cross_size.is_initial() &&
self.content_cross_sizes.preferred.is_initial() &&
item_with_auto_cross_size_stretches_to_line_size(self.align_self, &self.margin)
}
}
@ -1562,8 +1571,8 @@ impl InitialFlexLineLayout<'_> {
let violation = |item: &FlexibleLengthResolutionItem| {
let size = item.target_main_size.get();
let clamped = size.clamp_between_extremums(
item.item.content_min_size.main,
item.item.content_max_size.main,
item.item.content_min_main_size,
item.item.content_max_main_size,
);
clamped - size
};
@ -1588,7 +1597,7 @@ impl InitialFlexLineLayout<'_> {
// its a min violation.”
for item in items.iter() {
if violation(item) > Au::zero() {
item.target_main_size.set(item.item.content_min_size.main);
item.target_main_size.set(item.item.content_min_main_size);
item.frozen.set(true);
frozen_count += 1;
}
@ -1601,7 +1610,7 @@ impl InitialFlexLineLayout<'_> {
// its a max violation.”
for item in items.iter() {
if violation(item) < Au::zero() {
let Some(max_size) = item.item.content_max_size.main else {
let Some(max_size) = item.item.content_max_main_size else {
unreachable!()
};
item.target_main_size.set(max_size);
@ -1697,9 +1706,21 @@ impl InitialFlexLineLayout<'_> {
for item in self.items.iter_mut() {
let stretches = item.item.stretches_to_line();
let used_cross_size = if stretches {
(final_line_cross_size - item.item.pbm_auto_is_zero.cross).clamp_between_extremums(
item.item.content_min_size.cross,
item.item.content_max_size.cross,
let (axis, content_size) = match flex_context.config.flex_axis {
FlexAxis::Row => (Direction::Block, item.layout_result.content_size.block),
FlexAxis::Column => (Direction::Inline, item.layout_result.content_size.inline),
};
item.item.content_cross_sizes.resolve(
axis,
Size::Stretch,
Au::zero(),
Some(final_line_cross_size - item.item.pbm_auto_is_zero.cross),
|| content_size.into(),
// Tables have a special sizing in the block axis in that handles collapsed rows,
// but it would prevent stretching. So we only recognize tables in the inline axis.
// The interaction of collapsed table tracks and the flexbox algorithms is unclear,
// see https://github.com/w3c/csswg-drafts/issues/11408.
item.item.is_table() && axis == Direction::Inline,
)
} else {
item.layout_result.hypothetical_cross_size
@ -1909,11 +1930,10 @@ impl FlexItem<'_> {
.block
.to_definite()
.map(|size| Au::zero().max(size - self.pbm_auto_is_zero.cross));
SizeConstraint::new(
self.content_cross_size
.maybe_resolve_extrinsic(stretch_size),
self.content_min_size.cross,
self.content_max_size.cross,
self.content_cross_sizes.resolve_extrinsic(
Size::FitContent,
Au::zero(),
stretch_size,
)
},
};
@ -1931,7 +1951,7 @@ impl FlexItem<'_> {
};
let stretch_size =
Au::zero().max(containing_block.size.inline - self.pbm_auto_is_zero.cross);
let content_size = LazyCell::new(|| {
let get_content_size = || {
let constraint_space = ConstraintSpace::new(
SizeConstraint::Definite(used_main_size),
item_writing_mode,
@ -1940,13 +1960,15 @@ impl FlexItem<'_> {
independent_formatting_context
.inline_content_sizes(flex_context.layout_context, &constraint_space)
.sizes
});
self.content_cross_size
.resolve_for_preferred(automatic_size, Some(stretch_size), &content_size)
.clamp_between_extremums(
self.content_min_size.cross,
self.content_max_size.cross,
)
};
self.content_cross_sizes.resolve(
Direction::Inline,
automatic_size,
Au::zero(),
Some(stretch_size),
get_content_size,
self.is_table(),
)
});
// The main size of a flex item is considered to be definite if its flex basis is definite
// or the flex container has a definite main size.
@ -1968,8 +1990,16 @@ impl FlexItem<'_> {
let item_style = independent_formatting_context.style();
match &independent_formatting_context.contents {
IndependentFormattingContextContents::Replaced(replaced) => {
let min_size = flex_axis.vec2_to_flow_relative(self.content_min_size);
let max_size = flex_axis.vec2_to_flow_relative(self.content_max_size);
let min_size = flex_axis.vec2_to_flow_relative(FlexRelativeVec2 {
main: Size::Numeric(self.content_min_main_size),
cross: self.content_cross_sizes.min,
});
let max_size = flex_axis.vec2_to_flow_relative(FlexRelativeVec2 {
main: self
.content_max_main_size
.map_or(Size::Initial, Size::Numeric),
cross: self.content_cross_sizes.max,
});
let size = replaced.used_size_as_if_inline_element_from_content_box_sizes(
containing_block,
item_style,
@ -1979,13 +2009,13 @@ impl FlexItem<'_> {
block_size
.to_definite()
.map_or(Size::Initial, Size::Numeric),
Size::Numeric(min_size.block),
max_size.block.map_or(Size::Initial, Size::Numeric),
min_size.block,
max_size.block,
),
inline: &Sizes::new(
Size::Numeric(inline_size),
Size::Numeric(min_size.inline),
max_size.inline.map_or(Size::Initial, Size::Numeric),
min_size.inline,
max_size.inline,
),
},
Size::FitContent.into(),
@ -2043,13 +2073,14 @@ impl FlexItem<'_> {
.block
.to_definite()
.map(|size| Au::zero().max(size - self.pbm_auto_is_zero.cross));
let content_size = LazyCell::new(|| content_block_size.into());
self.content_cross_size
.resolve_for_preferred(Size::FitContent, stretch_size, &content_size)
.clamp_between_extremums(
self.content_min_size.cross,
self.content_max_size.cross,
)
self.content_cross_sizes.resolve(
Direction::Block,
Size::FitContent,
Au::zero(),
stretch_size,
|| content_block_size.into(),
self.is_table(),
)
};
let item_as_containing_block = ContainingBlock {
@ -2278,6 +2309,14 @@ impl FlexItem<'_> {
};
outer_cross_start + margin.cross_start + self.border.cross_start + self.padding.cross_start
}
#[inline]
fn is_table(&self) -> bool {
match &self.box_.independent_formatting_context.contents {
IndependentFormattingContextContents::NonReplaced(content) => content.is_table(),
IndependentFormattingContextContents::Replaced(_) => false,
}
}
}
impl FlexItemBox {

View file

@ -53,24 +53,6 @@
[.test 27]
expected: FAIL
[.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 40]
expected: FAIL
@ -88,39 +70,3 @@
[.test 45]
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,58 +1,4 @@
[keyword-sizes-on-flex-item-002.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 31]
expected: FAIL
@ -71,24 +17,6 @@
[.test 36]
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
[.test 49]
expected: FAIL

View file

@ -2,24 +2,12 @@
[[data-expected-height\] 8]
expected: FAIL
[[data-expected-height\] 19]
expected: FAIL
[[data-expected-height\] 21]
expected: FAIL
[[data-expected-height\] 22]
expected: FAIL
[[data-expected-height\]:not([skip-second-pass\]) 31]
expected: FAIL
[[data-expected-height\]:not([skip-second-pass\]) 42]
expected: FAIL
[[data-expected-height\]:not([skip-second-pass\]) 44]
expected: FAIL
[[data-expected-height\]:not([skip-second-pass\]) 45]
expected: FAIL

View file

@ -2,24 +2,12 @@
[[data-expected-width\] 8]
expected: FAIL
[[data-expected-width\] 20]
expected: FAIL
[[data-expected-width\] 21]
expected: FAIL
[[data-expected-width\] 22]
expected: FAIL
[[data-expected-width\]:not([skip-second-pass\]) 31]
expected: FAIL
[[data-expected-width\]:not([skip-second-pass\]) 43]
expected: FAIL
[[data-expected-width\]:not([skip-second-pass\]) 44]
expected: FAIL
[[data-expected-width\]:not([skip-second-pass\]) 45]
expected: FAIL

View file

@ -2,29 +2,17 @@
[[data-expected-height\] 8]
expected: FAIL
[[data-expected-height\] 19]
expected: FAIL
[[data-expected-height\] 20]
expected: FAIL
[[data-expected-height\] 21]
expected: FAIL
[[data-expected-height\] 22]
expected: FAIL
[[data-expected-height\] 31]
expected: FAIL
[[data-expected-height\] 42]
expected: FAIL
[[data-expected-height\] 43]
expected: FAIL
[[data-expected-height\] 44]
expected: FAIL
[[data-expected-height\] 45]
expected: FAIL

View file

@ -5,12 +5,6 @@
[[data-expected-width\] 19]
expected: FAIL
[[data-expected-width\] 20]
expected: FAIL
[[data-expected-width\] 21]
expected: FAIL
[[data-expected-width\] 22]
expected: FAIL
@ -20,11 +14,5 @@
[[data-expected-width\] 42]
expected: FAIL
[[data-expected-width\] 43]
expected: FAIL
[[data-expected-width\] 44]
expected: FAIL
[[data-expected-width\] 45]
expected: FAIL