Skip to main content

taffy/compute/grid/
placement.rs

1//! Implements placing items in the grid and resolving the implicit grid.
2//! <https://www.w3.org/TR/css-grid-1/#placement>
3use super::types::{CellOccupancyMatrix, CellOccupancyState, GridItem};
4use super::{OriginZeroLine, MAX_OZ_LINE, MIN_OZ_LINE};
5use crate::geometry::Line;
6use crate::geometry::{AbsoluteAxis, InBothAbsAxis};
7use crate::style::{GridAutoFlow, OriginZeroGridPlacement};
8
9#[inline]
10/// Advances the cursor by one track.
11fn advance_position(position: OriginZeroLine) -> OriginZeroLine {
12    OriginZeroLine(position.0.saturating_add(1))
13}
14
15#[inline]
16/// Resolves an indefinite span starting at `position`.
17fn resolve_indefinite_grid_span(position: OriginZeroLine, span: u16) -> Line<OriginZeroLine> {
18    let position = position.0 as i32;
19    let span = span as i32;
20    let line = |value: i32| OriginZeroLine(value.clamp(i16::MIN as i32, i16::MAX as i32) as i16);
21    Line { start: line(position), end: line(position + span) }
22}
23
24/// A grid item's `grid-row`/`grid-column` styles with named lines resolved, in origin-zero coordinates
25pub(super) type ItemPlacement = InBothAbsAxis<Line<OriginZeroGridPlacement>>;
26
27/// 8.5. Grid Item Placement Algorithm
28/// Place items into the grid, generating new rows/column into the implicit grid as required
29///
30/// `items` and `placements` are parallel slices in document order: `placements[i]` holds the `grid-row`/`grid-column`
31/// styles of `items[i]`, with named lines already resolved. The placement passes run over these (small) values rather
32/// than re-reading the children's styles.
33///
34/// [Specification](https://www.w3.org/TR/css-grid-2/#auto-placement-algo)
35pub(super) fn place_grid_items(
36    cell_occupancy_matrix: &mut CellOccupancyMatrix,
37    items: &mut [GridItem],
38    placements: &[ItemPlacement],
39    grid_auto_flow: GridAutoFlow,
40) {
41    debug_assert_eq!(items.len(), placements.len());
42    let primary_axis = grid_auto_flow.primary_axis();
43    let secondary_axis = primary_axis.other_axis();
44
45    // 1. Place children with definite positions
46    for (item, &placement) in items.iter_mut().zip(placements.iter()) {
47        if !(placement.horizontal.is_definite() && placement.vertical.is_definite()) {
48            continue;
49        }
50        #[cfg(test)]
51        println!("Definite Item {}\n==============", item.source_order);
52
53        let (row_span, col_span) = place_definite_grid_item(placement, primary_axis);
54        record_grid_placement(
55            cell_occupancy_matrix,
56            item,
57            primary_axis,
58            row_span,
59            col_span,
60            CellOccupancyState::DefinitelyPlaced,
61        );
62    }
63
64    // 2. Place remaining children with definite secondary axis positions
65    for (item, &placement) in items.iter_mut().zip(placements.iter()) {
66        if !(placement.get(secondary_axis).is_definite() && !placement.get(primary_axis).is_definite()) {
67            continue;
68        }
69        #[cfg(test)]
70        println!("Definite Secondary Item {}\n==============", item.source_order);
71
72        let (primary_span, secondary_span) =
73            place_definite_secondary_axis_item(&*cell_occupancy_matrix, placement, grid_auto_flow);
74
75        record_grid_placement(
76            cell_occupancy_matrix,
77            item,
78            primary_axis,
79            primary_span,
80            secondary_span,
81            CellOccupancyState::AutoPlaced,
82        );
83    }
84
85    // 3. Determine the number of columns in the implicit grid
86    // By the time we get to this point in the execution, this is actually already accounted for:
87    //
88    // 3.1 Start with the columns from the explicit grid
89    //        => Handled by grid size estimate which is used to pre-size the GridOccupancyMatrix
90    //
91    // 3.2 Among all the items with a definite column position (explicitly positioned items, items positioned in the previous step,
92    //     and items not yet positioned but with a definite column) add columns to the beginning and end of the implicit grid as necessary
93    //     to accommodate those items.
94    //        => Handled by expand_to_fit_range which expands the GridOccupancyMatrix as necessary
95    //            -> Called by mark_area_as
96    //            -> Called by record_grid_placement
97    //
98    // 3.3 If the largest column span among all the items without a definite column position is larger than the width of
99    //     the implicit grid, add columns to the end of the implicit grid to accommodate that column span.
100    //        => Handled by grid size estimate which is used to pre-size the GridOccupancyMatrix
101
102    // 4. Position the remaining grid items
103    // (which either have definite position only in the secondary axis or indefinite positions in both axis)
104    let primary_axis = grid_auto_flow.primary_axis();
105    let secondary_axis = primary_axis.other_axis();
106    let primary_axis_grid_start_line = cell_occupancy_matrix.track_counts(primary_axis).implicit_start_line();
107    let secondary_axis_grid_start_line = cell_occupancy_matrix.track_counts(secondary_axis).implicit_start_line();
108    let grid_start_position = (primary_axis_grid_start_line, secondary_axis_grid_start_line);
109    let mut grid_position = grid_start_position;
110    for (item, &placement) in items.iter_mut().zip(placements.iter()) {
111        if placement.get(secondary_axis).is_definite() {
112            continue;
113        }
114        #[cfg(test)]
115        println!("\nAuto Item {}\n==============", item.source_order);
116
117        // Compute placement
118        let (primary_span, secondary_span) =
119            place_indefinitely_positioned_item(&*cell_occupancy_matrix, placement, grid_auto_flow, grid_position);
120
121        // Record item
122        record_grid_placement(
123            cell_occupancy_matrix,
124            item,
125            primary_axis,
126            primary_span,
127            secondary_span,
128            CellOccupancyState::AutoPlaced,
129        );
130
131        // If using the "dense" placement algorithm then reset the grid position back to grid_start_position ready for the next item
132        // Otherwise set it to the position of the current item so that the next item it placed after it.
133        grid_position = match grid_auto_flow.is_dense() {
134            true => grid_start_position,
135            false => (primary_span.end, secondary_span.start),
136        };
137    }
138}
139
140/// 8.5. Grid Item Placement Algorithm
141/// Place a single definitely placed item into the grid
142fn place_definite_grid_item(
143    placement: ItemPlacement,
144    primary_axis: AbsoluteAxis,
145) -> (Line<OriginZeroLine>, Line<OriginZeroLine>) {
146    // Resolve spans to tracks
147    let primary_span = placement.get(primary_axis).resolve_definite_grid_lines();
148    let secondary_span = placement.get(primary_axis.other_axis()).resolve_definite_grid_lines();
149
150    (primary_span, secondary_span)
151}
152
153/// 8.5. Grid Item Placement Algorithm
154/// Step 2. Place remaining children with definite secondary axis positions
155fn place_definite_secondary_axis_item(
156    cell_occupancy_matrix: &CellOccupancyMatrix,
157    placement: ItemPlacement,
158    auto_flow: GridAutoFlow,
159) -> (Line<OriginZeroLine>, Line<OriginZeroLine>) {
160    let primary_axis = auto_flow.primary_axis();
161    let secondary_axis = primary_axis.other_axis();
162    let primary_axis_grid_start_line = cell_occupancy_matrix.track_counts(primary_axis).implicit_start_line();
163
164    let secondary_axis_placement = placement.get(secondary_axis).resolve_definite_grid_lines();
165    let starting_position = match auto_flow.is_dense() {
166        true => primary_axis_grid_start_line,
167        false => cell_occupancy_matrix
168            .auto_placement_cursor(primary_axis, secondary_axis_placement.start)
169            .unwrap_or(primary_axis_grid_start_line),
170    };
171    let primary_axis_span = placement.get(primary_axis).indefinite_span();
172
173    let mut position: OriginZeroLine = starting_position;
174    loop {
175        let primary_axis_placement = resolve_indefinite_grid_span(position, primary_axis_span);
176
177        let collision = cell_occupancy_matrix.line_area_collision_jump(
178            primary_axis,
179            primary_axis_placement,
180            secondary_axis_placement,
181        );
182
183        match collision {
184            None => return (primary_axis_placement, secondary_axis_placement),
185            Some(next_position) => position = next_position,
186        }
187    }
188}
189
190/// 8.5. Grid Item Placement Algorithm
191/// Step 4. Position the remaining grid items.
192fn place_indefinitely_positioned_item(
193    cell_occupancy_matrix: &CellOccupancyMatrix,
194    placement: ItemPlacement,
195    auto_flow: GridAutoFlow,
196    grid_position: (OriginZeroLine, OriginZeroLine),
197) -> (Line<OriginZeroLine>, Line<OriginZeroLine>) {
198    let primary_axis = auto_flow.primary_axis();
199    let secondary_axis = primary_axis.other_axis();
200
201    let primary_placement_style = placement.get(primary_axis);
202    let secondary_placement_style = placement.get(secondary_axis);
203
204    let secondary_span = secondary_placement_style.indefinite_span();
205    let has_definite_primary_axis_position = primary_placement_style.is_definite();
206    let primary_axis_grid_start_line = cell_occupancy_matrix.track_counts(primary_axis).implicit_start_line();
207    let primary_axis_grid_end_line = cell_occupancy_matrix.track_counts(primary_axis).implicit_end_line();
208    let secondary_axis_grid_start_line = cell_occupancy_matrix.track_counts(secondary_axis).implicit_start_line();
209
210    let (mut primary_idx, mut secondary_idx) = grid_position;
211
212    if has_definite_primary_axis_position {
213        let primary_span = primary_placement_style.resolve_definite_grid_lines();
214
215        // Compute secondary axis starting position for search
216        secondary_idx = match auto_flow.is_dense() {
217            // If auto-flow is dense then we always search from the first track
218            true => secondary_axis_grid_start_line,
219            false => {
220                if primary_span.start < primary_idx {
221                    advance_position(secondary_idx)
222                } else {
223                    secondary_idx
224                }
225            }
226        };
227
228        // Item has fixed primary axis position: so we simply increment the secondary axis position
229        // until we find a space that the item fits in
230        loop {
231            let secondary_span = resolve_indefinite_grid_span(secondary_idx, secondary_span);
232
233            // If area is occupied, jump the index past the collision and try again
234            let collision =
235                cell_occupancy_matrix.line_area_collision_jump(secondary_axis, secondary_span, primary_span);
236            if let Some(next_position) = collision {
237                secondary_idx = next_position;
238                continue;
239            }
240
241            // Once we find a free space, return that position
242            return (primary_span, secondary_span);
243        }
244    } else {
245        let primary_span = primary_placement_style.indefinite_span();
246
247        // Whether the item spans every track in the primary axis. Such an item can only be
248        // placed at the primary axis grid start, in a stripe of entirely unoccupied tracks.
249        let spans_all_primary_tracks = primary_span as usize >= cell_occupancy_matrix.track_counts(primary_axis).len();
250
251        // Item does not have any fixed axis, so we search along the primary axis until we hit the end of the already
252        // existent tracks, and then we reset the primary axis back to zero and increment the secondary axis index.
253        // We continue in this vein until we find a space that the item fits in.
254        loop {
255            let primary_span = resolve_indefinite_grid_span(primary_idx, primary_span);
256            let secondary_span = resolve_indefinite_grid_span(secondary_idx, secondary_span);
257
258            // If the primary index is out of bounds, then increment the secondary index and reset the primary
259            // index back to the start of the grid
260            let primary_out_of_bounds = primary_span.end > primary_axis_grid_end_line;
261            if primary_out_of_bounds {
262                // If the span is out of bounds even at the search start position then it can never fit,
263                // as searching only ever moves the span further away from the start of the grid. Bail out
264                // and let `record_grid_placement` clamp the placement into the limited grid
265                if primary_idx == primary_axis_grid_start_line {
266                    return (primary_span, secondary_span);
267                }
268                secondary_idx = advance_position(secondary_idx);
269                primary_idx = primary_axis_grid_start_line;
270                continue;
271            }
272
273            // If the item spans every primary axis track, it fits if and only if all of the
274            // secondary axis tracks it spans are entirely unoccupied. Jump the secondary index
275            // past any non-empty tracks in the spanned stripe.
276            if spans_all_primary_tracks {
277                match cell_occupancy_matrix.occupied_track_jump(secondary_axis, secondary_span) {
278                    Some(next_position) => {
279                        secondary_idx = next_position;
280                        primary_idx = primary_axis_grid_start_line;
281                        continue;
282                    }
283                    None => return (primary_span, secondary_span),
284                }
285            }
286
287            // If area is occupied, jump the primary index past the collision and try again
288            let collision = cell_occupancy_matrix.line_area_collision_jump(primary_axis, primary_span, secondary_span);
289            if let Some(next_position) = collision {
290                primary_idx = next_position;
291                continue;
292            }
293
294            // Once we find a free space that's in bounds, return that position
295            return (primary_span, secondary_span);
296        }
297    }
298}
299
300/// Clamp a placement into the limited grid, preserving a span of at least 1 track.
301/// Items placed outside of the limited grid are clamped into it.
302///
303/// See: <https://www.w3.org/TR/css-grid-1/#overlarge-grids>
304fn clamp_span_to_limited_grid(span: Line<OriginZeroLine>) -> Line<OriginZeroLine> {
305    let start = span.start.0.clamp(MIN_OZ_LINE, MAX_OZ_LINE - 1);
306    let end = span.end.0.clamp(start + 1, MAX_OZ_LINE);
307    Line { start: OriginZeroLine(start), end: OriginZeroLine(end) }
308}
309
310/// Record the grid item's placement in both the CellOccupancyMatrix and the item itself
311/// once a definite placement has been determined
312fn record_grid_placement(
313    cell_occupancy_matrix: &mut CellOccupancyMatrix,
314    item: &mut GridItem,
315    primary_axis: AbsoluteAxis,
316    primary_span: Line<OriginZeroLine>,
317    secondary_span: Line<OriginZeroLine>,
318    placement_type: CellOccupancyState,
319) {
320    #[cfg(test)]
321    println!("BEFORE placement:");
322    #[cfg(test)]
323    println!("{cell_occupancy_matrix:?}");
324
325    // Clamp placements into the limited grid to prevent arithmetic overflow when growing the
326    // implicit grid (https://www.w3.org/TR/css-grid-1/#overlarge-grids)
327    let primary_span = clamp_span_to_limited_grid(primary_span);
328    let secondary_span = clamp_span_to_limited_grid(secondary_span);
329
330    // Mark area of grid as occupied
331    cell_occupancy_matrix.mark_area_as(primary_axis, primary_span, secondary_span, placement_type);
332
333    // Record placement on the item
334    let (col_span, row_span) = match primary_axis {
335        AbsoluteAxis::Horizontal => (primary_span, secondary_span),
336        AbsoluteAxis::Vertical => (secondary_span, primary_span),
337    };
338    item.column = col_span;
339    item.row = row_span;
340
341    #[cfg(test)]
342    println!("AFTER placement:");
343    #[cfg(test)]
344    println!("{cell_occupancy_matrix:?}");
345    #[cfg(test)]
346    println!("\n");
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    mod test_placement_algorithm {
354        use crate::compute::grid::implicit_grid::compute_grid_size_estimate;
355        use crate::compute::grid::types::TrackCounts;
356        use crate::compute::grid::util::test_helpers::{grid_items, resolve_named_lines};
357        use crate::compute::grid::util::*;
358        use crate::compute::grid::CellOccupancyMatrix;
359        use crate::compute::grid::NamedLineResolver;
360        use crate::compute::grid::OriginZeroLine;
361        use crate::prelude::*;
362        use crate::style::GridAutoFlow;
363
364        use super::super::place_grid_items;
365
366        type ExpectedPlacement = (i16, i16, i16, i16);
367
368        fn placement_test_runner(
369            explicit_col_count: u16,
370            explicit_row_count: u16,
371            children: Vec<(usize, Style, ExpectedPlacement)>,
372            expected_col_counts: TrackCounts,
373            expected_row_counts: TrackCounts,
374            flow: GridAutoFlow,
375        ) {
376            // Setup test
377            let mut name_resolver = NamedLineResolver::new(&Style::DEFAULT, 0, 0);
378            name_resolver.set_explicit_column_count(explicit_col_count);
379            name_resolver.set_explicit_row_count(explicit_row_count);
380            let mut items = grid_items(children.iter().map(|(index, style, _)| (*index, style)));
381            let placements = resolve_named_lines(
382                children.iter().map(|(_, style, _)| style),
383                &name_resolver,
384                explicit_col_count,
385                explicit_row_count,
386            );
387            let estimated_sizes = compute_grid_size_estimate(explicit_col_count, explicit_row_count, &placements);
388            let mut cell_occupancy_matrix =
389                CellOccupancyMatrix::with_track_counts(estimated_sizes.0, estimated_sizes.1);
390
391            // Run placement algorithm
392            place_grid_items(&mut cell_occupancy_matrix, &mut items, &placements, flow);
393
394            // Assert that each item has been placed in the right location (items are in the same order as children)
395            assert_eq!(items.len(), children.len());
396            for (idx, ((id, _style, expected_placement), item)) in children.iter().zip(items.iter()).enumerate() {
397                assert_eq!(item.node, NodeId::from(*id));
398                let actual_placement = (item.column.start, item.column.end, item.row.start, item.row.end);
399                assert_eq!(actual_placement, (*expected_placement).into_oz(), "Item {idx} (0-indexed)");
400            }
401
402            // Assert that the correct number of implicit rows have been generated
403            let actual_row_counts = *cell_occupancy_matrix.track_counts(crate::compute::grid::AbsoluteAxis::Vertical);
404            assert_eq!(actual_row_counts, expected_row_counts, "row track counts");
405            let actual_col_counts = *cell_occupancy_matrix.track_counts(crate::compute::grid::AbsoluteAxis::Horizontal);
406            assert_eq!(actual_col_counts, expected_col_counts, "column track counts");
407        }
408
409        #[test]
410        fn test_only_fixed_placement() {
411            let flow = GridAutoFlow::Row;
412            let explicit_col_count = 2;
413            let explicit_row_count = 2;
414            let children = {
415                vec![
416                    // node, style (grid coords), expected_placement (oz coords)
417                    (1, (line(1), auto(), line(1), auto()).into_grid_child(), (0, 1, 0, 1)),
418                    (2, (line(-4), auto(), line(-3), auto()).into_grid_child(), (-1, 0, 0, 1)),
419                    (3, (line(-3), auto(), line(-4), auto()).into_grid_child(), (0, 1, -1, 0)),
420                    (4, (line(3), span(2), line(5), auto()).into_grid_child(), (2, 4, 4, 5)),
421                ]
422            };
423            let expected_cols = TrackCounts { negative_implicit: 1, explicit: 2, positive_implicit: 2 };
424            let expected_rows = TrackCounts { negative_implicit: 1, explicit: 2, positive_implicit: 3 };
425            placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
426        }
427
428        #[test]
429        fn test_placement_spanning_origin() {
430            let flow = GridAutoFlow::Row;
431            let explicit_col_count = 2;
432            let explicit_row_count = 2;
433            let children = {
434                vec![
435                    // node, style (grid coords), expected_placement (oz coords)
436                    (1, (line(-1), line(-1), line(-1), line(-1)).into_grid_child(), (2, 3, 2, 3)),
437                    (2, (line(-1), span(2), line(-1), span(2)).into_grid_child(), (2, 4, 2, 4)),
438                    (3, (line(-4), line(-4), line(-4), line(-4)).into_grid_child(), (-1, 0, -1, 0)),
439                    (4, (line(-4), span(2), line(-4), span(2)).into_grid_child(), (-1, 1, -1, 1)),
440                ]
441            };
442            let expected_cols = TrackCounts { negative_implicit: 1, explicit: 2, positive_implicit: 2 };
443            let expected_rows = TrackCounts { negative_implicit: 1, explicit: 2, positive_implicit: 2 };
444            placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
445        }
446
447        #[test]
448        fn test_only_auto_placement_row_flow() {
449            let flow = GridAutoFlow::Row;
450            let explicit_col_count = 2;
451            let explicit_row_count = 2;
452            let children = {
453                let auto_child = (auto(), auto(), auto(), auto()).into_grid_child();
454                vec![
455                    // output order, node, style (grid coords), expected_placement (oz coords)
456                    (1, auto_child.clone(), (0, 1, 0, 1)),
457                    (2, auto_child.clone(), (1, 2, 0, 1)),
458                    (3, auto_child.clone(), (0, 1, 1, 2)),
459                    (4, auto_child.clone(), (1, 2, 1, 2)),
460                    (5, auto_child.clone(), (0, 1, 2, 3)),
461                    (6, auto_child.clone(), (1, 2, 2, 3)),
462                    (7, auto_child.clone(), (0, 1, 3, 4)),
463                    (8, auto_child.clone(), (1, 2, 3, 4)),
464                ]
465            };
466            let expected_cols = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 0 };
467            let expected_rows = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 2 };
468            placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
469        }
470
471        #[test]
472        fn test_only_auto_placement_column_flow() {
473            let flow = GridAutoFlow::Column;
474            let explicit_col_count = 2;
475            let explicit_row_count = 2;
476            let children = {
477                let auto_child = (auto(), auto(), auto(), auto()).into_grid_child();
478                vec![
479                    // output order, node, style (grid coords), expected_placement (oz coords)
480                    (1, auto_child.clone(), (0, 1, 0, 1)),
481                    (2, auto_child.clone(), (0, 1, 1, 2)),
482                    (3, auto_child.clone(), (1, 2, 0, 1)),
483                    (4, auto_child.clone(), (1, 2, 1, 2)),
484                    (5, auto_child.clone(), (2, 3, 0, 1)),
485                    (6, auto_child.clone(), (2, 3, 1, 2)),
486                    (7, auto_child.clone(), (3, 4, 0, 1)),
487                    (8, auto_child.clone(), (3, 4, 1, 2)),
488                ]
489            };
490            let expected_cols = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 2 };
491            let expected_rows = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 0 };
492            placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
493        }
494
495        #[test]
496        fn test_oversized_item() {
497            let flow = GridAutoFlow::Row;
498            let explicit_col_count = 2;
499            let explicit_row_count = 2;
500            let children = {
501                vec![
502                    // output order, node, style (grid coords), expected_placement (oz coords)
503                    (1, (span(5), auto(), auto(), auto()).into_grid_child(), (0, 5, 0, 1)),
504                ]
505            };
506            let expected_cols = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 3 };
507            let expected_rows = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 0 };
508            placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
509        }
510
511        #[test]
512        fn test_fixed_in_secondary_axis() {
513            let flow = GridAutoFlow::Row;
514            let explicit_col_count = 2;
515            let explicit_row_count = 2;
516            let children = {
517                vec![
518                    // output order, node, style (grid coords), expected_placement (oz coords)
519                    (1, (span(2), auto(), line(1), auto()).into_grid_child(), (0, 2, 0, 1)),
520                    (2, (auto(), auto(), line(2), auto()).into_grid_child(), (0, 1, 1, 2)),
521                    (3, (auto(), auto(), line(1), auto()).into_grid_child(), (2, 3, 0, 1)),
522                    (4, (auto(), auto(), line(4), auto()).into_grid_child(), (0, 1, 3, 4)),
523                ]
524            };
525            let expected_cols = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 1 };
526            let expected_rows = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 2 };
527            placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
528        }
529
530        #[test]
531        fn test_definite_in_secondary_axis_with_fully_definite_negative() {
532            let flow = GridAutoFlow::Row;
533            let explicit_col_count = 2;
534            let explicit_row_count = 2;
535            let children = {
536                vec![
537                    // output order, node, style (grid coords), expected_placement (oz coords)
538                    (2, (auto(), auto(), line(2), auto()).into_grid_child(), (0, 1, 1, 2)),
539                    (1, (line(-4), auto(), line(2), auto()).into_grid_child(), (-1, 0, 1, 2)),
540                    (3, (auto(), auto(), line(1), auto()).into_grid_child(), (-1, 0, 0, 1)),
541                ]
542            };
543            let expected_cols = TrackCounts { negative_implicit: 1, explicit: 2, positive_implicit: 0 };
544            let expected_rows = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 0 };
545            placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
546        }
547
548        #[test]
549        fn test_dense_packing_algorithm() {
550            let flow = GridAutoFlow::RowDense;
551            let explicit_col_count = 4;
552            let explicit_row_count = 4;
553            let children = {
554                vec![
555                    // output order, node, style (grid coords), expected_placement (oz coords)
556                    (1, (line(2), auto(), line(1), auto()).into_grid_child(), (1, 2, 0, 1)), // Definitely positioned in column 2
557                    (2, (span(2), auto(), auto(), auto()).into_grid_child(), (2, 4, 0, 1)), // Spans 2 columns, so positioned after item 1
558                    (3, (auto(), auto(), auto(), auto()).into_grid_child(), (0, 1, 0, 1)), // Spans 1 column, so should be positioned before item 1
559                ]
560            };
561            let expected_cols = TrackCounts { negative_implicit: 0, explicit: 4, positive_implicit: 0 };
562            let expected_rows = TrackCounts { negative_implicit: 0, explicit: 4, positive_implicit: 0 };
563            placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
564        }
565
566        #[test]
567        fn test_sparse_packing_algorithm() {
568            let flow = GridAutoFlow::Row;
569            let explicit_col_count = 4;
570            let explicit_row_count = 4;
571            let children = {
572                vec![
573                    // output order, node, style (grid coords), expected_placement (oz coords)
574                    (1, (auto(), span(3), auto(), auto()).into_grid_child(), (0, 3, 0, 1)), // Width 3
575                    (2, (auto(), span(3), auto(), auto()).into_grid_child(), (0, 3, 1, 2)), // Width 3 (wraps to next row)
576                    (3, (auto(), span(1), auto(), auto()).into_grid_child(), (3, 4, 1, 2)), // Width 1 (uses second row as we're already on it)
577                ]
578            };
579            let expected_cols = TrackCounts { negative_implicit: 0, explicit: 4, positive_implicit: 0 };
580            let expected_rows = TrackCounts { negative_implicit: 0, explicit: 4, positive_implicit: 0 };
581            placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
582        }
583
584        #[test]
585        fn test_auto_placement_in_negative_tracks() {
586            let flow = GridAutoFlow::RowDense;
587            let explicit_col_count = 2;
588            let explicit_row_count = 2;
589            let children = {
590                vec![
591                    // output order, node, style (grid coords), expected_placement (oz coords)
592                    (1, (line(-5), auto(), line(1), auto()).into_grid_child(), (-2, -1, 0, 1)), // Row 1. Definitely positioned in column -2
593                    (2, (auto(), auto(), line(2), auto()).into_grid_child(), (-2, -1, 1, 2)), // Row 2. Auto positioned in column -2
594                    (3, (auto(), auto(), auto(), auto()).into_grid_child(), (-1, 0, 0, 1)), // Row 1. Auto positioned in column -1
595                ]
596            };
597            let expected_cols = TrackCounts { negative_implicit: 2, explicit: 2, positive_implicit: 0 };
598            let expected_rows = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 0 };
599            placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
600        }
601
602        #[test]
603        fn test_overlarge_placement_is_clamped() {
604            let explicit_col_count = 9_000;
605            let explicit_row_count = 0;
606            let style = (line(-19_005), auto(), auto(), auto()).into_grid_child();
607            let children = [(0usize, style)];
608            let mut name_resolver = NamedLineResolver::new(&Style::DEFAULT, 0, 0);
609            name_resolver.set_explicit_column_count(explicit_col_count);
610            name_resolver.set_explicit_row_count(explicit_row_count);
611            let mut items = grid_items(children.iter().map(|(index, style)| (*index, style)));
612            let placements = resolve_named_lines(
613                children.iter().map(|(_, style)| style),
614                &name_resolver,
615                explicit_col_count,
616                explicit_row_count,
617            );
618            let estimated_sizes = compute_grid_size_estimate(explicit_col_count, explicit_row_count, &placements);
619            let mut cell_occupancy_matrix =
620                CellOccupancyMatrix::with_track_counts(estimated_sizes.0, estimated_sizes.1);
621            place_grid_items(&mut cell_occupancy_matrix, &mut items, &placements, GridAutoFlow::Row);
622            assert_eq!(items[0].column, Line { start: OriginZeroLine(-10_000), end: OriginZeroLine(-9_999) });
623        }
624    }
625
626    #[test]
627    fn auto_placement_cursor_saturates_at_integer_bounds() {
628        assert_eq!(advance_position(OriginZeroLine(i16::MAX)), OriginZeroLine(i16::MAX));
629    }
630
631    #[test]
632    fn indefinite_spans_saturate_at_integer_bounds() {
633        assert_eq!(
634            resolve_indefinite_grid_span(OriginZeroLine(i16::MAX), 1),
635            Line { start: OriginZeroLine(i16::MAX), end: OriginZeroLine(i16::MAX) }
636        );
637    }
638}