| /* This Source Code Form is subject to the terms of the Mozilla Public |
| * License, v. 2.0. If a copy of the MPL was not distributed with this |
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ |
| |
| //! <https://drafts.csswg.org/css-sizing/> |
| |
| use std::cell::{LazyCell, OnceCell}; |
| use std::ops::{Add, AddAssign}; |
| |
| use app_units::{Au, MAX_AU}; |
| use malloc_size_of_derive::MallocSizeOf; |
| use style::Zero; |
| use style::logical_geometry::Direction; |
| use style::values::computed::{ |
| LengthPercentage, MaxSize as StyleMaxSize, Percentage, Size as StyleSize, |
| }; |
| |
| use crate::context::LayoutContext; |
| use crate::style_ext::{AspectRatio, Clamp, ComputedValuesExt, ContentBoxSizesAndPBM, LayoutStyle}; |
| use crate::{ConstraintSpace, IndefiniteContainingBlock, LogicalVec2}; |
| |
| #[derive(PartialEq)] |
| pub(crate) enum IntrinsicSizingMode { |
| /// Used to refer to a min-content contribution or max-content contribution. |
| /// This is the size that a box contributes to its containing block’s min-content |
| /// or max-content size. Note this is based on the outer size of the box, |
| /// and takes into account the relevant sizing properties of the element. |
| /// <https://drafts.csswg.org/css-sizing-3/#contributions> |
| Contribution, |
| /// Used to refer to a min-content size or max-content size. |
| /// This is the size based on the contents of an element, without regard for its context. |
| /// Note this is usually based on the inner (content-box) size of the box, |
| /// and ignores the relevant sizing properties of the element. |
| /// <https://drafts.csswg.org/css-sizing-3/#intrinsic> |
| Size, |
| } |
| |
| #[derive(Clone, Copy, Debug, Default, MallocSizeOf)] |
| pub(crate) struct ContentSizes { |
| pub min_content: Au, |
| pub max_content: Au, |
| } |
| |
| /// <https://drafts.csswg.org/css-sizing/#intrinsic-sizes> |
| impl ContentSizes { |
| pub fn max(&self, other: Self) -> Self { |
| Self { |
| min_content: self.min_content.max(other.min_content), |
| max_content: self.max_content.max(other.max_content), |
| } |
| } |
| |
| pub fn max_assign(&mut self, other: Self) { |
| *self = self.max(other); |
| } |
| |
| pub fn union(&self, other: &Self) -> Self { |
| Self { |
| min_content: self.min_content.max(other.min_content), |
| max_content: self.max_content + other.max_content, |
| } |
| } |
| |
| pub fn map(&self, f: impl Fn(Au) -> Au) -> Self { |
| Self { |
| min_content: f(self.min_content), |
| max_content: f(self.max_content), |
| } |
| } |
| } |
| |
| impl Zero for ContentSizes { |
| fn zero() -> Self { |
| Au::zero().into() |
| } |
| |
| fn is_zero(&self) -> bool { |
| self.min_content.is_zero() && self.max_content.is_zero() |
| } |
| } |
| |
| impl Add for ContentSizes { |
| type Output = Self; |
| |
| fn add(self, rhs: Self) -> Self { |
| Self { |
| min_content: self.min_content + rhs.min_content, |
| max_content: self.max_content + rhs.max_content, |
| } |
| } |
| } |
| |
| impl AddAssign for ContentSizes { |
| fn add_assign(&mut self, rhs: Self) { |
| *self = self.add(rhs) |
| } |
| } |
| |
| impl ContentSizes { |
| /// Clamps the provided amount to be between the min-content and the max-content. |
| /// This is called "shrink-to-fit" in CSS2, and "fit-content" in CSS Sizing. |
| /// <https://drafts.csswg.org/css2/visudet.html#shrink-to-fit-float> |
| /// <https://drafts.csswg.org/css-sizing/#funcdef-width-fit-content> |
| pub fn shrink_to_fit(&self, available_size: Au) -> Au { |
| // This formula is slightly different than what the spec says, |
| // to ensure that the minimum wins for a malformed ContentSize |
| // whose min_content is larger than its max_content. |
| available_size.min(self.max_content).max(self.min_content) |
| } |
| } |
| |
| impl From<Au> for ContentSizes { |
| fn from(size: Au) -> Self { |
| Self { |
| min_content: size, |
| max_content: size, |
| } |
| } |
| } |
| |
| #[allow(clippy::too_many_arguments)] |
| pub(crate) fn outer_inline( |
| layout_style: &LayoutStyle, |
| containing_block: &IndefiniteContainingBlock, |
| auto_minimum: &LogicalVec2<Au>, |
| auto_block_size_stretches_to_containing_block: bool, |
| is_replaced: bool, |
| establishes_containing_block: bool, |
| get_preferred_aspect_ratio: impl FnOnce(&LogicalVec2<Au>) -> Option<AspectRatio>, |
| get_inline_content_size: impl FnOnce(&ConstraintSpace) -> InlineContentSizesResult, |
| get_tentative_block_content_size: impl FnOnce(Option<AspectRatio>) -> Option<ContentSizes>, |
| ) -> InlineContentSizesResult { |
| let ContentBoxSizesAndPBM { |
| content_box_sizes, |
| pbm, |
| mut depends_on_block_constraints, |
| preferred_size_computes_to_auto, |
| } = layout_style.content_box_sizes_and_padding_border_margin(containing_block); |
| let margin = pbm.margin.map(|v| v.auto_is(Au::zero)); |
| let pbm_sums = LogicalVec2 { |
| block: pbm.padding_border_sums.block + margin.block_sum(), |
| inline: pbm.padding_border_sums.inline + margin.inline_sum(), |
| }; |
| let style = layout_style.style(); |
| let is_table = layout_style.is_table(); |
| let content_size = LazyCell::new(|| { |
| let constraint_space = if establishes_containing_block { |
| let available_block_size = containing_block |
| .size |
| .block |
| .map(|v| Au::zero().max(v - pbm_sums.block)); |
| let automatic_size = if preferred_size_computes_to_auto.block && |
| auto_block_size_stretches_to_containing_block |
| { |
| depends_on_block_constraints = true; |
| Size::Stretch |
| } else { |
| Size::FitContent |
| }; |
| let aspect_ratio = get_preferred_aspect_ratio(&pbm.padding_border_sums); |
| let block_size = |
| if let Some(block_content_size) = get_tentative_block_content_size(aspect_ratio) { |
| SizeConstraint::Definite(content_box_sizes.block.resolve( |
| Direction::Block, |
| automatic_size, |
| || auto_minimum.block, |
| available_block_size, |
| || block_content_size, |
| is_table, |
| )) |
| } else { |
| content_box_sizes.block.resolve_extrinsic( |
| automatic_size, |
| auto_minimum.block, |
| available_block_size, |
| ) |
| }; |
| ConstraintSpace::new(block_size, style.writing_mode, aspect_ratio) |
| } else { |
| // This assumes that there is no preferred aspect ratio, or that there is no |
| // block size constraint to be transferred so the ratio is irrelevant. |
| // We only get into here for anonymous blocks, for which the assumption holds. |
| ConstraintSpace::new( |
| containing_block.size.block.into(), |
| containing_block.writing_mode, |
| None, |
| ) |
| }; |
| get_inline_content_size(&constraint_space) |
| }); |
| let resolve_non_initial = |inline_size, stretch_values| { |
| Some(match inline_size { |
| Size::Initial => return None, |
| Size::Numeric(numeric) => (numeric, numeric, false), |
| Size::MinContent => ( |
| content_size.sizes.min_content, |
| content_size.sizes.min_content, |
| content_size.depends_on_block_constraints, |
| ), |
| Size::MaxContent => ( |
| content_size.sizes.max_content, |
| content_size.sizes.max_content, |
| content_size.depends_on_block_constraints, |
| ), |
| Size::FitContent => ( |
| content_size.sizes.min_content, |
| content_size.sizes.max_content, |
| content_size.depends_on_block_constraints, |
| ), |
| Size::FitContentFunction(size) => { |
| let size = content_size.sizes.shrink_to_fit(size); |
| (size, size, content_size.depends_on_block_constraints) |
| }, |
| Size::Stretch => return stretch_values, |
| }) |
| }; |
| let (mut preferred_min_content, preferred_max_content, preferred_depends_on_block_constraints) = |
| resolve_non_initial(content_box_sizes.inline.preferred, None) |
| .unwrap_or_else(|| resolve_non_initial(Size::FitContent, None).unwrap()); |
| let (mut min_min_content, mut min_max_content, mut min_depends_on_block_constraints) = |
| resolve_non_initial( |
| content_box_sizes.inline.min, |
| Some((Au::zero(), Au::zero(), false)), |
| ) |
| .unwrap_or((auto_minimum.inline, auto_minimum.inline, false)); |
| let (mut max_min_content, max_max_content, max_depends_on_block_constraints) = |
| resolve_non_initial(content_box_sizes.inline.max, None) |
| .map(|(min_content, max_content, depends_on_block_constraints)| { |
| ( |
| Some(min_content), |
| Some(max_content), |
| depends_on_block_constraints, |
| ) |
| }) |
| .unwrap_or_default(); |
| |
| // https://drafts.csswg.org/css-sizing-3/#replaced-percentage-min-contribution |
| // > If the box is replaced, a cyclic percentage in the value of any max size property |
| // > or preferred size property (width/max-width/height/max-height), is resolved against |
| // > zero when calculating the min-content contribution in the corresponding axis. |
| // |
| // This means that e.g. the min-content contribution of `width: calc(100% + 100px)` |
| // should be 100px, but it's just zero on other browsers, so we do the same. |
| if is_replaced { |
| let has_percentage = |size: Size<LengthPercentage>| { |
| // We need a comment here to avoid breaking `./mach test-tidy`. |
| matches!(size, Size::Numeric(numeric) if numeric.has_percentage()) |
| }; |
| if content_box_sizes.inline.preferred.is_initial() && |
| has_percentage(style.box_size(containing_block.writing_mode).inline) |
| { |
| preferred_min_content = Au::zero(); |
| } |
| if content_box_sizes.inline.max.is_initial() && |
| has_percentage(style.max_box_size(containing_block.writing_mode).inline) |
| { |
| max_min_content = Some(Au::zero()); |
| } |
| } |
| |
| // Regardless of their sizing properties, tables are always forced to be at least |
| // as big as their min-content size, so floor the minimums. |
| if is_table { |
| min_min_content.max_assign(content_size.sizes.min_content); |
| min_max_content.max_assign(content_size.sizes.min_content); |
| min_depends_on_block_constraints |= content_size.depends_on_block_constraints; |
| } |
| |
| InlineContentSizesResult { |
| sizes: ContentSizes { |
| min_content: preferred_min_content |
| .clamp_between_extremums(min_min_content, max_min_content) + |
| pbm_sums.inline, |
| max_content: preferred_max_content |
| .clamp_between_extremums(min_max_content, max_max_content) + |
| pbm_sums.inline, |
| }, |
| depends_on_block_constraints: depends_on_block_constraints && |
| (preferred_depends_on_block_constraints || |
| min_depends_on_block_constraints || |
| max_depends_on_block_constraints), |
| } |
| } |
| |
| #[derive(Clone, Copy, Debug, MallocSizeOf)] |
| pub(crate) struct InlineContentSizesResult { |
| pub sizes: ContentSizes, |
| pub depends_on_block_constraints: bool, |
| } |
| |
| pub(crate) trait ComputeInlineContentSizes { |
| fn compute_inline_content_sizes( |
| &self, |
| layout_context: &LayoutContext, |
| constraint_space: &ConstraintSpace, |
| ) -> InlineContentSizesResult; |
| |
| /// Returns the same result as [`Self::compute_inline_content_sizes()`], but adjusted |
| /// to floor the max-content size by the min-content size. |
| /// This is being discussed in <https://github.com/w3c/csswg-drafts/issues/12076>. |
| fn compute_inline_content_sizes_with_fixup( |
| &self, |
| layout_context: &LayoutContext, |
| constraint_space: &ConstraintSpace, |
| ) -> InlineContentSizesResult { |
| let mut result = self.compute_inline_content_sizes(layout_context, constraint_space); |
| let sizes = &mut result.sizes; |
| sizes.max_content.max_assign(sizes.min_content); |
| result |
| } |
| } |
| |
| /// The possible values accepted by the sizing properties. |
| /// <https://drafts.csswg.org/css-sizing/#sizing-properties> |
| #[derive(Clone, Debug, PartialEq)] |
| pub(crate) enum Size<T> { |
| /// Represents an `auto` value for the preferred and minimum size properties, |
| /// or `none` for the maximum size properties. |
| /// <https://drafts.csswg.org/css-sizing/#valdef-width-auto> |
| /// <https://drafts.csswg.org/css-sizing/#valdef-max-width-none> |
| Initial, |
| /// <https://drafts.csswg.org/css-sizing/#valdef-width-min-content> |
| MinContent, |
| /// <https://drafts.csswg.org/css-sizing/#valdef-width-max-content> |
| MaxContent, |
| /// <https://drafts.csswg.org/css-sizing-4/#valdef-width-fit-content> |
| FitContent, |
| /// <https://drafts.csswg.org/css-sizing-3/#funcdef-width-fit-content> |
| FitContentFunction(T), |
| /// <https://drafts.csswg.org/css-sizing-4/#valdef-width-stretch> |
| Stretch, |
| /// Represents a numeric `<length-percentage>`, but resolved as a `T`. |
| /// <https://drafts.csswg.org/css-sizing/#valdef-width-length-percentage-0> |
| Numeric(T), |
| } |
| |
| impl<T: Copy> Copy for Size<T> {} |
| |
| impl<T> Default for Size<T> { |
| #[inline] |
| fn default() -> Self { |
| Self::Initial |
| } |
| } |
| |
| impl<T> Size<T> { |
| #[inline] |
| pub(crate) fn is_initial(&self) -> bool { |
| matches!(self, Self::Initial) |
| } |
| } |
| |
| impl<T: Clone> Size<T> { |
| #[inline] |
| pub(crate) fn to_numeric(&self) -> Option<T> { |
| match self { |
| Self::Numeric(numeric) => Some(numeric).cloned(), |
| _ => None, |
| } |
| } |
| |
| #[inline] |
| pub(crate) fn map<U>(&self, f: impl FnOnce(T) -> U) -> Size<U> { |
| match self { |
| Size::Initial => Size::Initial, |
| Size::MinContent => Size::MinContent, |
| Size::MaxContent => Size::MaxContent, |
| Size::FitContent => Size::FitContent, |
| Size::FitContentFunction(size) => Size::FitContentFunction(f(size.clone())), |
| Size::Stretch => Size::Stretch, |
| Size::Numeric(numeric) => Size::Numeric(f(numeric.clone())), |
| } |
| } |
| } |
| |
| impl From<StyleSize> for Size<LengthPercentage> { |
| fn from(size: StyleSize) -> Self { |
| match size { |
| StyleSize::LengthPercentage(lp) => Size::Numeric(lp.0), |
| StyleSize::Auto => Size::Initial, |
| StyleSize::MinContent => Size::MinContent, |
| StyleSize::MaxContent => Size::MaxContent, |
| StyleSize::FitContent => Size::FitContent, |
| StyleSize::FitContentFunction(lp) => Size::FitContentFunction(lp.0), |
| StyleSize::Stretch => Size::Stretch, |
| StyleSize::AnchorSizeFunction(_) | StyleSize::AnchorContainingCalcFunction(_) => { |
| unreachable!("anchor-size() should be disabled") |
| }, |
| } |
| } |
| } |
| |
| impl From<StyleMaxSize> for Size<LengthPercentage> { |
| fn from(max_size: StyleMaxSize) -> Self { |
| match max_size { |
| StyleMaxSize::LengthPercentage(lp) => Size::Numeric(lp.0), |
| StyleMaxSize::None => Size::Initial, |
| StyleMaxSize::MinContent => Size::MinContent, |
| StyleMaxSize::MaxContent => Size::MaxContent, |
| StyleMaxSize::FitContent => Size::FitContent, |
| StyleMaxSize::FitContentFunction(lp) => Size::FitContentFunction(lp.0), |
| StyleMaxSize::Stretch => Size::Stretch, |
| StyleMaxSize::AnchorSizeFunction(_) | StyleMaxSize::AnchorContainingCalcFunction(_) => { |
| unreachable!("anchor-size() should be disabled") |
| }, |
| } |
| } |
| } |
| |
| impl Size<LengthPercentage> { |
| #[inline] |
| pub(crate) fn to_percentage(&self) -> Option<Percentage> { |
| self.to_numeric() |
| .and_then(|length_percentage| length_percentage.to_percentage()) |
| } |
| |
| /// Resolves percentages in a preferred size, against the provided basis. |
| /// If the basis is missing, percentages are considered cyclic. |
| /// <https://www.w3.org/TR/css-sizing-3/#preferred-size-properties> |
| /// <https://www.w3.org/TR/css-sizing-3/#cyclic-percentage-size> |
| #[inline] |
| pub(crate) fn resolve_percentages_for_preferred(&self, basis: Option<Au>) -> Size<Au> { |
| match self { |
| Size::Numeric(numeric) => numeric |
| .maybe_to_used_value(basis) |
| .map_or(Size::Initial, Size::Numeric), |
| Size::FitContentFunction(numeric) => { |
| // Under discussion in https://github.com/w3c/csswg-drafts/issues/11805 |
| numeric |
| .maybe_to_used_value(basis) |
| .map_or(Size::FitContent, Size::FitContentFunction) |
| }, |
| _ => self.map(|_| unreachable!("This shouldn't be called for keywords")), |
| } |
| } |
| |
| /// Resolves percentages in a maximum size, against the provided basis. |
| /// If the basis is missing, percentages are considered cyclic. |
| /// <https://www.w3.org/TR/css-sizing-3/#preferred-size-properties> |
| /// <https://www.w3.org/TR/css-sizing-3/#cyclic-percentage-size> |
| #[inline] |
| pub(crate) fn resolve_percentages_for_max(&self, basis: Option<Au>) -> Size<Au> { |
| match self { |
| Size::Numeric(numeric) => numeric |
| .maybe_to_used_value(basis) |
| .map_or(Size::Initial, Size::Numeric), |
| Size::FitContentFunction(numeric) => { |
| // Under discussion in https://github.com/w3c/csswg-drafts/issues/11805 |
| numeric |
| .maybe_to_used_value(basis) |
| .map_or(Size::MaxContent, Size::FitContentFunction) |
| }, |
| _ => self.map(|_| unreachable!("This shouldn't be called for keywords")), |
| } |
| } |
| } |
| |
| impl LogicalVec2<Size<LengthPercentage>> { |
| pub(crate) fn percentages_relative_to_basis( |
| &self, |
| basis: &LogicalVec2<Au>, |
| ) -> LogicalVec2<Size<Au>> { |
| LogicalVec2 { |
| inline: self.inline.map(|value| value.to_used_value(basis.inline)), |
| block: self.block.map(|value| value.to_used_value(basis.block)), |
| } |
| } |
| } |
| |
| impl Size<Au> { |
| /// Resolves a preferred size into a numerical value. |
| /// <https://www.w3.org/TR/css-sizing-3/#preferred-size-properties> |
| #[inline] |
| pub(crate) fn resolve_for_preferred<F: FnOnce() -> ContentSizes>( |
| &self, |
| automatic_size: Size<Au>, |
| stretch_size: Option<Au>, |
| content_size: &LazyCell<ContentSizes, F>, |
| ) -> Au { |
| match self { |
| Self::Initial => { |
| assert!(!automatic_size.is_initial()); |
| automatic_size.resolve_for_preferred(automatic_size, stretch_size, content_size) |
| }, |
| Self::MinContent => content_size.min_content, |
| Self::MaxContent => content_size.max_content, |
| Self::FitContentFunction(size) => content_size.shrink_to_fit(*size), |
| Self::FitContent => { |
| content_size.shrink_to_fit(stretch_size.unwrap_or_else(|| content_size.max_content)) |
| }, |
| Self::Stretch => stretch_size.unwrap_or_else(|| content_size.max_content), |
| Self::Numeric(numeric) => *numeric, |
| } |
| } |
| |
| /// Resolves a minimum size into a numerical value. |
| /// <https://www.w3.org/TR/css-sizing-3/#min-size-properties> |
| #[inline] |
| pub(crate) fn resolve_for_min<F: FnOnce() -> ContentSizes>( |
| &self, |
| get_automatic_minimum_size: impl FnOnce() -> Au, |
| stretch_size: Option<Au>, |
| content_size: &LazyCell<ContentSizes, F>, |
| is_table: bool, |
| ) -> Au { |
| let result = match self { |
| Self::Initial => get_automatic_minimum_size(), |
| Self::MinContent => content_size.min_content, |
| Self::MaxContent => content_size.max_content, |
| Self::FitContentFunction(size) => content_size.shrink_to_fit(*size), |
| Self::FitContent => content_size.shrink_to_fit(stretch_size.unwrap_or_default()), |
| Self::Stretch => stretch_size.unwrap_or_default(), |
| Self::Numeric(numeric) => *numeric, |
| }; |
| if is_table { |
| // In addition to the specified minimum, the inline size of a table is forced to be |
| // at least as big as its min-content size. |
| // |
| // Note that if there are collapsed columns, only the inline size of the table grid will |
| // shrink, while the size of the table wrapper (being computed here) won't be affected. |
| // However, collapsed rows should typically affect the block size of the table wrapper, |
| // so it might be wrong to use this function for that case. |
| // This is being discussed in https://github.com/w3c/csswg-drafts/issues/11408 |
| result.max(content_size.min_content) |
| } else { |
| result |
| } |
| } |
| |
| /// Resolves a maximum size into a numerical value. |
| /// <https://www.w3.org/TR/css-sizing-3/#max-size-properties> |
| #[inline] |
| pub(crate) fn resolve_for_max<F: FnOnce() -> ContentSizes>( |
| &self, |
| stretch_size: Option<Au>, |
| content_size: &LazyCell<ContentSizes, F>, |
| ) -> Option<Au> { |
| Some(match self { |
| Self::Initial => return None, |
| Self::MinContent => content_size.min_content, |
| Self::MaxContent => content_size.max_content, |
| Self::FitContentFunction(size) => content_size.shrink_to_fit(*size), |
| Self::FitContent => content_size.shrink_to_fit(stretch_size.unwrap_or(MAX_AU)), |
| Self::Stretch => return stretch_size, |
| Self::Numeric(numeric) => *numeric, |
| }) |
| } |
| |
| /// Tries to resolve an extrinsic size into a numerical value. |
| /// Extrinsic sizes are those based on the context of an element, without regard for its contents. |
| /// <https://drafts.csswg.org/css-sizing-3/#extrinsic> |
| /// |
| /// Returns `None` if either: |
| /// - The size is intrinsic. |
| /// - The size is the initial one. |
| /// TODO: should we allow it to behave as `stretch` instead of assuming it's intrinsic? |
| /// - The provided `stretch_size` is `None` but we need its value. |
| #[inline] |
| pub(crate) fn maybe_resolve_extrinsic(&self, stretch_size: Option<Au>) -> Option<Au> { |
| match self { |
| Self::Initial | |
| Self::MinContent | |
| Self::MaxContent | |
| Self::FitContent | |
| Self::FitContentFunction(_) => None, |
| Self::Stretch => stretch_size, |
| Self::Numeric(numeric) => Some(*numeric), |
| } |
| } |
| } |
| |
| /// Represents the sizing constraint that the preferred, min and max sizing properties |
| /// impose on one axis. |
| #[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq)] |
| pub(crate) enum SizeConstraint { |
| /// Represents a definite preferred size, clamped by minimum and maximum sizes (if any). |
| Definite(Au), |
| /// Represents an indefinite preferred size that allows a range of values between |
| /// the first argument (minimum size) and the second one (maximum size). |
| MinMax(Au, Option<Au>), |
| } |
| |
| impl Default for SizeConstraint { |
| #[inline] |
| fn default() -> Self { |
| Self::MinMax(Au::default(), None) |
| } |
| } |
| |
| impl SizeConstraint { |
| #[inline] |
| pub(crate) fn new(preferred_size: Option<Au>, min_size: Au, max_size: Option<Au>) -> Self { |
| preferred_size.map_or_else( |
| || Self::MinMax(min_size, max_size), |
| |size| Self::Definite(size.clamp_between_extremums(min_size, max_size)), |
| ) |
| } |
| |
| #[inline] |
| pub(crate) fn is_definite(self) -> bool { |
| matches!(self, Self::Definite(_)) |
| } |
| |
| #[inline] |
| pub(crate) fn to_definite(self) -> Option<Au> { |
| match self { |
| Self::Definite(size) => Some(size), |
| _ => None, |
| } |
| } |
| } |
| |
| impl From<Option<Au>> for SizeConstraint { |
| fn from(size: Option<Au>) -> Self { |
| size.map(SizeConstraint::Definite).unwrap_or_default() |
| } |
| } |
| |
| #[derive(Clone, Debug, Default)] |
| pub(crate) struct Sizes { |
| /// <https://drafts.csswg.org/css-sizing-3/#preferred-size-properties> |
| pub preferred: Size<Au>, |
| /// <https://drafts.csswg.org/css-sizing-3/#min-size-properties> |
| pub min: Size<Au>, |
| /// <https://drafts.csswg.org/css-sizing-3/#max-size-properties> |
| pub max: Size<Au>, |
| } |
| |
| impl Sizes { |
| #[inline] |
| pub(crate) fn new(preferred: Size<Au>, min: Size<Au>, max: Size<Au>) -> Self { |
| Self { |
| preferred, |
| min, |
| max, |
| } |
| } |
| |
| /// Resolves the three sizes into a single numerical value. |
| #[inline] |
| pub(crate) fn resolve( |
| &self, |
| axis: Direction, |
| automatic_size: Size<Au>, |
| get_automatic_minimum_size: impl FnOnce() -> Au, |
| stretch_size: Option<Au>, |
| get_content_size: impl FnOnce() -> ContentSizes, |
| is_table: bool, |
| ) -> Au { |
| if is_table && axis == Direction::Block { |
| // The intrinsic block size of a table already takes sizing properties into account, |
| // but it can be a smaller amount if there are collapsed rows. |
| // Therefore, disregard sizing properties and just defer to the intrinsic size. |
| // This is being discussed in https://github.com/w3c/csswg-drafts/issues/11408 |
| return get_content_size().max_content; |
| } |
| let (preferred, min, max) = self.resolve_each( |
| automatic_size, |
| get_automatic_minimum_size, |
| stretch_size, |
| get_content_size, |
| is_table, |
| ); |
| preferred.clamp_between_extremums(min, max) |
| } |
| |
| /// Resolves each of the three sizes into a numerical value, separately. |
| /// - The 1st returned value is the resolved preferred size. |
| /// - The 2nd returned value is the resolved minimum size. |
| /// - The 3rd returned value is the resolved maximum size. `None` means no maximum. |
| #[inline] |
| pub(crate) fn resolve_each( |
| &self, |
| automatic_size: Size<Au>, |
| get_automatic_minimum_size: impl FnOnce() -> Au, |
| stretch_size: Option<Au>, |
| get_content_size: impl FnOnce() -> ContentSizes, |
| is_table: bool, |
| ) -> (Au, Au, Option<Au>) { |
| // The provided `get_content_size` is a FnOnce but we may need its result multiple times. |
| // A LazyCell will only invoke it once if needed, and then reuse the result. |
| let content_size = LazyCell::new(get_content_size); |
| ( |
| self.preferred |
| .resolve_for_preferred(automatic_size, stretch_size, &content_size), |
| self.min.resolve_for_min( |
| get_automatic_minimum_size, |
| stretch_size, |
| &content_size, |
| is_table, |
| ), |
| self.max.resolve_for_max(stretch_size, &content_size), |
| ) |
| } |
| |
| /// Tries to extrinsically resolve the three sizes into a single [`SizeConstraint`]. |
| /// Values that are intrinsic or need `stretch_size` when it's `None` are handled as such: |
| /// - On the preferred size, they make the returned value be an indefinite [`SizeConstraint::MinMax`]. |
| /// - On the min size, they are treated as `auto`, enforcing the automatic minimum size. |
| /// - On the max size, they are treated as `none`, enforcing no maximum. |
| #[inline] |
| pub(crate) fn resolve_extrinsic( |
| &self, |
| automatic_size: Size<Au>, |
| automatic_minimum_size: Au, |
| stretch_size: Option<Au>, |
| ) -> SizeConstraint { |
| let (preferred, min, max) = |
| self.resolve_each_extrinsic(automatic_size, automatic_minimum_size, stretch_size); |
| SizeConstraint::new(preferred, min, max) |
| } |
| |
| /// Tries to extrinsically resolve each of the three sizes into a numerical value, separately. |
| /// This can't resolve values that are intrinsic or need `stretch_size` but it's `None`. |
| /// - The 1st returned value is the resolved preferred size. If it can't be resolved then |
| /// the returned value is `None`. Note that this is different than treating it as `auto`. |
| /// TODO: This needs to be discussed in <https://github.com/w3c/csswg-drafts/issues/11387>. |
| /// - The 2nd returned value is the resolved minimum size. If it can't be resolved then we |
| /// treat it as the initial `auto`, returning the automatic minimum size. |
| /// - The 3rd returned value is the resolved maximum size. If it can't be resolved then we |
| /// treat it as the initial `none`, returning `None`. |
| #[inline] |
| pub(crate) fn resolve_each_extrinsic( |
| &self, |
| automatic_size: Size<Au>, |
| automatic_minimum_size: Au, |
| stretch_size: Option<Au>, |
| ) -> (Option<Au>, Au, Option<Au>) { |
| ( |
| if self.preferred.is_initial() { |
| automatic_size.maybe_resolve_extrinsic(stretch_size) |
| } else { |
| self.preferred.maybe_resolve_extrinsic(stretch_size) |
| }, |
| self.min |
| .maybe_resolve_extrinsic(stretch_size) |
| .unwrap_or(automatic_minimum_size), |
| self.max.maybe_resolve_extrinsic(stretch_size), |
| ) |
| } |
| } |
| |
| struct LazySizeData<'a> { |
| sizes: &'a Sizes, |
| axis: Direction, |
| automatic_size: Size<Au>, |
| get_automatic_minimum_size: fn() -> Au, |
| stretch_size: Option<Au>, |
| is_table: bool, |
| } |
| |
| /// Represents a size that can't be fully resolved until the intrinsic size |
| /// is known. This is useful in the block axis, since the intrinsic size |
| /// depends on layout, but the other inputs are known beforehand. |
| pub(crate) struct LazySize<'a> { |
| result: OnceCell<Au>, |
| data: Option<LazySizeData<'a>>, |
| } |
| |
| impl<'a> LazySize<'a> { |
| pub(crate) fn new( |
| sizes: &'a Sizes, |
| axis: Direction, |
| automatic_size: Size<Au>, |
| get_automatic_minimum_size: fn() -> Au, |
| stretch_size: Option<Au>, |
| is_table: bool, |
| ) -> Self { |
| Self { |
| result: OnceCell::new(), |
| data: Some(LazySizeData { |
| sizes, |
| axis, |
| automatic_size, |
| get_automatic_minimum_size, |
| stretch_size, |
| is_table, |
| }), |
| } |
| } |
| |
| /// Creates a [`LazySize`] that will resolve to the intrinsic size. |
| /// Should be equivalent to [`LazySize::new()`] with default parameters, |
| /// but avoiding the trouble of getting a reference to a [`Sizes::default()`] |
| /// which lives long enough. |
| /// |
| /// TODO: It's not clear what this should do if/when [`LazySize::resolve()`] |
| /// is changed to accept a [`ContentSizes`] as the intrinsic size. |
| pub(crate) fn intrinsic() -> Self { |
| Self { |
| result: OnceCell::new(), |
| data: None, |
| } |
| } |
| |
| /// Resolves the [`LazySize`] into [`Au`], caching the result. |
| /// The argument is a callback that computes the intrinsic size lazily. |
| /// |
| /// TODO: The intrinsic size should probably be a [`ContentSizes`] instead of [`Au`]. |
| pub(crate) fn resolve(&self, get_content_size: impl FnOnce() -> Au) -> Au { |
| *self.result.get_or_init(|| { |
| let Some(ref data) = self.data else { |
| return get_content_size(); |
| }; |
| data.sizes.resolve( |
| data.axis, |
| data.automatic_size, |
| data.get_automatic_minimum_size, |
| data.stretch_size, |
| || get_content_size().into(), |
| data.is_table, |
| ) |
| }) |
| } |
| } |
| |
| impl From<Au> for LazySize<'_> { |
| /// Creates a [`LazySize`] that will resolve to the given [`Au`], |
| /// ignoring the intrinsic size. |
| fn from(value: Au) -> Self { |
| let result = OnceCell::new(); |
| result.set(value).unwrap(); |
| LazySize { result, data: None } |
| } |
| } |