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