Skip to main content

taffy/compute/grid/
implicit_grid.rs

1//! This module is not required for spec compliance, but is used as a performance optimisation
2//! to reduce the number of allocations required when creating a grid.
3use crate::geometry::Line;
4use crate::style::{GenericGridPlacement, GridPlacement};
5use crate::{CheapCloneStr, Direction, GridItemStyle};
6use core::cmp::{max, min};
7
8use super::types::TrackCounts;
9use super::{OriginZeroLine, MAX_OZ_LINE, MIN_OZ_LINE};
10
11/// Estimate the number of rows and columns in the grid
12/// This is used as a performance optimisation to pre-size vectors and reduce allocations. It also forms a necessary step
13/// in the auto-placement
14///   - The estimates for the explicit and negative implicit track counts are exact.
15///   - However, the estimates for the positive explicit track count is a lower bound as auto-placement can affect this
16///     in ways which are impossible to predict until the auto-placement algorithm is run.
17///
18/// Note that this function internally mixes use of grid track numbers and grid line numbers
19pub(crate) fn compute_grid_size_estimate<'a, S: GridItemStyle + 'a>(
20    explicit_col_count: u16,
21    explicit_row_count: u16,
22    direction: Direction,
23    child_styles_iter: impl Iterator<Item = S>,
24) -> (TrackCounts, TrackCounts) {
25    // Iterate over children, producing an estimate of the min and max grid lines (in origin-zero coordinates where)
26    // along with the span of each item
27    let (col_min, col_max, col_max_span, row_min, row_max, row_max_span) =
28        get_known_child_positions(child_styles_iter, explicit_col_count, explicit_row_count, direction);
29
30    // Compute *track* count estimates for each axis from:
31    //   - The explicit track counts
32    //   - The origin-zero coordinate min and max grid line variables
33    let negative_implicit_inline_tracks = col_min.implied_negative_implicit_tracks();
34    let explicit_inline_tracks = explicit_col_count;
35    let mut positive_implicit_inline_tracks = col_max.implied_positive_implicit_tracks(explicit_col_count);
36    let negative_implicit_block_tracks = row_min.implied_negative_implicit_tracks();
37    let explicit_block_tracks = explicit_row_count;
38    let mut positive_implicit_block_tracks = row_max.implied_positive_implicit_tracks(explicit_row_count);
39
40    // In each axis, adjust positive track estimate if any items have a span that does not fit within
41    // the total number of tracks in the estimate
42    let tot_inline_tracks = negative_implicit_inline_tracks + explicit_inline_tracks + positive_implicit_inline_tracks;
43    if tot_inline_tracks < col_max_span {
44        positive_implicit_inline_tracks = col_max_span - explicit_inline_tracks - negative_implicit_inline_tracks;
45    }
46
47    let tot_block_tracks = negative_implicit_block_tracks + explicit_block_tracks + positive_implicit_block_tracks;
48    if tot_block_tracks < row_max_span {
49        positive_implicit_block_tracks = row_max_span - explicit_block_tracks - negative_implicit_block_tracks;
50    }
51
52    let column_counts =
53        TrackCounts::from_raw(negative_implicit_inline_tracks, explicit_inline_tracks, positive_implicit_inline_tracks);
54
55    let row_counts =
56        TrackCounts::from_raw(negative_implicit_block_tracks, explicit_block_tracks, positive_implicit_block_tracks);
57
58    (column_counts, row_counts)
59}
60
61/// Iterate over children, producing an estimate of the min and max grid *lines* along with the span of each item
62///
63/// Min and max grid lines are returned in origin-zero coordinates)
64/// The span is measured in tracks spanned
65fn get_known_child_positions<'a, S: GridItemStyle + 'a>(
66    children_iter: impl Iterator<Item = S>,
67    explicit_col_count: u16,
68    explicit_row_count: u16,
69    direction: Direction,
70) -> (OriginZeroLine, OriginZeroLine, u16, OriginZeroLine, OriginZeroLine, u16) {
71    let (mut col_min, mut col_max, mut col_max_span) = (OriginZeroLine(0), OriginZeroLine(0), 0);
72    let (mut row_min, mut row_max, mut row_max_span) = (OriginZeroLine(0), OriginZeroLine(0), 0);
73    children_iter.for_each(|child_style| {
74        let col_line = child_style.grid_column();
75        let row_line = child_style.grid_row();
76
77        // Note: that the children reference the lines in between (and around) the tracks not tracks themselves,
78        // and thus we must subtract 1 to get an accurate estimate of the number of tracks
79        let (mut child_col_min, mut child_col_max, child_col_span) =
80            child_min_line_max_line_span::<S::CustomIdent>(col_line, explicit_col_count);
81        let (child_row_min, child_row_max, child_row_span) =
82            child_min_line_max_line_span::<S::CustomIdent>(row_line, explicit_row_count);
83
84        // Placement mirrors horizontal spans in RTL, so mirror known column line bounds here
85        // to keep implicit-grid pre-sizing consistent with actual placement.
86        if direction.is_rtl() && (child_col_min != OriginZeroLine(0) || child_col_max != OriginZeroLine(0)) {
87            let explicit_col_end_line = explicit_col_count as i16;
88            let mirrored_min = OriginZeroLine(explicit_col_end_line - child_col_max.0);
89            let mirrored_max = OriginZeroLine(explicit_col_end_line - child_col_min.0);
90            child_col_min = mirrored_min;
91            child_col_max = mirrored_max;
92        }
93
94        col_min = min(col_min, child_col_min);
95        col_max = max(col_max, child_col_max);
96        col_max_span = max(col_max_span, child_col_span);
97        row_min = min(row_min, child_row_min);
98        row_max = max(row_max, child_row_max);
99        row_max_span = max(row_max_span, child_row_span);
100    });
101
102    (col_min, col_max, col_max_span, row_min, row_max, row_max_span)
103}
104
105/// Helper function for `compute_grid_size_estimate`
106/// Produces a conservative estimate of the greatest and smallest grid lines used by a single grid item
107///
108/// Values are returned in origin-zero coordinates
109#[inline]
110fn child_min_line_max_line_span<S: CheapCloneStr>(
111    line: Line<GridPlacement<S>>,
112    explicit_track_count: u16,
113) -> (OriginZeroLine, OriginZeroLine, u16) {
114    use GenericGridPlacement::*;
115
116    // 8.3.1. Grid Placement Conflict Handling
117    // A. If the placement for a grid item contains two lines, and the start line is further end-ward than the end line, swap the two lines.
118    // B. If the start line is equal to the end line, remove the end line.
119    // C. If the placement contains two spans, remove the one contributed by the end grid-placement property.
120    // D. If the placement contains only a span for a named line, replace it with a span of 1.
121
122    // Convert line into origin-zero coordinates before attempting to analyze
123    // We ignore named lines here as they are accounted for separately
124    let oz_line = line.into_origin_zero_ignoring_named(explicit_track_count);
125
126    let min = match (oz_line.start, oz_line.end) {
127        // Both tracks specified
128        (Line(track1), Line(track2)) => {
129            // See rules A and B above
130            if track1 == track2 {
131                track1
132            } else {
133                min(track1, track2)
134            }
135        }
136
137        // Start track specified
138        (Line(track), Auto) => track,
139        (Line(track), Span(_)) => track,
140
141        // End track specified
142        (Auto, Line(track)) => track,
143        (Span(span), Line(track)) => track - span,
144
145        // Only spans or autos
146        // We ignore spans here by returning 0 which never effect the estimate as these are accounted for separately
147        (Auto | Span(_), Auto | Span(_)) => OriginZeroLine(0),
148    };
149
150    let max = match (oz_line.start, oz_line.end) {
151        // Both tracks specified
152        (Line(track1), Line(track2)) => {
153            // See rules A and B above
154            if track1 == track2 {
155                track1 + 1
156            } else {
157                max(track1, track2)
158            }
159        }
160
161        // Start track specified
162        (Line(track), Auto) => track + 1,
163        (Line(track), Span(span)) => track + span,
164
165        // End track specified
166        (Auto, Line(track)) => track,
167        (Span(_), Line(track)) => track,
168
169        // Only spans or autos
170        // We ignore spans here by returning 0 which never effect the estimate as these are accounted for separately
171        (Auto | Span(_), Auto | Span(_)) => OriginZeroLine(0),
172    };
173
174    // Calculate span only for indefinitely placed items as we don't need for other items (whose required space will
175    // be taken into account by min and max)
176    let span = match (oz_line.start, oz_line.end) {
177        (Auto | Span(_), Auto | Span(_)) => oz_line.indefinite_span(),
178        _ => 1,
179    };
180
181    // Clamp the min and max lines into the limited grid so that the estimated implicit track counts
182    // stay within the maximum track limit (https://www.w3.org/TR/css-grid-1/#overlarge-grids).
183    // This matches the clamping of the actual item placements performed during placement.
184    let clamped_min = OriginZeroLine(min.0.max(MIN_OZ_LINE));
185    let clamped_max = OriginZeroLine(max.0.min(MAX_OZ_LINE));
186
187    (clamped_min, clamped_max, span)
188}
189
190#[allow(clippy::bool_assert_comparison)]
191#[cfg(test)]
192mod tests {
193    mod test_child_min_max_line {
194        type S = String;
195        use super::super::child_min_line_max_line_span;
196        use super::super::OriginZeroLine;
197        use crate::geometry::Line;
198        use crate::style_helpers::*;
199
200        #[test]
201        fn child_min_max_line_auto() {
202            let (min_col, max_col, span) = child_min_line_max_line_span::<S>(Line { start: line(5), end: span(6) }, 6);
203            assert_eq!(min_col, OriginZeroLine(4));
204            assert_eq!(max_col, OriginZeroLine(10));
205            assert_eq!(span, 1);
206        }
207
208        #[test]
209        fn child_min_max_line_negative_track() {
210            let (min_col, max_col, span) = child_min_line_max_line_span::<S>(Line { start: line(-5), end: span(3) }, 6);
211            assert_eq!(min_col, OriginZeroLine(2));
212            assert_eq!(max_col, OriginZeroLine(5));
213            assert_eq!(span, 1);
214        }
215    }
216
217    mod test_initial_grid_sizing {
218        use super::super::compute_grid_size_estimate;
219        use crate::compute::grid::util::test_helpers::*;
220        use crate::style_helpers::*;
221        use crate::Direction;
222
223        #[test]
224        fn explicit_grid_sizing_with_children() {
225            let explicit_col_count = 6;
226            let explicit_row_count = 8;
227            let child_styles = [
228                (line(1), span(2), line(2), auto()).into_grid_child(),
229                (line(-4), auto(), line(-2), auto()).into_grid_child(),
230            ];
231            let (inline, block) =
232                compute_grid_size_estimate(explicit_col_count, explicit_row_count, Direction::Ltr, child_styles.iter());
233            assert_eq!(inline.negative_implicit, 0);
234            assert_eq!(inline.explicit, explicit_col_count);
235            assert_eq!(inline.positive_implicit, 0);
236            assert_eq!(block.negative_implicit, 0);
237            assert_eq!(block.explicit, explicit_row_count);
238            assert_eq!(block.positive_implicit, 0);
239        }
240
241        #[test]
242        fn negative_implicit_grid_sizing() {
243            let explicit_col_count = 4;
244            let explicit_row_count = 4;
245            let child_styles = [
246                (line(-6), span(2), line(-8), auto()).into_grid_child(),
247                (line(4), auto(), line(3), auto()).into_grid_child(),
248            ];
249            let (inline, block) =
250                compute_grid_size_estimate(explicit_col_count, explicit_row_count, Direction::Ltr, child_styles.iter());
251            assert_eq!(inline.negative_implicit, 1);
252            assert_eq!(inline.explicit, explicit_col_count);
253            assert_eq!(inline.positive_implicit, 0);
254            assert_eq!(block.negative_implicit, 3);
255            assert_eq!(block.explicit, explicit_row_count);
256            assert_eq!(block.positive_implicit, 0);
257        }
258    }
259}