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