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