Skip to main content

layout/
sizing.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! <https://drafts.csswg.org/css-sizing/>
6
7use std::cell::{Cell, LazyCell, OnceCell};
8use std::ops::{Add, AddAssign};
9use std::sync::atomic::Ordering;
10
11use app_units::{Au, MAX_AU};
12use malloc_size_of_derive::MallocSizeOf;
13use style::Zero;
14use style::logical_geometry::LogicalAxis;
15use style::values::computed::{
16    LengthPercentage, MaxSize as StyleMaxSize, Percentage, Size as StyleSize,
17};
18
19use crate::context::LayoutContext;
20use crate::layout_box_base::LayoutBoxBase;
21use crate::style_ext::{AspectRatio, Clamp, ComputedValuesExt, ContentBoxSizesAndPBM, LayoutStyle};
22use crate::{ConstraintSpace, IndefiniteContainingBlock, LogicalVec2};
23
24#[derive(PartialEq)]
25pub(crate) enum IntrinsicSizingMode {
26    /// Used to refer to a min-content contribution or max-content contribution.
27    /// This is the size that a box contributes to its containing block’s min-content
28    /// or max-content size. Note this is based on the outer size of the box,
29    /// and takes into account the relevant sizing properties of the element.
30    /// <https://drafts.csswg.org/css-sizing-3/#contributions>
31    Contribution,
32    /// Used to refer to a min-content size or max-content size.
33    /// This is the size based on the contents of an element, without regard for its context.
34    /// Note this is usually based on the inner (content-box) size of the box,
35    /// and ignores the relevant sizing properties of the element.
36    /// <https://drafts.csswg.org/css-sizing-3/#intrinsic>
37    Size,
38}
39
40#[derive(Clone, Copy, Debug, Default, MallocSizeOf)]
41pub(crate) struct ContentSizes {
42    pub min_content: Au,
43    pub max_content: Au,
44}
45
46/// <https://drafts.csswg.org/css-sizing/#intrinsic-sizes>
47impl ContentSizes {
48    pub fn max(&self, other: Self) -> Self {
49        Self {
50            min_content: self.min_content.max(other.min_content),
51            max_content: self.max_content.max(other.max_content),
52        }
53    }
54
55    pub fn max_assign(&mut self, other: Self) {
56        *self = self.max(other);
57    }
58
59    pub fn union(&self, other: &Self) -> Self {
60        Self {
61            min_content: self.min_content.max(other.min_content),
62            max_content: self.max_content + other.max_content,
63        }
64    }
65
66    pub fn union_assign(&mut self, other: &Self) {
67        self.min_content.max_assign(other.min_content);
68        self.max_content += other.max_content;
69    }
70
71    pub fn map(&self, f: impl Fn(Au) -> Au) -> Self {
72        Self {
73            min_content: f(self.min_content),
74            max_content: f(self.max_content),
75        }
76    }
77}
78
79impl Zero for ContentSizes {
80    fn zero() -> Self {
81        Au::zero().into()
82    }
83
84    fn is_zero(&self) -> bool {
85        self.min_content.is_zero() && self.max_content.is_zero()
86    }
87}
88
89impl Add for ContentSizes {
90    type Output = Self;
91
92    fn add(self, rhs: Self) -> Self {
93        Self {
94            min_content: self.min_content + rhs.min_content,
95            max_content: self.max_content + rhs.max_content,
96        }
97    }
98}
99
100impl AddAssign for ContentSizes {
101    fn add_assign(&mut self, rhs: Self) {
102        *self = self.add(rhs)
103    }
104}
105
106impl ContentSizes {
107    /// Clamps the provided amount to be between the min-content and the max-content.
108    /// This is called "shrink-to-fit" in CSS2, and "fit-content" in CSS Sizing.
109    /// <https://drafts.csswg.org/css2/visudet.html#shrink-to-fit-float>
110    /// <https://drafts.csswg.org/css-sizing/#funcdef-width-fit-content>
111    pub fn shrink_to_fit(&self, available_size: Au) -> Au {
112        // This formula is slightly different than what the spec says,
113        // to ensure that the minimum wins for a malformed ContentSize
114        // whose min_content is larger than its max_content.
115        available_size.min(self.max_content).max(self.min_content)
116    }
117}
118
119impl From<Au> for ContentSizes {
120    fn from(size: Au) -> Self {
121        Self {
122            min_content: size,
123            max_content: size,
124        }
125    }
126}
127
128#[expect(clippy::too_many_arguments)]
129pub(crate) fn outer_inline(
130    base: &LayoutBoxBase,
131    layout_style: &LayoutStyle,
132    containing_block: &IndefiniteContainingBlock,
133    auto_minimum: &LogicalVec2<Au>,
134    auto_block_size_stretches_to_containing_block: bool,
135    is_replaced: bool,
136    establishes_containing_block: bool,
137    skippable_for_block_percentage_quirk: bool,
138    get_preferred_aspect_ratio: impl FnOnce(&LogicalVec2<Au>) -> Option<AspectRatio>,
139    get_inline_content_size: impl FnOnce(&ConstraintSpace) -> InlineContentSizesResult,
140    get_tentative_block_content_size: impl FnOnce(Option<AspectRatio>) -> Option<ContentSizes>,
141) -> InlineContentSizesResult {
142    let ContentBoxSizesAndPBM {
143        content_box_sizes,
144        pbm,
145        mut depends_on_block_constraints,
146        preferred_size_computes_to_auto,
147    } = layout_style.content_box_sizes_and_padding_border_margin(containing_block);
148    let margin = pbm.margin.map(|v| v.auto_is(Au::zero));
149    let pbm_sums = LogicalVec2 {
150        block: pbm.padding_border_sums.block + margin.block_sum(),
151        inline: pbm.padding_border_sums.inline + margin.inline_sum(),
152    };
153    let style = layout_style.style();
154    let is_table = layout_style.is_table();
155    // TODO: Replace `depends_on_contents` with `content_size.get().is_some()` once `LazyCell::get()`
156    // becomes stable.
157    let depends_on_contents = Cell::new(false);
158    let content_size = LazyCell::new(|| {
159        depends_on_contents.set(true);
160        let constraint_space = if establishes_containing_block {
161            let block_stretch_size = containing_block.block_stretch_size(pbm_sums.block);
162            let automatic_size = if preferred_size_computes_to_auto.block &&
163                auto_block_size_stretches_to_containing_block
164            {
165                depends_on_block_constraints = true;
166                Size::Stretch
167            } else {
168                Size::FitContent
169            };
170            let aspect_ratio = get_preferred_aspect_ratio(&pbm.padding_border_sums);
171            let mut block_size =
172                if let Some(block_content_size) = get_tentative_block_content_size(aspect_ratio) {
173                    SizeConstraint::Definite(content_box_sizes.block.resolve(
174                        LogicalAxis::Block,
175                        automatic_size,
176                        || auto_minimum.block,
177                        block_stretch_size,
178                        || block_content_size,
179                        is_table,
180                    ))
181                } else {
182                    content_box_sizes.block.resolve_extrinsic(
183                        automatic_size,
184                        auto_minimum.block,
185                        block_stretch_size,
186                    )
187                };
188            if skippable_for_block_percentage_quirk {
189                block_size.replace_if_indefinite_with(containing_block.size.block.into())
190            }
191            ConstraintSpace::new(block_size, style, aspect_ratio)
192        } else {
193            // Even if the size doesn't directly depend on block constraints, since this box
194            // doesn't establish a containing block, its contents can depend on its block
195            // constraints. And thus have a dependency via the intrinsic size.
196            depends_on_block_constraints = true;
197            // This assumes that there is no preferred aspect ratio, or that there is no
198            // block size constraint to be transferred so the ratio is irrelevant.
199            // We only get into here for anonymous blocks, for which the assumption holds.
200            ConstraintSpace::new(
201                containing_block.size.block.into(),
202                containing_block.style,
203                None,
204            )
205        };
206        get_inline_content_size(&constraint_space)
207    });
208    let resolve_non_initial = |inline_size, stretch_values| {
209        Some(match inline_size {
210            Size::Initial => return None,
211            Size::Numeric(numeric) => (numeric, numeric, false),
212            Size::MinContent => (
213                content_size.sizes.min_content,
214                content_size.sizes.min_content,
215                content_size.depends_on_block_constraints,
216            ),
217            Size::MaxContent => (
218                content_size.sizes.max_content,
219                content_size.sizes.max_content,
220                content_size.depends_on_block_constraints,
221            ),
222            Size::FitContent => (
223                content_size.sizes.min_content,
224                content_size.sizes.max_content,
225                content_size.depends_on_block_constraints,
226            ),
227            Size::FitContentFunction(size) => {
228                let size = content_size.sizes.shrink_to_fit(size);
229                (size, size, content_size.depends_on_block_constraints)
230            },
231            Size::Stretch => return stretch_values,
232            Size::QuirkyStretch => unreachable!("Quirky stretch can't be an inline size"),
233        })
234    };
235    let (mut preferred_min_content, preferred_max_content, preferred_depends_on_block_constraints) =
236        resolve_non_initial(content_box_sizes.inline.preferred, None)
237            .unwrap_or_else(|| resolve_non_initial(Size::FitContent, None).unwrap());
238    let (mut min_min_content, mut min_max_content, mut min_depends_on_block_constraints) =
239        resolve_non_initial(
240            content_box_sizes.inline.min,
241            Some((Au::zero(), Au::zero(), false)),
242        )
243        .unwrap_or((auto_minimum.inline, auto_minimum.inline, false));
244    let (mut max_min_content, max_max_content, max_depends_on_block_constraints) =
245        resolve_non_initial(content_box_sizes.inline.max, None)
246            .map(|(min_content, max_content, depends_on_block_constraints)| {
247                (
248                    Some(min_content),
249                    Some(max_content),
250                    depends_on_block_constraints,
251                )
252            })
253            .unwrap_or_default();
254
255    // https://drafts.csswg.org/css-sizing-3/#replaced-percentage-min-contribution
256    // > If the box is replaced, a cyclic percentage in the value of any max size property
257    // > or preferred size property (width/max-width/height/max-height), is resolved against
258    // > zero when calculating the min-content contribution in the corresponding axis.
259    //
260    // This means that e.g. the min-content contribution of `width: calc(100% + 100px)`
261    // should be 100px, but it's just zero on other browsers, so we do the same.
262    if is_replaced {
263        let has_percentage = |size: Size<LengthPercentage>| {
264            // We need a comment here to avoid breaking `./mach test-tidy`.
265            matches!(size, Size::Numeric(numeric) if numeric.has_percentage())
266        };
267        let writing_mode = containing_block.style.writing_mode;
268        if content_box_sizes.inline.preferred.is_initial() &&
269            has_percentage(style.box_size(writing_mode).inline)
270        {
271            preferred_min_content = Au::zero();
272        }
273        if content_box_sizes.inline.max.is_initial() &&
274            has_percentage(style.max_box_size(writing_mode).inline)
275        {
276            max_min_content = Some(Au::zero());
277        }
278    }
279
280    // Regardless of their sizing properties, tables are always forced to be at least
281    // as big as their min-content size, so floor the minimums.
282    if is_table {
283        min_min_content.max_assign(content_size.sizes.min_content);
284        min_max_content.max_assign(content_size.sizes.min_content);
285        min_depends_on_block_constraints |= content_size.depends_on_block_constraints;
286    }
287
288    base.outer_inline_content_sizes_depend_on_content
289        .store(depends_on_contents.get(), Ordering::Relaxed);
290    InlineContentSizesResult {
291        sizes: ContentSizes {
292            min_content: preferred_min_content
293                .clamp_between_extremums(min_min_content, max_min_content) +
294                pbm_sums.inline,
295            max_content: preferred_max_content
296                .clamp_between_extremums(min_max_content, max_max_content) +
297                pbm_sums.inline,
298        },
299        depends_on_block_constraints: depends_on_block_constraints &&
300            (preferred_depends_on_block_constraints ||
301                min_depends_on_block_constraints ||
302                max_depends_on_block_constraints),
303    }
304}
305
306#[derive(Clone, Copy, Debug, MallocSizeOf)]
307pub(crate) struct InlineContentSizesResult {
308    pub sizes: ContentSizes,
309    pub depends_on_block_constraints: bool,
310}
311
312pub(crate) trait ComputeInlineContentSizes {
313    fn compute_inline_content_sizes(
314        &self,
315        layout_context: &LayoutContext,
316        constraint_space: &ConstraintSpace,
317    ) -> InlineContentSizesResult;
318
319    /// Returns the same result as [`Self::compute_inline_content_sizes()`], but adjusted
320    /// to floor the max-content size by the min-content size.
321    /// This is being discussed in <https://github.com/w3c/csswg-drafts/issues/12076>.
322    fn compute_inline_content_sizes_with_fixup(
323        &self,
324        layout_context: &LayoutContext,
325        constraint_space: &ConstraintSpace,
326    ) -> InlineContentSizesResult {
327        let mut result = self.compute_inline_content_sizes(layout_context, constraint_space);
328        let sizes = &mut result.sizes;
329        sizes.max_content.max_assign(sizes.min_content);
330        result
331    }
332}
333
334/// The possible values accepted by the sizing properties.
335/// <https://drafts.csswg.org/css-sizing/#sizing-properties>
336#[derive(Clone, Debug, PartialEq)]
337pub(crate) enum Size<T> {
338    /// Represents an `auto` value for the preferred and minimum size properties,
339    /// or `none` for the maximum size properties.
340    /// <https://drafts.csswg.org/css-sizing/#valdef-width-auto>
341    /// <https://drafts.csswg.org/css-sizing/#valdef-max-width-none>
342    Initial,
343    /// <https://drafts.csswg.org/css-sizing/#valdef-width-min-content>
344    MinContent,
345    /// <https://drafts.csswg.org/css-sizing/#valdef-width-max-content>
346    MaxContent,
347    /// <https://drafts.csswg.org/css-sizing-4/#valdef-width-fit-content>
348    FitContent,
349    /// <https://drafts.csswg.org/css-sizing-3/#funcdef-width-fit-content>
350    FitContentFunction(T),
351    /// <https://drafts.csswg.org/css-sizing-4/#valdef-width-stretch>
352    Stretch,
353    /// Represents the special stretching behavior of `<html>` and `<body>`
354    /// in quirks mode. Roughly, it behaves as the maximum of `fit-content`
355    /// and `stretch`. It can only be used as a preferred block size.
356    /// <https://quirks.spec.whatwg.org/#the-html-element-fills-the-viewport-quirk>
357    /// <https://quirks.spec.whatwg.org/#the-body-element-fills-the-html-element-quirk>
358    QuirkyStretch,
359    /// Represents a numeric `<length-percentage>`, but resolved as a `T`.
360    /// <https://drafts.csswg.org/css-sizing/#valdef-width-length-percentage-0>
361    Numeric(T),
362}
363
364impl<T: Copy> Copy for Size<T> {}
365
366impl<T> Default for Size<T> {
367    #[inline]
368    fn default() -> Self {
369        Self::Initial
370    }
371}
372
373impl<T> Size<T> {
374    #[inline]
375    pub(crate) fn is_initial(&self) -> bool {
376        matches!(self, Self::Initial)
377    }
378}
379
380impl<T: Clone> Size<T> {
381    #[inline]
382    pub(crate) fn to_numeric(&self) -> Option<T> {
383        match self {
384            Self::Numeric(numeric) => Some(numeric).cloned(),
385            _ => None,
386        }
387    }
388
389    #[inline]
390    pub(crate) fn map<U>(&self, f: impl FnOnce(T) -> U) -> Size<U> {
391        match self {
392            Size::Initial => Size::Initial,
393            Size::MinContent => Size::MinContent,
394            Size::MaxContent => Size::MaxContent,
395            Size::FitContent => Size::FitContent,
396            Size::FitContentFunction(size) => Size::FitContentFunction(f(size.clone())),
397            Size::Stretch => Size::Stretch,
398            Size::QuirkyStretch => Size::QuirkyStretch,
399            Size::Numeric(numeric) => Size::Numeric(f(numeric.clone())),
400        }
401    }
402}
403
404impl From<&StyleSize> for Size<LengthPercentage> {
405    fn from(size: &StyleSize) -> Self {
406        match size {
407            StyleSize::LengthPercentage(lp) => Size::Numeric(lp.0.clone()),
408            StyleSize::Auto => Size::Initial,
409            StyleSize::MinContent => Size::MinContent,
410            StyleSize::MaxContent => Size::MaxContent,
411            StyleSize::FitContent => Size::FitContent,
412            StyleSize::FitContentFunction(lp) => Size::FitContentFunction(lp.0.clone()),
413            StyleSize::Stretch | StyleSize::WebkitFillAvailable => Size::Stretch,
414            StyleSize::AnchorSizeFunction(_) | StyleSize::AnchorContainingCalcFunction(_) => {
415                unreachable!("anchor-size() should be disabled")
416            },
417        }
418    }
419}
420
421impl From<&StyleMaxSize> for Size<LengthPercentage> {
422    fn from(max_size: &StyleMaxSize) -> Self {
423        match max_size {
424            StyleMaxSize::LengthPercentage(lp) => Size::Numeric(lp.0.clone()),
425            StyleMaxSize::None => Size::Initial,
426            StyleMaxSize::MinContent => Size::MinContent,
427            StyleMaxSize::MaxContent => Size::MaxContent,
428            StyleMaxSize::FitContent => Size::FitContent,
429            StyleMaxSize::FitContentFunction(lp) => Size::FitContentFunction(lp.0.clone()),
430            StyleMaxSize::Stretch | StyleMaxSize::WebkitFillAvailable => Size::Stretch,
431            StyleMaxSize::AnchorSizeFunction(_) | StyleMaxSize::AnchorContainingCalcFunction(_) => {
432                unreachable!("anchor-size() should be disabled")
433            },
434        }
435    }
436}
437
438impl Size<LengthPercentage> {
439    #[inline]
440    pub(crate) fn to_percentage(&self) -> Option<Percentage> {
441        self.to_numeric()
442            .and_then(|length_percentage| length_percentage.to_percentage())
443    }
444
445    /// Resolves percentages in a preferred size, against the provided basis.
446    /// If the basis is missing, percentages are considered cyclic.
447    /// <https://www.w3.org/TR/css-sizing-3/#preferred-size-properties>
448    /// <https://www.w3.org/TR/css-sizing-3/#cyclic-percentage-size>
449    #[inline]
450    pub(crate) fn resolve_percentages_for_preferred(&self, basis: Option<Au>) -> Size<Au> {
451        match self {
452            Size::Numeric(numeric) => numeric
453                .maybe_to_used_value(basis)
454                .map_or(Size::Initial, Size::Numeric),
455            Size::FitContentFunction(numeric) => {
456                // Under discussion in https://github.com/w3c/csswg-drafts/issues/11805
457                numeric
458                    .maybe_to_used_value(basis)
459                    .map_or(Size::FitContent, Size::FitContentFunction)
460            },
461            _ => self.map(|_| unreachable!("This shouldn't be called for keywords")),
462        }
463    }
464
465    /// Resolves percentages in a maximum size, against the provided basis.
466    /// If the basis is missing, percentages are considered cyclic.
467    /// <https://www.w3.org/TR/css-sizing-3/#preferred-size-properties>
468    /// <https://www.w3.org/TR/css-sizing-3/#cyclic-percentage-size>
469    #[inline]
470    pub(crate) fn resolve_percentages_for_max(&self, basis: Option<Au>) -> Size<Au> {
471        match self {
472            Size::Numeric(numeric) => numeric
473                .maybe_to_used_value(basis)
474                .map_or(Size::Initial, Size::Numeric),
475            Size::FitContentFunction(numeric) => {
476                // Under discussion in https://github.com/w3c/csswg-drafts/issues/11805
477                numeric
478                    .maybe_to_used_value(basis)
479                    .map_or(Size::MaxContent, Size::FitContentFunction)
480            },
481            _ => self.map(|_| unreachable!("This shouldn't be called for keywords")),
482        }
483    }
484}
485
486impl LogicalVec2<Size<LengthPercentage>> {
487    pub(crate) fn percentages_relative_to_basis(
488        &self,
489        basis: &LogicalVec2<Au>,
490    ) -> LogicalVec2<Size<Au>> {
491        LogicalVec2 {
492            inline: self.inline.map(|value| value.to_used_value(basis.inline)),
493            block: self.block.map(|value| value.to_used_value(basis.block)),
494        }
495    }
496}
497
498impl Size<Au> {
499    /// Resolves a preferred size into a numerical value.
500    /// <https://www.w3.org/TR/css-sizing-3/#preferred-size-properties>
501    #[inline]
502    pub(crate) fn resolve_for_preferred<F: FnOnce() -> ContentSizes>(
503        &self,
504        automatic_size: Size<Au>,
505        stretch_size: Option<Au>,
506        content_size: &LazyCell<ContentSizes, F>,
507    ) -> Au {
508        match self {
509            Self::Initial => {
510                assert!(!automatic_size.is_initial());
511                automatic_size.resolve_for_preferred(automatic_size, stretch_size, content_size)
512            },
513            Self::MinContent => content_size.min_content,
514            Self::MaxContent => content_size.max_content,
515            Self::FitContentFunction(size) => content_size.shrink_to_fit(*size),
516            Self::FitContent => {
517                content_size.shrink_to_fit(stretch_size.unwrap_or_else(|| content_size.max_content))
518            },
519            Self::Stretch => stretch_size.unwrap_or_else(|| content_size.max_content),
520            Self::QuirkyStretch => stretch_size
521                .unwrap_or_else(|| content_size.max_content)
522                .max(content_size.min_content),
523            Self::Numeric(numeric) => *numeric,
524        }
525    }
526
527    /// Resolves a minimum size into a numerical value.
528    /// <https://www.w3.org/TR/css-sizing-3/#min-size-properties>
529    #[inline]
530    pub(crate) fn resolve_for_min<F: FnOnce() -> ContentSizes>(
531        &self,
532        get_automatic_minimum_size: impl FnOnce() -> Au,
533        stretch_size: Option<Au>,
534        content_size: &LazyCell<ContentSizes, F>,
535        is_table: bool,
536    ) -> Au {
537        let result = match self {
538            Self::Initial => get_automatic_minimum_size(),
539            Self::MinContent => content_size.min_content,
540            Self::MaxContent => content_size.max_content,
541            Self::FitContentFunction(size) => content_size.shrink_to_fit(*size),
542            Self::FitContent => content_size.shrink_to_fit(stretch_size.unwrap_or_default()),
543            Self::Stretch => stretch_size.unwrap_or_default(),
544            Self::QuirkyStretch => unreachable!("Quirky stretch can't be a min size"),
545            Self::Numeric(numeric) => *numeric,
546        };
547        if is_table {
548            // In addition to the specified minimum, the inline size of a table is forced to be
549            // at least as big as its min-content size.
550            //
551            // Note that if there are collapsed columns, only the inline size of the table grid will
552            // shrink, while the size of the table wrapper (being computed here) won't be affected.
553            // However, collapsed rows should typically affect the block size of the table wrapper,
554            // so it might be wrong to use this function for that case.
555            // This is being discussed in https://github.com/w3c/csswg-drafts/issues/11408
556            result.max(content_size.min_content)
557        } else {
558            result
559        }
560    }
561
562    /// Resolves a maximum size into a numerical value.
563    /// <https://www.w3.org/TR/css-sizing-3/#max-size-properties>
564    #[inline]
565    pub(crate) fn resolve_for_max<F: FnOnce() -> ContentSizes>(
566        &self,
567        stretch_size: Option<Au>,
568        content_size: &LazyCell<ContentSizes, F>,
569    ) -> Option<Au> {
570        Some(match self {
571            Self::Initial => return None,
572            Self::MinContent => content_size.min_content,
573            Self::MaxContent => content_size.max_content,
574            Self::FitContentFunction(size) => content_size.shrink_to_fit(*size),
575            Self::FitContent => content_size.shrink_to_fit(stretch_size.unwrap_or(MAX_AU)),
576            Self::Stretch => return stretch_size,
577            Self::QuirkyStretch => unreachable!("Quirky stretch can't be a max size"),
578            Self::Numeric(numeric) => *numeric,
579        })
580    }
581
582    /// Tries to resolve an extrinsic size into a numerical value.
583    /// Extrinsic sizes are those based on the context of an element, without regard for its contents.
584    /// <https://drafts.csswg.org/css-sizing-3/#extrinsic>
585    ///
586    /// Returns `None` if either:
587    /// - The size is intrinsic. A quirky stretch doesn't count as intrinsic here.
588    /// - The size is the initial one.
589    ///   TODO: should we allow it to behave as `stretch` instead of assuming it's intrinsic?
590    /// - The provided `stretch_size` is `None` but we need its value.
591    #[inline]
592    pub(crate) fn maybe_resolve_extrinsic(&self, stretch_size: Option<Au>) -> Option<Au> {
593        match self {
594            Self::Initial |
595            Self::MinContent |
596            Self::MaxContent |
597            Self::FitContent |
598            Self::FitContentFunction(_) => None,
599            Self::Stretch | Self::QuirkyStretch => stretch_size,
600            Self::Numeric(numeric) => Some(*numeric),
601        }
602    }
603}
604
605/// Represents the sizing constraint that the preferred, min and max sizing properties
606/// impose on one axis.
607#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq)]
608pub(crate) enum SizeConstraint {
609    /// Represents a definite preferred size, clamped by minimum and maximum sizes (if any).
610    Definite(Au),
611    /// Represents an indefinite preferred size that allows a range of values between
612    /// the first argument (minimum size) and the second one (maximum size).
613    MinMax(Au, Option<Au>),
614}
615
616impl Default for SizeConstraint {
617    #[inline]
618    fn default() -> Self {
619        Self::MinMax(Au::default(), None)
620    }
621}
622
623impl SizeConstraint {
624    #[inline]
625    pub(crate) fn new(preferred_size: Option<Au>, min_size: Au, max_size: Option<Au>) -> Self {
626        preferred_size.map_or_else(
627            || Self::MinMax(min_size, max_size),
628            |size| Self::Definite(size.clamp_between_extremums(min_size, max_size)),
629        )
630    }
631
632    #[inline]
633    pub(crate) fn is_definite(self) -> bool {
634        matches!(self, Self::Definite(_))
635    }
636
637    #[inline]
638    pub(crate) fn to_definite(self) -> Option<Au> {
639        match self {
640            Self::Definite(size) => Some(size),
641            _ => None,
642        }
643    }
644
645    #[inline]
646    pub(crate) fn definite_or_min(self) -> Au {
647        match self {
648            Self::Definite(size) => size,
649            Self::MinMax(min, _) => min,
650        }
651    }
652
653    #[inline]
654    pub(crate) fn replace_if_indefinite_with(&mut self, replacement: Self) {
655        if !self.is_definite() {
656            *self = replacement
657        }
658    }
659}
660
661impl From<Option<Au>> for SizeConstraint {
662    fn from(size: Option<Au>) -> Self {
663        size.map(SizeConstraint::Definite).unwrap_or_default()
664    }
665}
666
667#[derive(Clone, Debug, Default)]
668pub(crate) struct Sizes {
669    /// <https://drafts.csswg.org/css-sizing-3/#preferred-size-properties>
670    pub preferred: Size<Au>,
671    /// <https://drafts.csswg.org/css-sizing-3/#min-size-properties>
672    pub min: Size<Au>,
673    /// <https://drafts.csswg.org/css-sizing-3/#max-size-properties>
674    pub max: Size<Au>,
675}
676
677impl Sizes {
678    #[inline]
679    pub(crate) fn new(preferred: Size<Au>, min: Size<Au>, max: Size<Au>) -> Self {
680        Self {
681            preferred,
682            min,
683            max,
684        }
685    }
686
687    /// Resolves the three sizes into a single numerical value.
688    #[inline]
689    pub(crate) fn resolve(
690        &self,
691        axis: LogicalAxis,
692        automatic_size: Size<Au>,
693        get_automatic_minimum_size: impl FnOnce() -> Au,
694        stretch_size: Option<Au>,
695        get_content_size: impl FnOnce() -> ContentSizes,
696        is_table: bool,
697    ) -> Au {
698        if is_table && axis == LogicalAxis::Block {
699            // The intrinsic block size of a table already takes sizing properties into account,
700            // but it can be a smaller amount if there are collapsed rows.
701            // Therefore, disregard sizing properties and just defer to the intrinsic size.
702            // This is being discussed in https://github.com/w3c/csswg-drafts/issues/11408
703            return get_content_size().max_content;
704        }
705        let (preferred, min, max) = self.resolve_each(
706            automatic_size,
707            get_automatic_minimum_size,
708            stretch_size,
709            get_content_size,
710            is_table,
711        );
712        preferred.clamp_between_extremums(min, max)
713    }
714
715    /// Resolves each of the three sizes into a numerical value, separately.
716    /// - The 1st returned value is the resolved preferred size.
717    /// - The 2nd returned value is the resolved minimum size.
718    /// - The 3rd returned value is the resolved maximum size. `None` means no maximum.
719    #[inline]
720    pub(crate) fn resolve_each(
721        &self,
722        automatic_size: Size<Au>,
723        get_automatic_minimum_size: impl FnOnce() -> Au,
724        stretch_size: Option<Au>,
725        get_content_size: impl FnOnce() -> ContentSizes,
726        is_table: bool,
727    ) -> (Au, Au, Option<Au>) {
728        // The provided `get_content_size` is a FnOnce but we may need its result multiple times.
729        // A LazyCell will only invoke it once if needed, and then reuse the result.
730        let content_size = LazyCell::new(get_content_size);
731        (
732            self.preferred
733                .resolve_for_preferred(automatic_size, stretch_size, &content_size),
734            self.min.resolve_for_min(
735                get_automatic_minimum_size,
736                stretch_size,
737                &content_size,
738                is_table,
739            ),
740            self.max.resolve_for_max(stretch_size, &content_size),
741        )
742    }
743
744    /// Tries to extrinsically resolve the three sizes into a single [`SizeConstraint`].
745    /// Values that are intrinsic or need `stretch_size` when it's `None` are handled as such:
746    /// - On the preferred size, they make the returned value be an indefinite [`SizeConstraint::MinMax`].
747    /// - On the min size, they are treated as `auto`, enforcing the automatic minimum size.
748    /// - On the max size, they are treated as `none`, enforcing no maximum.
749    #[inline]
750    pub(crate) fn resolve_extrinsic(
751        &self,
752        automatic_size: Size<Au>,
753        automatic_minimum_size: Au,
754        stretch_size: Option<Au>,
755    ) -> SizeConstraint {
756        let (preferred, min, max) =
757            self.resolve_each_extrinsic(automatic_size, automatic_minimum_size, stretch_size);
758        SizeConstraint::new(preferred, min, max)
759    }
760
761    /// Tries to extrinsically resolve each of the three sizes into a numerical value, separately.
762    /// This can't resolve values that are intrinsic or need `stretch_size` but it's `None`.
763    /// - The 1st returned value is the resolved preferred size. If it can't be resolved then
764    ///   the returned value is `None`. Note that this is different than treating it as `auto`.
765    ///   TODO: This needs to be discussed in <https://github.com/w3c/csswg-drafts/issues/11387>.
766    /// - The 2nd returned value is the resolved minimum size. If it can't be resolved then we
767    ///   treat it as the initial `auto`, returning the automatic minimum size.
768    /// - The 3rd returned value is the resolved maximum size. If it can't be resolved then we
769    ///   treat it as the initial `none`, returning `None`.
770    #[inline]
771    pub(crate) fn resolve_each_extrinsic(
772        &self,
773        automatic_size: Size<Au>,
774        automatic_minimum_size: Au,
775        stretch_size: Option<Au>,
776    ) -> (Option<Au>, Au, Option<Au>) {
777        (
778            if self.preferred.is_initial() {
779                automatic_size.maybe_resolve_extrinsic(stretch_size)
780            } else {
781                self.preferred.maybe_resolve_extrinsic(stretch_size)
782            },
783            self.min
784                .maybe_resolve_extrinsic(stretch_size)
785                .unwrap_or(automatic_minimum_size),
786            self.max.maybe_resolve_extrinsic(stretch_size),
787        )
788    }
789}
790
791struct LazySizeData<'a> {
792    sizes: &'a Sizes,
793    axis: LogicalAxis,
794    automatic_size: Size<Au>,
795    get_automatic_minimum_size: fn() -> Au,
796    stretch_size: Option<Au>,
797    is_table: bool,
798}
799
800/// Represents a size that can't be fully resolved until the intrinsic size
801/// is known. This is useful in the block axis, since the intrinsic size
802/// depends on layout, but the other inputs are known beforehand.
803pub(crate) struct LazySize<'a> {
804    result: OnceCell<Au>,
805    data: Option<LazySizeData<'a>>,
806}
807
808impl<'a> LazySize<'a> {
809    pub(crate) fn new(
810        sizes: &'a Sizes,
811        axis: LogicalAxis,
812        automatic_size: Size<Au>,
813        get_automatic_minimum_size: fn() -> Au,
814        stretch_size: Option<Au>,
815        is_table: bool,
816    ) -> Self {
817        Self {
818            result: OnceCell::new(),
819            data: Some(LazySizeData {
820                sizes,
821                axis,
822                automatic_size,
823                get_automatic_minimum_size,
824                stretch_size,
825                is_table,
826            }),
827        }
828    }
829
830    /// Creates a [`LazySize`] that will resolve to the intrinsic size.
831    /// Should be equivalent to [`LazySize::new()`] with default parameters,
832    /// but avoiding the trouble of getting a reference to a [`Sizes::default()`]
833    /// which lives long enough.
834    ///
835    /// TODO: It's not clear what this should do if/when [`LazySize::resolve()`]
836    /// is changed to accept a [`ContentSizes`] as the intrinsic size.
837    pub(crate) fn intrinsic() -> Self {
838        Self {
839            result: OnceCell::new(),
840            data: None,
841        }
842    }
843
844    /// Resolves the [`LazySize`] into [`Au`], caching the result.
845    /// The argument is a callback that computes the intrinsic size lazily.
846    ///
847    /// TODO: The intrinsic size should probably be a [`ContentSizes`] instead of [`Au`].
848    pub(crate) fn resolve(&self, get_content_size: impl FnOnce() -> Au) -> Au {
849        *self.result.get_or_init(|| {
850            let Some(ref data) = self.data else {
851                return get_content_size();
852            };
853            data.sizes.resolve(
854                data.axis,
855                data.automatic_size,
856                data.get_automatic_minimum_size,
857                data.stretch_size,
858                || get_content_size().into(),
859                data.is_table,
860            )
861        })
862    }
863}
864
865impl From<Au> for LazySize<'_> {
866    /// Creates a [`LazySize`] that will resolve to the given [`Au`],
867    /// ignoring the intrinsic size.
868    fn from(value: Au) -> Self {
869        let result = OnceCell::new();
870        result.set(value).unwrap();
871        LazySize { result, data: None }
872    }
873}