gioser

tawasuyu/taffy

A high performance rust-powered UI layout library

HTML 5544Rust 88Markdown 11TOML 91926 Sutichaykuna

⎇ a3f94c2748ff655b3ab42444640d8e69e917aaab src/compute/grid/placement.rs Rust · 782 siq'ikuna · 34.9 KB Ñawpa willay Pi imata llamkarqan ch'uya

Clamp CSS grids at 10000 tracks in each dimension a3f94c274 · Nico Burns ·

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::{max_oz_line, NamedLineResolver, OriginZeroLine, MIN_OZ_LINE};
5use crate::geometry::Line;
6use crate::geometry::{AbsoluteAxis, InBothAbsAxis};
7use crate::style::{AlignItems, GridAutoFlow, OriginZeroGridPlacement};
8use crate::tree::NodeId;
9use crate::util::sys::Vec;
10use crate::{CoreStyle, Direction, GridItemStyle};
11
12#[inline]
13/// Returns whether placement/search should run in reverse for this axis.
14fn axis_is_reversed(direction: Direction, axis: AbsoluteAxis) -> bool {
15    direction.is_rtl() && axis == AbsoluteAxis::Horizontal
16}
17
18#[inline]
19/// Advances the cursor by one track in the active search direction.
20fn advance_position(position: OriginZeroLine, axis_is_reversed: bool) -> OriginZeroLine {
21    if axis_is_reversed {
22        OriginZeroLine(position.0 - 1)
23    } else {
24        OriginZeroLine(position.0 + 1)
25    }
26}
27
28#[inline]
29/// Returns the initial search line for sparse/dense placement in the given axis direction.
30fn search_start_line(
31    grid_start_line: OriginZeroLine,
32    grid_end_line: OriginZeroLine,
33    axis_is_reversed: bool,
34) -> OriginZeroLine {
35    if axis_is_reversed {
36        grid_end_line - 1
37    } else {
38        grid_start_line
39    }
40}
41
42#[inline]
43/// Resolves an indefinite span at `position`, respecting the active axis direction.
44fn resolve_indefinite_grid_span(position: OriginZeroLine, span: u16, axis_is_reversed: bool) -> Line<OriginZeroLine> {
45    if axis_is_reversed {
46        Line { start: (position - span) + 1, end: position + 1 }
47    } else {
48        Line { start: position, end: position + span }
49    }
50}
51
52#[inline]
53/// Mirrors a horizontal span around the explicit grid width.
54fn mirror_horizontal_span(span: Line<OriginZeroLine>, explicit_col_count: u16) -> Line<OriginZeroLine> {
55    let explicit_col_end_line = explicit_col_count as i16;
56    Line {
57        start: OriginZeroLine(explicit_col_end_line - span.end.0),
58        end: OriginZeroLine(explicit_col_end_line - span.start.0),
59    }
60}
61
62#[inline]
63/// Mirrors horizontal spans for RTL while leaving all other spans unchanged.
64fn maybe_mirror_span(
65    span: Line<OriginZeroLine>,
66    axis: AbsoluteAxis,
67    direction: Direction,
68    explicit_col_count: u16,
69) -> Line<OriginZeroLine> {
70    if axis == AbsoluteAxis::Horizontal && direction.is_rtl() {
71        mirror_horizontal_span(span, explicit_col_count)
72    } else {
73        span
74    }
75}
76
77/// 8.5. Grid Item Placement Algorithm
78/// Place items into the grid, generating new rows/column into the implicit grid as required
79///
80/// [Specification](https://www.w3.org/TR/css-grid-2/#auto-placement-algo)
81#[allow(clippy::too_many_arguments)]
82pub(super) fn place_grid_items<'a, S, ChildIter>(
83    cell_occupancy_matrix: &mut CellOccupancyMatrix,
84    items: &mut Vec<GridItem>,
85    children_iter: impl Fn() -> ChildIter,
86    direction: Direction,
87    grid_auto_flow: GridAutoFlow,
88    align_items: AlignItems,
89    justify_items: AlignItems,
90    named_line_resolver: &NamedLineResolver<<S as CoreStyle>::CustomIdent>,
91) where
92    S: GridItemStyle + 'a,
93    ChildIter: Iterator<Item = (usize, NodeId, S)>,
94{
95    let primary_axis = grid_auto_flow.primary_axis();
96    let secondary_axis = primary_axis.other_axis();
97    let explicit_col_count = cell_occupancy_matrix.track_counts(AbsoluteAxis::Horizontal).explicit;
98
99    let map_child_style_to_origin_zero_placement = {
100        let explicit_row_count = cell_occupancy_matrix.track_counts(AbsoluteAxis::Vertical).explicit;
101        move |(index, node, style): (usize, NodeId, S)| -> (_, _, _, S) {
102            let origin_zero_placement = InBothAbsAxis {
103                horizontal: named_line_resolver
104                    .resolve_column_names(&style.grid_column())
105                    .map(|placement| placement.into_origin_zero_placement(explicit_col_count)),
106                vertical: named_line_resolver
107                    .resolve_row_names(&style.grid_row())
108                    .map(|placement| placement.into_origin_zero_placement(explicit_row_count)),
109            };
110            (index, node, origin_zero_placement, style)
111        }
112    };
113
114    // 1. Place children with definite positions
115    let mut idx = 0;
116    children_iter()
117        .map(map_child_style_to_origin_zero_placement)
118        .filter(|(_, _, placement, _)| placement.horizontal.is_definite() && placement.vertical.is_definite())
119        .for_each(|(index, child_node, child_placement, style)| {
120            idx += 1;
121            #[cfg(test)]
122            println!("Definite Item {idx}\n==============");
123
124            let (row_span, col_span) =
125                place_definite_grid_item(child_placement, primary_axis, direction, explicit_col_count);
126            record_grid_placement(
127                cell_occupancy_matrix,
128                items,
129                child_node,
130                index,
131                style,
132                align_items,
133                justify_items,
134                primary_axis,
135                row_span,
136                col_span,
137                CellOccupancyState::DefinitelyPlaced,
138            );
139        });
140
141    // 2. Place remaining children with definite secondary axis positions
142    let mut idx = 0;
143    children_iter()
144        .map(map_child_style_to_origin_zero_placement)
145        .filter(|(_, _, placement, _)| {
146            placement.get(secondary_axis).is_definite() && !placement.get(primary_axis).is_definite()
147        })
148        .for_each(|(index, child_node, child_placement, style)| {
149            idx += 1;
150            #[cfg(test)]
151            println!("Definite Secondary Item {idx}\n==============");
152
153            let (primary_span, secondary_span) = place_definite_secondary_axis_item(
154                &*cell_occupancy_matrix,
155                child_placement,
156                grid_auto_flow,
157                direction,
158                explicit_col_count,
159            );
160
161            record_grid_placement(
162                cell_occupancy_matrix,
163                items,
164                child_node,
165                index,
166                style,
167                align_items,
168                justify_items,
169                primary_axis,
170                primary_span,
171                secondary_span,
172                CellOccupancyState::AutoPlaced,
173            );
174        });
175
176    // 3. Determine the number of columns in the implicit grid
177    // By the time we get to this point in the execution, this is actually already accounted for:
178    //
179    // 3.1 Start with the columns from the explicit grid
180    //        => Handled by grid size estimate which is used to pre-size the GridOccupancyMatrix
181    //
182    // 3.2 Among all the items with a definite column position (explicitly positioned items, items positioned in the previous step,
183    //     and items not yet positioned but with a definite column) add columns to the beginning and end of the implicit grid as necessary
184    //     to accommodate those items.
185    //        => Handled by expand_to_fit_range which expands the GridOccupancyMatrix as necessary
186    //            -> Called by mark_area_as
187    //            -> Called by record_grid_placement
188    //
189    // 3.3 If the largest column span among all the items without a definite column position is larger than the width of
190    //     the implicit grid, add columns to the end of the implicit grid to accommodate that column span.
191    //        => Handled by grid size estimate which is used to pre-size the GridOccupancyMatrix
192
193    // 4. Position the remaining grid items
194    // (which either have definite position only in the secondary axis or indefinite positions in both axis)
195    let primary_axis = grid_auto_flow.primary_axis();
196    let secondary_axis = primary_axis.other_axis();
197    let primary_axis_grid_start_line = cell_occupancy_matrix.track_counts(primary_axis).implicit_start_line();
198    let primary_axis_grid_end_line = cell_occupancy_matrix.track_counts(primary_axis).implicit_end_line();
199    let secondary_axis_grid_start_line = cell_occupancy_matrix.track_counts(secondary_axis).implicit_start_line();
200    let secondary_axis_grid_end_line = cell_occupancy_matrix.track_counts(secondary_axis).implicit_end_line();
201    let primary_axis_is_reversed = axis_is_reversed(direction, primary_axis);
202    let grid_start_position = (
203        search_start_line(primary_axis_grid_start_line, primary_axis_grid_end_line, primary_axis_is_reversed),
204        search_start_line(
205            secondary_axis_grid_start_line,
206            secondary_axis_grid_end_line,
207            axis_is_reversed(direction, secondary_axis),
208        ),
209    );
210    let mut grid_position = grid_start_position;
211    let mut idx = 0;
212    children_iter()
213        .map(map_child_style_to_origin_zero_placement)
214        .filter(|(_, _, placement, _)| !placement.get(secondary_axis).is_definite())
215        .for_each(|(index, child_node, child_placement, style)| {
216            idx += 1;
217            #[cfg(test)]
218            println!("\nAuto Item {idx}\n==============");
219
220            // Compute placement
221            let (primary_span, secondary_span) = place_indefinitely_positioned_item(
222                &*cell_occupancy_matrix,
223                child_placement,
224                grid_auto_flow,
225                grid_position,
226                direction,
227                explicit_col_count,
228            );
229
230            // Record item
231            record_grid_placement(
232                cell_occupancy_matrix,
233                items,
234                child_node,
235                index,
236                style,
237                align_items,
238                justify_items,
239                primary_axis,
240                primary_span,
241                secondary_span,
242                CellOccupancyState::AutoPlaced,
243            );
244
245            // If using the "dense" placement algorithm then reset the grid position back to grid_start_position ready for the next item
246            // Otherwise set it to the position of the current item so that the next item it placed after it.
247            grid_position = match (grid_auto_flow.is_dense(), primary_axis_is_reversed) {
248                (true, _) => grid_start_position,
249                (false, false) => (primary_span.end, secondary_span.start),
250                (false, true) => (primary_span.start, secondary_span.start),
251            };
252        });
253}
254
255/// 8.5. Grid Item Placement Algorithm
256/// Place a single definitely placed item into the grid
257fn place_definite_grid_item(
258    placement: InBothAbsAxis<Line<OriginZeroGridPlacement>>,
259    primary_axis: AbsoluteAxis,
260    direction: Direction,
261    explicit_col_count: u16,
262) -> (Line<OriginZeroLine>, Line<OriginZeroLine>) {
263    // Resolve spans to tracks
264    let primary_span = maybe_mirror_span(
265        placement.get(primary_axis).resolve_definite_grid_lines(),
266        primary_axis,
267        direction,
268        explicit_col_count,
269    );
270    let secondary_span = maybe_mirror_span(
271        placement.get(primary_axis.other_axis()).resolve_definite_grid_lines(),
272        primary_axis.other_axis(),
273        direction,
274        explicit_col_count,
275    );
276
277    (primary_span, secondary_span)
278}
279
280/// 8.5. Grid Item Placement Algorithm
281/// Step 2. Place remaining children with definite secondary axis positions
282fn place_definite_secondary_axis_item(
283    cell_occupancy_matrix: &CellOccupancyMatrix,
284    placement: InBothAbsAxis<Line<OriginZeroGridPlacement>>,
285    auto_flow: GridAutoFlow,
286    direction: Direction,
287    explicit_col_count: u16,
288) -> (Line<OriginZeroLine>, Line<OriginZeroLine>) {
289    let primary_axis = auto_flow.primary_axis();
290    let secondary_axis = primary_axis.other_axis();
291    let primary_axis_is_reversed = axis_is_reversed(direction, primary_axis);
292    let primary_axis_grid_start_line = cell_occupancy_matrix.track_counts(primary_axis).implicit_start_line();
293    let primary_axis_grid_end_line = cell_occupancy_matrix.track_counts(primary_axis).implicit_end_line();
294
295    let secondary_axis_placement = maybe_mirror_span(
296        placement.get(secondary_axis).resolve_definite_grid_lines(),
297        secondary_axis,
298        direction,
299        explicit_col_count,
300    );
301    let starting_position = match auto_flow.is_dense() {
302        true => search_start_line(primary_axis_grid_start_line, primary_axis_grid_end_line, primary_axis_is_reversed),
303        false => {
304            let lookup_result = if primary_axis_is_reversed {
305                cell_occupancy_matrix.first_of_type(
306                    primary_axis,
307                    secondary_axis_placement.start,
308                    CellOccupancyState::AutoPlaced,
309                )
310            } else {
311                cell_occupancy_matrix.last_of_type(
312                    primary_axis,
313                    secondary_axis_placement.start,
314                    CellOccupancyState::AutoPlaced,
315                )
316            };
317            lookup_result.unwrap_or(search_start_line(
318                primary_axis_grid_start_line,
319                primary_axis_grid_end_line,
320                primary_axis_is_reversed,
321            ))
322        }
323    };
324    let primary_axis_span = placement.get(primary_axis).indefinite_span();
325
326    let mut position: OriginZeroLine = starting_position;
327    loop {
328        let primary_axis_placement =
329            resolve_indefinite_grid_span(position, primary_axis_span, primary_axis_is_reversed);
330
331        let does_fit = cell_occupancy_matrix.line_area_is_unoccupied(
332            primary_axis,
333            primary_axis_placement,
334            secondary_axis_placement,
335        );
336
337        if does_fit {
338            return (primary_axis_placement, secondary_axis_placement);
339        } else {
340            position = advance_position(position, primary_axis_is_reversed);
341        }
342    }
343}
344
345/// 8.5. Grid Item Placement Algorithm
346/// Step 4. Position the remaining grid items.
347fn place_indefinitely_positioned_item(
348    cell_occupancy_matrix: &CellOccupancyMatrix,
349    placement: InBothAbsAxis<Line<OriginZeroGridPlacement>>,
350    auto_flow: GridAutoFlow,
351    grid_position: (OriginZeroLine, OriginZeroLine),
352    direction: Direction,
353    explicit_col_count: u16,
354) -> (Line<OriginZeroLine>, Line<OriginZeroLine>) {
355    let primary_axis = auto_flow.primary_axis();
356    let secondary_axis = primary_axis.other_axis();
357    let primary_axis_is_reversed = axis_is_reversed(direction, primary_axis);
358    let secondary_axis_is_reversed = axis_is_reversed(direction, secondary_axis);
359
360    let primary_placement_style = placement.get(primary_axis);
361    let secondary_placement_style = placement.get(secondary_axis);
362
363    let secondary_span = secondary_placement_style.indefinite_span();
364    let has_definite_primary_axis_position = primary_placement_style.is_definite();
365    let primary_axis_grid_start_line = cell_occupancy_matrix.track_counts(primary_axis).implicit_start_line();
366    let primary_axis_grid_end_line = cell_occupancy_matrix.track_counts(primary_axis).implicit_end_line();
367    let secondary_axis_grid_start_line = cell_occupancy_matrix.track_counts(secondary_axis).implicit_start_line();
368    let secondary_axis_grid_end_line = cell_occupancy_matrix.track_counts(secondary_axis).implicit_end_line();
369    let primary_start_position =
370        search_start_line(primary_axis_grid_start_line, primary_axis_grid_end_line, primary_axis_is_reversed);
371    let secondary_start_position =
372        search_start_line(secondary_axis_grid_start_line, secondary_axis_grid_end_line, secondary_axis_is_reversed);
373
374    let line_area_is_occupied = |primary_span, secondary_span| {
375        !cell_occupancy_matrix.line_area_is_unoccupied(primary_axis, primary_span, secondary_span)
376    };
377
378    let (mut primary_idx, mut secondary_idx) = grid_position;
379
380    if has_definite_primary_axis_position {
381        let primary_span = maybe_mirror_span(
382            primary_placement_style.resolve_definite_grid_lines(),
383            primary_axis,
384            direction,
385            explicit_col_count,
386        );
387
388        // Compute secondary axis starting position for search
389        secondary_idx = match auto_flow.is_dense() {
390            // If auto-flow is dense then we always search from the first track
391            true => secondary_start_position,
392            false => {
393                let should_advance_secondary = if primary_axis_is_reversed {
394                    primary_span.start > primary_idx
395                } else {
396                    primary_span.start < primary_idx
397                };
398                if should_advance_secondary {
399                    advance_position(secondary_idx, secondary_axis_is_reversed)
400                } else {
401                    secondary_idx
402                }
403            }
404        };
405
406        // Item has fixed primary axis position: so we simply increment the secondary axis position
407        // until we find a space that the item fits in
408        loop {
409            let secondary_span =
410                resolve_indefinite_grid_span(secondary_idx, secondary_span, secondary_axis_is_reversed);
411
412            // If area is occupied, increment the index and try again
413            if line_area_is_occupied(primary_span, secondary_span) {
414                secondary_idx = advance_position(secondary_idx, secondary_axis_is_reversed);
415                continue;
416            }
417
418            // Once we find a free space, return that position
419            return (primary_span, secondary_span);
420        }
421    } else {
422        let primary_span = primary_placement_style.indefinite_span();
423
424        // Item does not have any fixed axis, so we search along the primary axis until we hit the end of the already
425        // existent tracks, and then we reset the primary axis back to zero and increment the secondary axis index.
426        // We continue in this vein until we find a space that the item fits in.
427        loop {
428            let primary_span = resolve_indefinite_grid_span(primary_idx, primary_span, primary_axis_is_reversed);
429            let secondary_span =
430                resolve_indefinite_grid_span(secondary_idx, secondary_span, secondary_axis_is_reversed);
431
432            // If the primary index is out of bounds, then increment the secondary index and reset the primary
433            // index back to the start of the grid
434            let primary_out_of_bounds = if primary_axis_is_reversed {
435                primary_span.start < primary_axis_grid_start_line
436            } else {
437                primary_span.end > primary_axis_grid_end_line
438            };
439            if primary_out_of_bounds {
440                secondary_idx = advance_position(secondary_idx, secondary_axis_is_reversed);
441                primary_idx = primary_start_position;
442                continue;
443            }
444
445            // If area is occupied, increment the primary index and try again
446            if line_area_is_occupied(primary_span, secondary_span) {
447                primary_idx = advance_position(primary_idx, primary_axis_is_reversed);
448                continue;
449            }
450
451            // Once we find a free space that's in bounds, return that position
452            return (primary_span, secondary_span);
453        }
454    }
455}
456
457/// Clamp a placement into the limited grid `[-MAX_GRID_TRACKS, explicit_track_count + MAX_GRID_TRACKS]`,
458/// preserving a span of at least 1 track. Items placed outside of the limited grid are clamped into it.
459///
460/// See: <https://www.w3.org/TR/css-grid-1/#overlarge-grids>
461fn clamp_span_to_limited_grid(span: Line<OriginZeroLine>, explicit_track_count: u16) -> Line<OriginZeroLine> {
462    let max_line = max_oz_line(explicit_track_count);
463    let start = span.start.0.clamp(MIN_OZ_LINE, max_line - 1);
464    let end = span.end.0.clamp(start + 1, max_line);
465    Line { start: OriginZeroLine(start), end: OriginZeroLine(end) }
466}
467
468/// Record the grid item in both CellOccupancyMatric and the GridItems list
469/// once a definite placement has been determined
470#[allow(clippy::too_many_arguments)]
471fn record_grid_placement<S: GridItemStyle>(
472    cell_occupancy_matrix: &mut CellOccupancyMatrix,
473    items: &mut Vec<GridItem>,
474    node: NodeId,
475    index: usize,
476    style: S,
477    parent_align_items: AlignItems,
478    parent_justify_items: AlignItems,
479    primary_axis: AbsoluteAxis,
480    primary_span: Line<OriginZeroLine>,
481    secondary_span: Line<OriginZeroLine>,
482    placement_type: CellOccupancyState,
483) {
484    #[cfg(test)]
485    println!("BEFORE placement:");
486    #[cfg(test)]
487    println!("{cell_occupancy_matrix:?}");
488
489    // Clamp placements into the limited grid to prevent arithmetic overflow when growing the
490    // implicit grid (https://www.w3.org/TR/css-grid-1/#overlarge-grids)
491    let primary_explicit_count = cell_occupancy_matrix.track_counts(primary_axis).explicit;
492    let secondary_explicit_count = cell_occupancy_matrix.track_counts(primary_axis.other_axis()).explicit;
493    let primary_span = clamp_span_to_limited_grid(primary_span, primary_explicit_count);
494    let secondary_span = clamp_span_to_limited_grid(secondary_span, secondary_explicit_count);
495
496    // Mark area of grid as occupied
497    cell_occupancy_matrix.mark_area_as(primary_axis, primary_span, secondary_span, placement_type);
498
499    // Create grid item
500    let (col_span, row_span) = match primary_axis {
501        AbsoluteAxis::Horizontal => (primary_span, secondary_span),
502        AbsoluteAxis::Vertical => (secondary_span, primary_span),
503    };
504    items.push(GridItem::new_with_placement_style_and_order(
505        node,
506        col_span,
507        row_span,
508        style,
509        parent_align_items,
510        parent_justify_items,
511        index as u16,
512    ));
513
514    #[cfg(test)]
515    println!("AFTER placement:");
516    #[cfg(test)]
517    println!("{cell_occupancy_matrix:?}");
518    #[cfg(test)]
519    println!("\n");
520}
521
522#[cfg(test)]
523mod tests {
524
525    mod test_placement_algorithm {
526        use crate::compute::grid::implicit_grid::compute_grid_size_estimate;
527        use crate::compute::grid::types::TrackCounts;
528        use crate::compute::grid::util::*;
529        use crate::compute::grid::CellOccupancyMatrix;
530        use crate::compute::grid::NamedLineResolver;
531        use crate::prelude::*;
532        use crate::style::GridAutoFlow;
533        use crate::Direction;
534
535        use super::super::place_grid_items;
536
537        type ExpectedPlacement = (i16, i16, i16, i16);
538
539        fn placement_test_runner(
540            explicit_col_count: u16,
541            explicit_row_count: u16,
542            children: Vec<(usize, Style, ExpectedPlacement)>,
543            expected_col_counts: TrackCounts,
544            expected_row_counts: TrackCounts,
545            flow: GridAutoFlow,
546        ) {
547            // Setup test
548            let children_iter = || children.iter().map(|(index, style, _)| (*index, NodeId::from(*index), style));
549            let child_styles_iter = children.iter().map(|(_, style, _)| style);
550            let estimated_sizes =
551                compute_grid_size_estimate(explicit_col_count, explicit_row_count, Direction::Ltr, child_styles_iter);
552            let mut items = Vec::new();
553            let mut cell_occupancy_matrix =
554                CellOccupancyMatrix::with_track_counts(estimated_sizes.0, estimated_sizes.1);
555            let mut name_resolver = NamedLineResolver::new(&Style::DEFAULT, 0, 0);
556            name_resolver.set_explicit_column_count(explicit_col_count);
557            name_resolver.set_explicit_row_count(explicit_row_count);
558
559            // Run placement algorithm
560            place_grid_items(
561                &mut cell_occupancy_matrix,
562                &mut items,
563                children_iter,
564                Direction::Ltr,
565                flow,
566                AlignSelf::START,
567                AlignSelf::START,
568                // TODO: actually test named line resolution
569                &name_resolver,
570            );
571
572            // Assert that each item has been placed in the right location
573            let mut sorted_children = children.clone();
574            sorted_children.sort_by_key(|child| child.0);
575            for (idx, ((id, _style, expected_placement), item)) in sorted_children.iter().zip(items.iter()).enumerate()
576            {
577                assert_eq!(item.node, NodeId::from(*id));
578                let actual_placement = (item.column.start, item.column.end, item.row.start, item.row.end);
579                assert_eq!(actual_placement, (*expected_placement).into_oz(), "Item {idx} (0-indexed)");
580            }
581
582            // Assert that the correct number of implicit rows have been generated
583            let actual_row_counts = *cell_occupancy_matrix.track_counts(crate::compute::grid::AbsoluteAxis::Vertical);
584            assert_eq!(actual_row_counts, expected_row_counts, "row track counts");
585            let actual_col_counts = *cell_occupancy_matrix.track_counts(crate::compute::grid::AbsoluteAxis::Horizontal);
586            assert_eq!(actual_col_counts, expected_col_counts, "column track counts");
587        }
588
589        #[test]
590        fn test_only_fixed_placement() {
591            let flow = GridAutoFlow::Row;
592            let explicit_col_count = 2;
593            let explicit_row_count = 2;
594            let children = {
595                vec![
596                    // node, style (grid coords), expected_placement (oz coords)
597                    (1, (line(1), auto(), line(1), auto()).into_grid_child(), (0, 1, 0, 1)),
598                    (2, (line(-4), auto(), line(-3), auto()).into_grid_child(), (-1, 0, 0, 1)),
599                    (3, (line(-3), auto(), line(-4), auto()).into_grid_child(), (0, 1, -1, 0)),
600                    (4, (line(3), span(2), line(5), auto()).into_grid_child(), (2, 4, 4, 5)),
601                ]
602            };
603            let expected_cols = TrackCounts { negative_implicit: 1, explicit: 2, positive_implicit: 2 };
604            let expected_rows = TrackCounts { negative_implicit: 1, explicit: 2, positive_implicit: 3 };
605            placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
606        }
607
608        #[test]
609        fn test_placement_spanning_origin() {
610            let flow = GridAutoFlow::Row;
611            let explicit_col_count = 2;
612            let explicit_row_count = 2;
613            let children = {
614                vec![
615                    // node, style (grid coords), expected_placement (oz coords)
616                    (1, (line(-1), line(-1), line(-1), line(-1)).into_grid_child(), (2, 3, 2, 3)),
617                    (2, (line(-1), span(2), line(-1), span(2)).into_grid_child(), (2, 4, 2, 4)),
618                    (3, (line(-4), line(-4), line(-4), line(-4)).into_grid_child(), (-1, 0, -1, 0)),
619                    (4, (line(-4), span(2), line(-4), span(2)).into_grid_child(), (-1, 1, -1, 1)),
620                ]
621            };
622            let expected_cols = TrackCounts { negative_implicit: 1, explicit: 2, positive_implicit: 2 };
623            let expected_rows = TrackCounts { negative_implicit: 1, explicit: 2, positive_implicit: 2 };
624            placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
625        }
626
627        #[test]
628        fn test_only_auto_placement_row_flow() {
629            let flow = GridAutoFlow::Row;
630            let explicit_col_count = 2;
631            let explicit_row_count = 2;
632            let children = {
633                let auto_child = (auto(), auto(), auto(), auto()).into_grid_child();
634                vec![
635                    // output order, node, style (grid coords), expected_placement (oz coords)
636                    (1, auto_child.clone(), (0, 1, 0, 1)),
637                    (2, auto_child.clone(), (1, 2, 0, 1)),
638                    (3, auto_child.clone(), (0, 1, 1, 2)),
639                    (4, auto_child.clone(), (1, 2, 1, 2)),
640                    (5, auto_child.clone(), (0, 1, 2, 3)),
641                    (6, auto_child.clone(), (1, 2, 2, 3)),
642                    (7, auto_child.clone(), (0, 1, 3, 4)),
643                    (8, auto_child.clone(), (1, 2, 3, 4)),
644                ]
645            };
646            let expected_cols = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 0 };
647            let expected_rows = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 2 };
648            placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
649        }
650
651        #[test]
652        fn test_only_auto_placement_column_flow() {
653            let flow = GridAutoFlow::Column;
654            let explicit_col_count = 2;
655            let explicit_row_count = 2;
656            let children = {
657                let auto_child = (auto(), auto(), auto(), auto()).into_grid_child();
658                vec![
659                    // output order, node, style (grid coords), expected_placement (oz coords)
660                    (1, auto_child.clone(), (0, 1, 0, 1)),
661                    (2, auto_child.clone(), (0, 1, 1, 2)),
662                    (3, auto_child.clone(), (1, 2, 0, 1)),
663                    (4, auto_child.clone(), (1, 2, 1, 2)),
664                    (5, auto_child.clone(), (2, 3, 0, 1)),
665                    (6, auto_child.clone(), (2, 3, 1, 2)),
666                    (7, auto_child.clone(), (3, 4, 0, 1)),
667                    (8, auto_child.clone(), (3, 4, 1, 2)),
668                ]
669            };
670            let expected_cols = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 2 };
671            let expected_rows = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 0 };
672            placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
673        }
674
675        #[test]
676        fn test_oversized_item() {
677            let flow = GridAutoFlow::Row;
678            let explicit_col_count = 2;
679            let explicit_row_count = 2;
680            let children = {
681                vec![
682                    // output order, node, style (grid coords), expected_placement (oz coords)
683                    (1, (span(5), auto(), auto(), auto()).into_grid_child(), (0, 5, 0, 1)),
684                ]
685            };
686            let expected_cols = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 3 };
687            let expected_rows = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 0 };
688            placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
689        }
690
691        #[test]
692        fn test_fixed_in_secondary_axis() {
693            let flow = GridAutoFlow::Row;
694            let explicit_col_count = 2;
695            let explicit_row_count = 2;
696            let children = {
697                vec![
698                    // output order, node, style (grid coords), expected_placement (oz coords)
699                    (1, (span(2), auto(), line(1), auto()).into_grid_child(), (0, 2, 0, 1)),
700                    (2, (auto(), auto(), line(2), auto()).into_grid_child(), (0, 1, 1, 2)),
701                    (3, (auto(), auto(), line(1), auto()).into_grid_child(), (2, 3, 0, 1)),
702                    (4, (auto(), auto(), line(4), auto()).into_grid_child(), (0, 1, 3, 4)),
703                ]
704            };
705            let expected_cols = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 1 };
706            let expected_rows = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 2 };
707            placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
708        }
709
710        #[test]
711        fn test_definite_in_secondary_axis_with_fully_definite_negative() {
712            let flow = GridAutoFlow::Row;
713            let explicit_col_count = 2;
714            let explicit_row_count = 2;
715            let children = {
716                vec![
717                    // output order, node, style (grid coords), expected_placement (oz coords)
718                    (2, (auto(), auto(), line(2), auto()).into_grid_child(), (0, 1, 1, 2)),
719                    (1, (line(-4), auto(), line(2), auto()).into_grid_child(), (-1, 0, 1, 2)),
720                    (3, (auto(), auto(), line(1), auto()).into_grid_child(), (-1, 0, 0, 1)),
721                ]
722            };
723            let expected_cols = TrackCounts { negative_implicit: 1, explicit: 2, positive_implicit: 0 };
724            let expected_rows = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 0 };
725            placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
726        }
727
728        #[test]
729        fn test_dense_packing_algorithm() {
730            let flow = GridAutoFlow::RowDense;
731            let explicit_col_count = 4;
732            let explicit_row_count = 4;
733            let children = {
734                vec![
735                    // output order, node, style (grid coords), expected_placement (oz coords)
736                    (1, (line(2), auto(), line(1), auto()).into_grid_child(), (1, 2, 0, 1)), // Definitely positioned in column 2
737                    (2, (span(2), auto(), auto(), auto()).into_grid_child(), (2, 4, 0, 1)), // Spans 2 columns, so positioned after item 1
738                    (3, (auto(), auto(), auto(), auto()).into_grid_child(), (0, 1, 0, 1)), // Spans 1 column, so should be positioned before item 1
739                ]
740            };
741            let expected_cols = TrackCounts { negative_implicit: 0, explicit: 4, positive_implicit: 0 };
742            let expected_rows = TrackCounts { negative_implicit: 0, explicit: 4, positive_implicit: 0 };
743            placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
744        }
745
746        #[test]
747        fn test_sparse_packing_algorithm() {
748            let flow = GridAutoFlow::Row;
749            let explicit_col_count = 4;
750            let explicit_row_count = 4;
751            let children = {
752                vec![
753                    // output order, node, style (grid coords), expected_placement (oz coords)
754                    (1, (auto(), span(3), auto(), auto()).into_grid_child(), (0, 3, 0, 1)), // Width 3
755                    (2, (auto(), span(3), auto(), auto()).into_grid_child(), (0, 3, 1, 2)), // Width 3 (wraps to next row)
756                    (3, (auto(), span(1), auto(), auto()).into_grid_child(), (3, 4, 1, 2)), // Width 1 (uses second row as we're already on it)
757                ]
758            };
759            let expected_cols = TrackCounts { negative_implicit: 0, explicit: 4, positive_implicit: 0 };
760            let expected_rows = TrackCounts { negative_implicit: 0, explicit: 4, positive_implicit: 0 };
761            placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
762        }
763
764        #[test]
765        fn test_auto_placement_in_negative_tracks() {
766            let flow = GridAutoFlow::RowDense;
767            let explicit_col_count = 2;
768            let explicit_row_count = 2;
769            let children = {
770                vec![
771                    // output order, node, style (grid coords), expected_placement (oz coords)
772                    (1, (line(-5), auto(), line(1), auto()).into_grid_child(), (-2, -1, 0, 1)), // Row 1. Definitely positioned in column -2
773                    (2, (auto(), auto(), line(2), auto()).into_grid_child(), (-2, -1, 1, 2)), // Row 2. Auto positioned in column -2
774                    (3, (auto(), auto(), auto(), auto()).into_grid_child(), (-1, 0, 0, 1)), // Row 1. Auto positioned in column -1
775                ]
776            };
777            let expected_cols = TrackCounts { negative_implicit: 2, explicit: 2, positive_implicit: 0 };
778            let expected_rows = TrackCounts { negative_implicit: 0, explicit: 2, positive_implicit: 0 };
779            placement_test_runner(explicit_col_count, explicit_row_count, children, expected_cols, expected_rows, flow);
780        }
781    }
782}