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