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