Skip to main content

taffy/compute/common/
sizing_keyword.rs

1//! Shared resolution logic for sizing keywords (`min-content`, `max-content`, `fit-content`,
2//! `fit-content(...)`, and `stretch`) on the `width`/`height` style properties
3use crate::geometry::{AbsoluteAxis, Line, Rect, Size};
4use crate::style::AvailableSpace;
5use crate::tree::{LayoutPartialTree, LayoutPartialTreeExt, NodeId, SizingMode};
6use crate::util::sys::f32_max;
7use crate::{CompactLength, Dimension};
8
9/// How a sizing keyword resolves to a used size
10pub(crate) enum SizingKeywordResolution {
11    /// The size is the result of measuring the item under the given available space constraint
12    Measure(AvailableSpace),
13    /// The size resolves to an exact value without measuring the item
14    Exact(f32),
15}
16
17/// Resolve an item's size style in one axis if it is a sizing keyword (`min-content`,
18/// `max-content`, `fit-content`, `fit-content(...)`, or `stretch`).
19///
20/// - `stretch_size` is the size the item would take if stretched to fill the available space
21///   (available space minus margins). Used by `fit-content` and `stretch`.
22/// - `percent_resolution_basis` is the size that percentages resolve against in this axis.
23///   Used by `fit-content(<percentage>)`.
24///
25/// Returns `None` if the size style is not a sizing keyword, or if it cannot
26/// be resolved in the current context (in which case it behaves as `auto`).
27#[inline]
28pub(crate) fn resolve_sizing_keyword(
29    style: Dimension,
30    stretch_size: Option<f32>,
31    percent_resolution_basis: Option<f32>,
32    calc_resolver: impl Fn(*const (), f32) -> f32,
33) -> Option<SizingKeywordResolution> {
34    #[cfg(not(feature = "calc"))]
35    let _ = &calc_resolver;
36    match style.tag() {
37        CompactLength::MIN_CONTENT_TAG => Some(SizingKeywordResolution::Measure(AvailableSpace::MinContent)),
38        CompactLength::MAX_CONTENT_TAG => Some(SizingKeywordResolution::Measure(AvailableSpace::MaxContent)),
39        CompactLength::FIT_CONTENT_PX_TAG => {
40            Some(SizingKeywordResolution::Measure(AvailableSpace::Definite(style.value())))
41        }
42        CompactLength::FIT_CONTENT_PERCENT_TAG => percent_resolution_basis
43            .map(|basis| SizingKeywordResolution::Measure(AvailableSpace::Definite(basis * style.value()))),
44        CompactLength::FIT_CONTENT_KEYWORD_TAG => {
45            stretch_size.map(|size| SizingKeywordResolution::Measure(AvailableSpace::Definite(size)))
46        }
47        #[cfg(feature = "calc")]
48        _ if style.is_fit_content_calc() => percent_resolution_basis.map(|basis| {
49            SizingKeywordResolution::Measure(AvailableSpace::Definite(calc_resolver(style.calc_value(), basis)))
50        }),
51        CompactLength::STRETCH_TAG => stretch_size.map(SizingKeywordResolution::Exact),
52        _ => None,
53    }
54}
55
56/// Resolve the sizing keywords (`min-content`, `max-content`, `fit-content`, `fit-content(...)`,
57/// and `stretch`) on the size styles of an absolutely positioned item, filling in the
58/// corresponding `known_dimensions` axes.
59///
60/// - `area_size` is the size of the item's containing block (which insets and percentages
61///   resolve against).
62/// - The stretch size in each axis is the containing block minus the item's insets and margins
63///   in that axis.
64#[allow(clippy::too_many_arguments)]
65pub(crate) fn resolve_absolute_sizing_keywords(
66    tree: &mut impl LayoutPartialTree,
67    node: NodeId,
68    known_dimensions: &mut Size<Option<f32>>,
69    size_style: Size<Dimension>,
70    area_size: Size<f32>,
71    inset: Rect<Option<f32>>,
72    margin: Rect<Option<f32>>,
73    sizing_mode: SizingMode,
74) {
75    let stretch_size = Size {
76        width: f32_max(
77            area_size.width
78                - inset.left.unwrap_or(0.0)
79                - inset.right.unwrap_or(0.0)
80                - margin.left.unwrap_or(0.0)
81                - margin.right.unwrap_or(0.0),
82            0.0,
83        ),
84        height: f32_max(
85            area_size.height
86                - inset.top.unwrap_or(0.0)
87                - inset.bottom.unwrap_or(0.0)
88                - margin.top.unwrap_or(0.0)
89                - margin.bottom.unwrap_or(0.0),
90            0.0,
91        ),
92    };
93
94    let keyword_width = if known_dimensions.width.is_none() {
95        resolve_sizing_keyword(size_style.width, Some(stretch_size.width), Some(area_size.width), |val, basis| {
96            tree.calc(val, basis)
97        })
98    } else {
99        None
100    };
101    let keyword_height = if known_dimensions.height.is_none() {
102        resolve_sizing_keyword(size_style.height, Some(stretch_size.height), Some(area_size.height), |val, basis| {
103            tree.calc(val, basis)
104        })
105    } else {
106        None
107    };
108
109    match (keyword_width, keyword_height) {
110        // If both axes need to be measured then resolve them with a single measure call
111        (
112            Some(SizingKeywordResolution::Measure(available_width)),
113            Some(SizingKeywordResolution::Measure(available_height)),
114        ) => {
115            let measured_size = tree.measure_child_size_both(
116                node,
117                Size::NONE,
118                area_size.map(Some),
119                Size { width: available_width, height: available_height },
120                sizing_mode,
121                Line::FALSE,
122            );
123            *known_dimensions = measured_size.map(Some);
124        }
125        (keyword_width, keyword_height) => {
126            if let Some(resolution) = keyword_width {
127                known_dimensions.width = Some(match resolution {
128                    SizingKeywordResolution::Exact(width) => width,
129                    SizingKeywordResolution::Measure(available_width) => tree.measure_child_size(
130                        node,
131                        *known_dimensions,
132                        area_size.map(Some),
133                        Size { width: available_width, height: AvailableSpace::Definite(stretch_size.height) },
134                        sizing_mode,
135                        AbsoluteAxis::Horizontal,
136                        Line::FALSE,
137                    ),
138                });
139            }
140            if let Some(resolution) = keyword_height {
141                known_dimensions.height = Some(match resolution {
142                    SizingKeywordResolution::Exact(height) => height,
143                    SizingKeywordResolution::Measure(available_height) => tree.measure_child_size(
144                        node,
145                        *known_dimensions,
146                        area_size.map(Some),
147                        Size {
148                            width: known_dimensions
149                                .width
150                                .map(AvailableSpace::Definite)
151                                .unwrap_or(AvailableSpace::Definite(stretch_size.width)),
152                            height: available_height,
153                        },
154                        sizing_mode,
155                        AbsoluteAxis::Vertical,
156                        Line::FALSE,
157                    ),
158                });
159            }
160        }
161    }
162}