diff options
author | Oriol Brufau <obrufau@igalia.com> | 2025-03-19 10:03:49 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-03-19 09:03:49 +0000 |
commit | ba6c3916fc916daeffd1e8d2c7ccba21d0eb1041 (patch) | |
tree | 9f3b4db94b5fcecc73d78fc2189ffd3354e2fb04 /components/layout_2020 | |
parent | 2362e4c134b3534287465092d6305043e5a580a6 (diff) | |
download | servo-ba6c3916fc916daeffd1e8d2c7ccba21d0eb1041.tar.gz servo-ba6c3916fc916daeffd1e8d2c7ccba21d0eb1041.zip |
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>
Diffstat (limited to 'components/layout_2020')
-rw-r--r-- | components/layout_2020/flexbox/layout.rs | 442 | ||||
-rw-r--r-- | components/layout_2020/geom.rs | 6 |
2 files changed, 159 insertions, 289 deletions
diff --git a/components/layout_2020/flexbox/layout.rs b/components/layout_2020/flexbox/layout.rs index c81524314ac..a95f2fd1143 100644 --- a/components/layout_2020/flexbox/layout.rs +++ b/components/layout_2020/flexbox/layout.rs @@ -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 (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 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 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)), }; - 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( + + // <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 container’s inner cross size + // > (clamped to the flex item’s 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 item’s 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 item’s main size is in its block axis, or when + // > it has a preferred aspect ratio) and the flex item’s cross size is auto and not definite, + // > in this calculation use fit-content as the flex item’s cross size. The flex base size is + // > the item’s 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 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 hypothetical_main_size = flex_base_size.clamp_between_extremums( - flex_relative_content_min_size.main, - flex_relative_content_max_size.main, + 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 item’s 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 it’s 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 item’s 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 it’s 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 container’s inner cross size - // > (clamped to the flex item’s 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 item’s 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 item’s main size is in its block axis, or when - // > it has a preferred aspect ratio) and the flex item’s cross size is auto and not definite, - // > in this calculation use fit-content as the flex item’s cross size. The flex base size is - // > the item’s 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", diff --git a/components/layout_2020/geom.rs b/components/layout_2020/geom.rs index 248598fe2d3..8d5ff95f743 100644 --- a/components/layout_2020/geom.rs +++ b/components/layout_2020/geom.rs @@ -730,12 +730,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 { Size::Initial => Size::Initial, |