taffy/compute/grid/types/
grid_item.rs

1//! Contains GridItem used to represent a single grid item during layout
2use super::GridTrack;
3use crate::compute::grid::OriginZeroLine;
4use crate::geometry::AbstractAxis;
5use crate::geometry::{Line, Point, Rect, Size};
6use crate::style::{
7    AlignItems, AlignSelf, AvailableSpace, Dimension, LengthPercentageAuto, MaxTrackSizingFunction,
8    MinTrackSizingFunction, Overflow, Style,
9};
10use crate::tree::{NodeId, PartialLayoutTree, PartialLayoutTreeExt, SizingMode};
11use crate::util::{MaybeMath, MaybeResolve, ResolveOrZero};
12use core::ops::Range;
13
14/// Represents a single grid item
15#[derive(Debug)]
16pub(in super::super) struct GridItem {
17    /// The id of the node that this item represents
18    pub node: NodeId,
19
20    /// The order of the item in the children array
21    ///
22    /// We sort the list of grid items during track sizing. This field allows us to sort back the original order
23    /// for final positioning
24    pub source_order: u16,
25
26    /// The item's definite row-start and row-end, as resolved by the placement algorithm
27    /// (in origin-zero coordinates)
28    pub row: Line<OriginZeroLine>,
29    /// The items definite column-start and column-end, as resolved by the placement algorithm
30    /// (in origin-zero coordinates)
31    pub column: Line<OriginZeroLine>,
32
33    /// The item's overflow style
34    pub overflow: Point<Overflow>,
35    /// The item's size style
36    pub size: Size<Dimension>,
37    /// The item's min_size style
38    pub min_size: Size<Dimension>,
39    /// The item's max_size style
40    pub max_size: Size<Dimension>,
41    /// The item's aspect_ratio style
42    pub aspect_ratio: Option<f32>,
43    /// The item's margin style
44    pub margin: Rect<LengthPercentageAuto>,
45    /// The item's align_self property, or the parent's align_items property is not set
46    pub align_self: AlignSelf,
47    /// The item's justify_self property, or the parent's justify_items property is not set
48    pub justify_self: AlignSelf,
49    /// The items first baseline (horizontal)
50    pub baseline: Option<f32>,
51    /// Shim for baseline alignment that acts like an extra top margin
52    /// TODO: Support last baseline and vertical text baselines
53    pub baseline_shim: f32,
54
55    /// The item's definite row-start and row-end (same as `row` field, except in a different coordinate system)
56    /// (as indexes into the Vec<GridTrack> stored in a grid's AbstractAxisTracks)
57    pub row_indexes: Line<u16>,
58    /// The items definite column-start and column-end (same as `column` field, except in a different coordinate system)
59    /// (as indexes into the Vec<GridTrack> stored in a grid's AbstractAxisTracks)
60    pub column_indexes: Line<u16>,
61
62    /// Whether the item crosses a flexible row
63    pub crosses_flexible_row: bool,
64    /// Whether the item crosses a flexible column
65    pub crosses_flexible_column: bool,
66    /// Whether the item crosses a intrinsic row
67    pub crosses_intrinsic_row: bool,
68    /// Whether the item crosses a intrinsic column
69    pub crosses_intrinsic_column: bool,
70
71    // Caches for intrinsic size computation. These caches are only valid for a single run of the track-sizing algorithm.
72    /// Cache for the known_dimensions input to intrinsic sizing computation
73    pub available_space_cache: Option<Size<Option<f32>>>,
74    /// Cache for the min-content size
75    pub min_content_contribution_cache: Size<Option<f32>>,
76    /// Cache for the minimum contribution
77    pub minimum_contribution_cache: Size<Option<f32>>,
78    /// Cache for the max-content size
79    pub max_content_contribution_cache: Size<Option<f32>>,
80
81    /// Final y position. Used to compute baseline alignment for the container.
82    pub y_position: f32,
83    /// Final height. Used to compute baseline alignment for the container.
84    pub height: f32,
85}
86
87impl GridItem {
88    /// Create a new item given a concrete placement in both axes
89    pub fn new_with_placement_style_and_order(
90        node: NodeId,
91        col_span: Line<OriginZeroLine>,
92        row_span: Line<OriginZeroLine>,
93        style: &Style,
94        parent_align_items: AlignItems,
95        parent_justify_items: AlignItems,
96        source_order: u16,
97    ) -> Self {
98        GridItem {
99            node,
100            source_order,
101            row: row_span,
102            column: col_span,
103            overflow: style.overflow,
104            size: style.size,
105            min_size: style.min_size,
106            max_size: style.max_size,
107            aspect_ratio: style.aspect_ratio,
108            margin: style.margin,
109            align_self: style.align_self.unwrap_or(parent_align_items),
110            justify_self: style.justify_self.unwrap_or(parent_justify_items),
111            baseline: None,
112            baseline_shim: 0.0,
113            row_indexes: Line { start: 0, end: 0 }, // Properly initialised later
114            column_indexes: Line { start: 0, end: 0 }, // Properly initialised later
115            crosses_flexible_row: false,            // Properly initialised later
116            crosses_flexible_column: false,         // Properly initialised later
117            crosses_intrinsic_row: false,           // Properly initialised later
118            crosses_intrinsic_column: false,        // Properly initialised later
119            available_space_cache: None,
120            min_content_contribution_cache: Size::NONE,
121            max_content_contribution_cache: Size::NONE,
122            minimum_contribution_cache: Size::NONE,
123            y_position: 0.0,
124            height: 0.0,
125        }
126    }
127
128    /// This item's placement in the specified axis in OriginZero coordinates
129    pub fn placement(&self, axis: AbstractAxis) -> Line<OriginZeroLine> {
130        match axis {
131            AbstractAxis::Block => self.row,
132            AbstractAxis::Inline => self.column,
133        }
134    }
135
136    /// This item's placement in the specified axis as GridTrackVec indices
137    pub fn placement_indexes(&self, axis: AbstractAxis) -> Line<u16> {
138        match axis {
139            AbstractAxis::Block => self.row_indexes,
140            AbstractAxis::Inline => self.column_indexes,
141        }
142    }
143
144    /// Returns a range which can be used as an index into the GridTrackVec in the specified axis
145    /// which will produce a sub-slice of covering all the tracks and lines that this item spans
146    /// excluding the lines that bound it.
147    pub fn track_range_excluding_lines(&self, axis: AbstractAxis) -> Range<usize> {
148        let indexes = self.placement_indexes(axis);
149        (indexes.start as usize + 1)..(indexes.end as usize)
150    }
151
152    /// Returns the number of tracks that this item spans in the specified axis
153    pub fn span(&self, axis: AbstractAxis) -> u16 {
154        match axis {
155            AbstractAxis::Block => self.row.span(),
156            AbstractAxis::Inline => self.column.span(),
157        }
158    }
159
160    /// Returns the pre-computed value indicating whether the grid item crosses a flexible track in
161    /// the specified axis
162    pub fn crosses_flexible_track(&self, axis: AbstractAxis) -> bool {
163        match axis {
164            AbstractAxis::Inline => self.crosses_flexible_column,
165            AbstractAxis::Block => self.crosses_flexible_row,
166        }
167    }
168
169    /// Returns the pre-computed value indicating whether the grid item crosses an intrinsic track in
170    /// the specified axis
171    pub fn crosses_intrinsic_track(&self, axis: AbstractAxis) -> bool {
172        match axis {
173            AbstractAxis::Inline => self.crosses_intrinsic_column,
174            AbstractAxis::Block => self.crosses_intrinsic_row,
175        }
176    }
177
178    /// For an item spanning multiple tracks, the upper limit used to calculate its limited min-/max-content contribution is the
179    /// sum of the fixed max track sizing functions of any tracks it spans, and is applied if it only spans such tracks.
180    pub fn spanned_track_limit(
181        &mut self,
182        axis: AbstractAxis,
183        axis_tracks: &[GridTrack],
184        axis_parent_size: Option<f32>,
185    ) -> Option<f32> {
186        let spanned_tracks = &axis_tracks[self.track_range_excluding_lines(axis)];
187        let tracks_all_fixed = spanned_tracks
188            .iter()
189            .all(|track| track.max_track_sizing_function.definite_limit(axis_parent_size).is_some());
190        if tracks_all_fixed {
191            let limit: f32 = spanned_tracks
192                .iter()
193                .map(|track| track.max_track_sizing_function.definite_limit(axis_parent_size).unwrap())
194                .sum();
195            Some(limit)
196        } else {
197            None
198        }
199    }
200
201    /// Similar to the spanned_track_limit, but excludes FitContent arguments from the limit.
202    /// Used to clamp the automatic minimum contributions of an item
203    pub fn spanned_fixed_track_limit(
204        &mut self,
205        axis: AbstractAxis,
206        axis_tracks: &[GridTrack],
207        axis_parent_size: Option<f32>,
208    ) -> Option<f32> {
209        let spanned_tracks = &axis_tracks[self.track_range_excluding_lines(axis)];
210        let tracks_all_fixed = spanned_tracks
211            .iter()
212            .all(|track| track.max_track_sizing_function.definite_value(axis_parent_size).is_some());
213        if tracks_all_fixed {
214            let limit: f32 = spanned_tracks
215                .iter()
216                .map(|track| track.max_track_sizing_function.definite_value(axis_parent_size).unwrap())
217                .sum();
218            Some(limit)
219        } else {
220            None
221        }
222    }
223
224    /// Compute the known_dimensions to be passed to the child sizing functions
225    /// The key thing that is being done here is applying stretch alignment, which is necessary to
226    /// allow percentage sizes further down the tree to resolve properly in some cases
227    fn known_dimensions(
228        &self,
229        inner_node_size: Size<Option<f32>>,
230        grid_area_size: Size<Option<f32>>,
231    ) -> Size<Option<f32>> {
232        let margins = self.margins_axis_sums_with_baseline_shims(inner_node_size.width);
233
234        let aspect_ratio = self.aspect_ratio;
235        let inherent_size = self.size.maybe_resolve(grid_area_size).maybe_apply_aspect_ratio(aspect_ratio);
236        let min_size = self.min_size.maybe_resolve(grid_area_size).maybe_apply_aspect_ratio(aspect_ratio);
237        let max_size = self.max_size.maybe_resolve(grid_area_size).maybe_apply_aspect_ratio(aspect_ratio);
238
239        let grid_area_minus_item_margins_size = grid_area_size.maybe_sub(margins);
240
241        // If node is absolutely positioned and width is not set explicitly, then deduce it
242        // from left, right and container_content_box if both are set.
243        let width = inherent_size.width.or_else(|| {
244            // Apply width based on stretch alignment if:
245            //  - Alignment style is "stretch"
246            //  - The node is not absolutely positioned
247            //  - The node does not have auto margins in this axis.
248            if self.margin.left != LengthPercentageAuto::Auto
249                && self.margin.right != LengthPercentageAuto::Auto
250                && self.justify_self == AlignSelf::Stretch
251            {
252                return grid_area_minus_item_margins_size.width;
253            }
254
255            None
256        });
257        // Reapply aspect ratio after stretch and absolute position width adjustments
258        let Size { width, height } =
259            Size { width, height: inherent_size.height }.maybe_apply_aspect_ratio(aspect_ratio);
260
261        let height = height.or_else(|| {
262            // Apply height based on stretch alignment if:
263            //  - Alignment style is "stretch"
264            //  - The node is not absolutely positioned
265            //  - The node does not have auto margins in this axis.
266            if self.margin.top != LengthPercentageAuto::Auto
267                && self.margin.bottom != LengthPercentageAuto::Auto
268                && self.align_self == AlignSelf::Stretch
269            {
270                return grid_area_minus_item_margins_size.height;
271            }
272
273            None
274        });
275        // Reapply aspect ratio after stretch and absolute position height adjustments
276        let Size { width, height } = Size { width, height }.maybe_apply_aspect_ratio(aspect_ratio);
277
278        // Clamp size by min and max width/height
279        let Size { width, height } = Size { width, height }.maybe_clamp(min_size, max_size);
280
281        Size { width, height }
282    }
283
284    /// Compute the available_space to be passed to the child sizing functions
285    /// These are estimates based on either the max track sizing function or the provisional base size in the opposite
286    /// axis to the one currently being sized.
287    /// https://www.w3.org/TR/css-grid-1/#algo-overview
288    pub fn available_space(
289        &self,
290        axis: AbstractAxis,
291        other_axis_tracks: &[GridTrack],
292        other_axis_available_space: Option<f32>,
293        get_track_size_estimate: impl Fn(&GridTrack, Option<f32>) -> Option<f32>,
294    ) -> Size<Option<f32>> {
295        let item_other_axis_size: Option<f32> = {
296            other_axis_tracks[self.track_range_excluding_lines(axis.other())]
297                .iter()
298                .map(|track| {
299                    get_track_size_estimate(track, other_axis_available_space)
300                        .map(|size| size + track.content_alignment_adjustment)
301                })
302                .sum::<Option<f32>>()
303        };
304
305        let mut size = Size::NONE;
306        size.set(axis.other(), item_other_axis_size);
307        size
308    }
309
310    /// Retrieve the available_space from the cache or compute them using the passed parameters
311    pub fn available_space_cached(
312        &mut self,
313        axis: AbstractAxis,
314        other_axis_tracks: &[GridTrack],
315        other_axis_available_space: Option<f32>,
316        get_track_size_estimate: impl Fn(&GridTrack, Option<f32>) -> Option<f32>,
317    ) -> Size<Option<f32>> {
318        self.available_space_cache.unwrap_or_else(|| {
319            let available_spaces =
320                self.available_space(axis, other_axis_tracks, other_axis_available_space, get_track_size_estimate);
321            self.available_space_cache = Some(available_spaces);
322            available_spaces
323        })
324    }
325
326    /// Compute the item's resolved margins for size contributions. Horizontal percentage margins always resolve
327    /// to zero if the container size is indefinite as otherwise this would introduce a cyclic dependency.
328    #[inline(always)]
329    pub fn margins_axis_sums_with_baseline_shims(&self, inner_node_width: Option<f32>) -> Size<f32> {
330        Rect {
331            left: self.margin.left.resolve_or_zero(Some(0.0)),
332            right: self.margin.right.resolve_or_zero(Some(0.0)),
333            top: self.margin.top.resolve_or_zero(inner_node_width) + self.baseline_shim,
334            bottom: self.margin.bottom.resolve_or_zero(inner_node_width),
335        }
336        .sum_axes()
337    }
338
339    /// Compute the item's min content contribution from the provided parameters
340    pub fn min_content_contribution(
341        &self,
342        axis: AbstractAxis,
343        tree: &mut impl PartialLayoutTree,
344        available_space: Size<Option<f32>>,
345        inner_node_size: Size<Option<f32>>,
346    ) -> f32 {
347        let known_dimensions = self.known_dimensions(inner_node_size, available_space);
348        tree.measure_child_size(
349            self.node,
350            known_dimensions,
351            available_space,
352            available_space.map(|opt| match opt {
353                Some(size) => AvailableSpace::Definite(size),
354                None => AvailableSpace::MinContent,
355            }),
356            SizingMode::InherentSize,
357            axis.as_abs_naive(),
358            Line::FALSE,
359        )
360    }
361
362    /// Retrieve the item's min content contribution from the cache or compute it using the provided parameters
363    #[inline(always)]
364    pub fn min_content_contribution_cached(
365        &mut self,
366        axis: AbstractAxis,
367        tree: &mut impl PartialLayoutTree,
368        available_space: Size<Option<f32>>,
369        inner_node_size: Size<Option<f32>>,
370    ) -> f32 {
371        self.min_content_contribution_cache.get(axis).unwrap_or_else(|| {
372            let size = self.min_content_contribution(axis, tree, available_space, inner_node_size);
373            self.min_content_contribution_cache.set(axis, Some(size));
374            size
375        })
376    }
377
378    /// Compute the item's max content contribution from the provided parameters
379    pub fn max_content_contribution(
380        &self,
381        axis: AbstractAxis,
382        tree: &mut impl PartialLayoutTree,
383        available_space: Size<Option<f32>>,
384        inner_node_size: Size<Option<f32>>,
385    ) -> f32 {
386        let known_dimensions = self.known_dimensions(inner_node_size, available_space);
387        tree.measure_child_size(
388            self.node,
389            known_dimensions,
390            available_space,
391            available_space.map(|opt| match opt {
392                Some(size) => AvailableSpace::Definite(size),
393                None => AvailableSpace::MaxContent,
394            }),
395            SizingMode::InherentSize,
396            axis.as_abs_naive(),
397            Line::FALSE,
398        )
399    }
400
401    /// Retrieve the item's max content contribution from the cache or compute it using the provided parameters
402    #[inline(always)]
403    pub fn max_content_contribution_cached(
404        &mut self,
405        axis: AbstractAxis,
406        tree: &mut impl PartialLayoutTree,
407        available_space: Size<Option<f32>>,
408        inner_node_size: Size<Option<f32>>,
409    ) -> f32 {
410        self.max_content_contribution_cache.get(axis).unwrap_or_else(|| {
411            let size = self.max_content_contribution(axis, tree, available_space, inner_node_size);
412            self.max_content_contribution_cache.set(axis, Some(size));
413            size
414        })
415    }
416
417    /// The minimum contribution of an item is the smallest outer size it can have.
418    /// Specifically:
419    ///   - If the item’s computed preferred size behaves as auto or depends on the size of its containing block in the relevant axis:
420    ///     Its minimum contribution is the outer size that would result from assuming the item’s used minimum size as its preferred size;
421    ///   - Else the item’s minimum contribution is its min-content contribution.
422    /// Because the minimum contribution often depends on the size of the item’s content, it is considered a type of intrinsic size contribution.
423    /// See: https://www.w3.org/TR/css-grid-1/#min-size-auto
424    pub fn minimum_contribution(
425        &mut self,
426        tree: &mut impl PartialLayoutTree,
427        axis: AbstractAxis,
428        axis_tracks: &[GridTrack],
429        known_dimensions: Size<Option<f32>>,
430        inner_node_size: Size<Option<f32>>,
431    ) -> f32 {
432        let size = self
433            .size
434            .maybe_resolve(inner_node_size)
435            .maybe_apply_aspect_ratio(self.aspect_ratio)
436            .get(axis)
437            .or_else(|| {
438                self.min_size.maybe_resolve(inner_node_size).maybe_apply_aspect_ratio(self.aspect_ratio).get(axis)
439            })
440            .or_else(|| self.overflow.get(axis).maybe_into_automatic_min_size())
441            .unwrap_or_else(|| {
442                // Automatic minimum size. See https://www.w3.org/TR/css-grid-1/#min-size-auto
443
444                // To provide a more reasonable default minimum size for grid items, the used value of its automatic minimum size
445                // in a given axis is the content-based minimum size if all of the following are true:
446                let item_axis_tracks = &axis_tracks[self.track_range_excluding_lines(axis)];
447
448                // it is not a scroll container
449                // TODO: support overflow propety
450
451                // it spans at least one track in that axis whose min track sizing function is auto
452                let spans_auto_min_track = axis_tracks
453                    .iter()
454                    // TODO: should this be 'behaves as auto' rather than just literal auto?
455                    .any(|track| track.min_track_sizing_function == MinTrackSizingFunction::Auto);
456
457                // if it spans more than one track in that axis, none of those tracks are flexible
458                let only_span_one_track = item_axis_tracks.len() == 1;
459                let spans_a_flexible_track = axis_tracks
460                    .iter()
461                    .any(|track| matches!(track.max_track_sizing_function, MaxTrackSizingFunction::Fraction(_)));
462
463                let use_content_based_minimum =
464                    spans_auto_min_track && (only_span_one_track || !spans_a_flexible_track);
465
466                // Otherwise, the automatic minimum size is zero, as usual.
467                if use_content_based_minimum {
468                    self.min_content_contribution_cached(axis, tree, known_dimensions, inner_node_size)
469                } else {
470                    0.0
471                }
472            });
473
474        // In all cases, the size suggestion is additionally clamped by the maximum size in the affected axis, if it’s definite.
475        // Note: The argument to fit-content() does not clamp the content-based minimum size in the same way as a fixed max track
476        // sizing function.
477        let limit = self.spanned_fixed_track_limit(axis, axis_tracks, inner_node_size.get(axis));
478        size.maybe_min(limit)
479    }
480
481    /// Retrieve the item's minimum contribution from the cache or compute it using the provided parameters
482    #[inline(always)]
483    pub fn minimum_contribution_cached(
484        &mut self,
485        tree: &mut impl PartialLayoutTree,
486        axis: AbstractAxis,
487        axis_tracks: &[GridTrack],
488        known_dimensions: Size<Option<f32>>,
489        inner_node_size: Size<Option<f32>>,
490    ) -> f32 {
491        self.minimum_contribution_cache.get(axis).unwrap_or_else(|| {
492            let size = self.minimum_contribution(tree, axis, axis_tracks, known_dimensions, inner_node_size);
493            self.minimum_contribution_cache.set(axis, Some(size));
494            size
495        })
496    }
497}