Skip to main content

taffy/compute/grid/
mod.rs

1//! This module is a partial implementation of the CSS Grid Level 1 specification
2//! <https://www.w3.org/TR/css-grid-1>
3use crate::geometry::{AbsoluteAxis, AbstractAxis, InBothAbsAxis};
4use crate::geometry::{Line, Point, Rect, Size};
5use crate::style::{AlignContent, AlignItems, AvailableSpace, Contain, ContainingBlockClaims, Overflow};
6use crate::tree::{
7    AxisStaticPosition, Baselines, Layout, LayoutInput, LayoutOutput, LayoutPartialTreeExt, NodeId, OofCandidate,
8    OofCandidates, OofPositioningArea, RunMode, SizingMode,
9};
10use crate::util::debug::debug_log;
11use crate::util::sys::{f32_max, f32_min, GridTrackVec, Vec};
12use crate::util::MaybeMath;
13use crate::util::{MaybeResolve, ResolveOrZero};
14use crate::{
15    style_helpers::*, BoxGenerationMode, BoxSizing, CoreStyle, Direction, GridContainerStyle, GridItemStyle,
16    LayoutGridContainer, RequestedAxis,
17};
18use alignment::{align_and_position_item, align_tracks};
19use explicit_grid::{compute_explicit_grid_size_in_axis, initialize_grid_tracks, AutoRepeatStrategy};
20use implicit_grid::compute_grid_size_estimate;
21use placement::{place_grid_items, ItemPlacement};
22use track_sizing::{
23    determine_if_item_crosses_flexible_or_intrinsic_tracks, resolve_item_track_indexes, track_sizing_algorithm,
24};
25use types::{CellOccupancyMatrix, GridAreasMap, GridTrack, NamedLineResolver};
26
27use crate::sys::{DefaultCheapStr, String};
28use crate::{CheapCloneStr, GridAreaAxis, GridPlacement};
29use core::ops::Range;
30use types::{GridItem, GridTrackKind, TrackCounts};
31
32pub(crate) use types::{GridCoordinate, GridLine, OriginZeroLine, MAX_GRID_TRACKS, MAX_OZ_LINE, MIN_OZ_LINE};
33
34pub use types::{GridLineNames, GridLineNamesIter};
35
36mod alignment;
37mod explicit_grid;
38mod implicit_grid;
39mod placement;
40mod track_sizing;
41mod types;
42mod util;
43/// The grid container's resolved box, size constraints, alignment styles and available space,
44/// computed once from the container's style and the layout inputs before the grid is laid out
45#[derive(Clone, Copy)]
46struct GridContainerConstants {
47    /// The container's `direction` style
48    direction: Direction,
49    /// The container's `contain` style
50    contain: Contain,
51    /// Which positioning schemes the container is a containing block for
52    containing_block_claims: ContainingBlockClaims,
53    /// The container's resolved padding
54    padding: Rect<f32>,
55    /// The container's resolved border
56    border: Rect<f32>,
57    /// The sum of padding and border in each axis
58    padding_border_size: Size<f32>,
59    /// The container's border-box min size, if definite
60    min_size: Size<Option<f32>>,
61    /// The container's border-box max size, if definite
62    max_size: Size<Option<f32>>,
63    /// The container's border-box preferred size, if definite
64    preferred_size: Size<Option<f32>>,
65    /// The space reserved for scrollbars in each axis
66    scrollbar_gutter: Point<f32>,
67    /// Whether the container's overflow makes it a scroll container
68    #[cfg(feature = "content_size")]
69    is_scroll_container: bool,
70    /// Padding, border and scrollbar gutter combined
71    content_box_inset: Rect<f32>,
72    /// The container's `align-content` style
73    align_content: AlignContent,
74    /// The container's `justify-content` style
75    justify_content: AlignContent,
76    /// The container's `align-items` style
77    align_items: AlignItems,
78    /// The container's `justify-items` style
79    justify_items: AlignItems,
80    /// The space available to size the grid tracks in each axis
81    available_grid_space: Size<AvailableSpace>,
82    /// The container's border-box size, if definite
83    outer_node_size: Size<Option<f32>>,
84    /// The container's content-box min size, if definite
85    inner_min_size: Size<Option<f32>>,
86    /// The container's content-box max size, if definite
87    inner_max_size: Size<Option<f32>>,
88    /// The container's content-box size, if definite
89    inner_node_size: Size<Option<f32>>,
90}
91
92/// Resolve the grid container's style against the layout inputs (step 1 of `compute_grid_layout`)
93fn compute_container_constants<Tree: LayoutGridContainer>(
94    tree: &Tree,
95    style: &Tree::GridContainerStyle<'_>,
96    inputs: LayoutInput,
97) -> GridContainerConstants {
98    let LayoutInput { known_dimensions, parent_size, available_space, .. } = inputs;
99
100    let direction = style.direction();
101    let contain = style.contain();
102    let containing_block_claims = style.is_containing_block();
103
104    // 1. Compute "available grid space"
105    // https://www.w3.org/TR/css-grid-1/#available-grid-space
106    let aspect_ratio = style.aspect_ratio();
107    let padding = style.padding().resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis));
108    let border = style.border().resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis));
109    let padding_border = padding + border;
110    let padding_border_size = padding_border.sum_axes();
111    let box_sizing_adjustment =
112        if style.box_sizing() == BoxSizing::ContentBox { padding_border_size } else { Size::ZERO };
113
114    let min_size = style
115        .min_size()
116        .maybe_resolve(parent_size, |val, basis| tree.calc(val, basis))
117        .maybe_apply_aspect_ratio(aspect_ratio)
118        .maybe_add(box_sizing_adjustment);
119    let max_size = style
120        .max_size()
121        .maybe_resolve(parent_size, |val, basis| tree.calc(val, basis))
122        .maybe_apply_aspect_ratio(aspect_ratio)
123        .maybe_add(box_sizing_adjustment);
124    let preferred_size = if inputs.sizing_mode == SizingMode::InherentSize {
125        style
126            .size()
127            .maybe_resolve(parent_size, |val, basis| tree.calc(val, basis))
128            .maybe_apply_aspect_ratio(style.aspect_ratio())
129            .maybe_add(box_sizing_adjustment)
130    } else {
131        Size::NONE
132    };
133
134    // Scrollbar gutters are reserved when the `overflow` property is set to `Overflow::Scroll`.
135    // However, the axis are switched (transposed) because a node that scrolls vertically needs
136    // *horizontal* space to be reserved for a scrollbar
137    let scrollbar_gutter = style.overflow().transpose().map(|overflow| match overflow {
138        Overflow::Scroll => style.scrollbar_width(),
139        _ => 0.0,
140    });
141    #[cfg(feature = "content_size")]
142    let is_scroll_container = {
143        let overflow = style.overflow();
144        overflow.x.is_scroll_container() || overflow.y.is_scroll_container()
145    };
146    let mut content_box_inset = padding_border;
147    content_box_inset.bottom += scrollbar_gutter.y;
148
149    match direction {
150        Direction::Ltr => content_box_inset.right += scrollbar_gutter.x,
151        Direction::Rtl => content_box_inset.left += scrollbar_gutter.x,
152    };
153
154    // `normal` behaves as `stretch` for grid containers
155    let align_content = style.grid_align_content(AbstractAxis::Block);
156    let justify_content = style.grid_align_content(AbstractAxis::Inline);
157    let align_items = style.align_items();
158    let justify_items = style.justify_items();
159    let constrained_available_space = known_dimensions
160        .or(preferred_size)
161        .map(|size| size.map(AvailableSpace::Definite))
162        .unwrap_or(available_space)
163        .maybe_clamp(min_size, max_size)
164        .maybe_max(padding_border_size);
165
166    let available_grid_space = Size {
167        width: constrained_available_space
168            .width
169            .map_definite_value(|space| space - content_box_inset.horizontal_axis_sum()),
170        height: constrained_available_space
171            .height
172            .map_definite_value(|space| space - content_box_inset.vertical_axis_sum()),
173    };
174
175    let outer_node_size =
176        known_dimensions.or(preferred_size).maybe_clamp(min_size, max_size).maybe_max(padding_border_size);
177
178    // The track sizing algorithm operates on the grid container's content box, so the min/max sizes
179    // (which are border-box sizes) need converting to content-box sizes before being passed to it
180    let inner_min_size = min_size.maybe_sub(content_box_inset.sum_axes());
181    let inner_max_size = max_size.maybe_sub(content_box_inset.sum_axes());
182    let inner_node_size = Size {
183        width: outer_node_size.width.map(|space| space - content_box_inset.horizontal_axis_sum()),
184        height: outer_node_size.height.map(|space| space - content_box_inset.vertical_axis_sum()),
185    };
186
187    GridContainerConstants {
188        direction,
189        contain,
190        containing_block_claims,
191        padding,
192        border,
193        padding_border_size,
194        min_size,
195        max_size,
196        preferred_size,
197        scrollbar_gutter,
198        #[cfg(feature = "content_size")]
199        is_scroll_container,
200        content_box_inset,
201        align_content,
202        justify_content,
203        align_items,
204        justify_items,
205        available_grid_space,
206        outer_node_size,
207        inner_min_size,
208        inner_max_size,
209        inner_node_size,
210    }
211}
212
213/// Grid layout algorithm
214/// This consists of a few phases:
215///   - Resolving the explicit grid
216///   - Placing items (which also resolves the implicit grid)
217///   - Track (row/column) sizing
218///   - Alignment & Final item placement
219pub fn compute_grid_layout<Tree: LayoutGridContainer>(
220    tree: &mut Tree,
221    node: NodeId,
222    inputs: LayoutInput,
223) -> LayoutOutput {
224    let LayoutInput { known_dimensions, run_mode, .. } = inputs;
225
226    let style = tree.get_grid_container_style(node);
227    let constants = compute_container_constants(tree, &style, inputs);
228    let GridContainerConstants {
229        direction,
230        contain,
231        containing_block_claims,
232        padding,
233        border,
234        padding_border_size,
235        min_size,
236        max_size,
237        preferred_size,
238        scrollbar_gutter,
239        #[cfg(feature = "content_size")]
240        is_scroll_container,
241        content_box_inset,
242        align_content,
243        justify_content,
244        align_items,
245        justify_items,
246        available_grid_space,
247        outer_node_size,
248        inner_min_size,
249        inner_max_size,
250        mut inner_node_size,
251    } = constants;
252
253    // Note: we avoid accessing the grid rows/columns methods more than once as this can
254    // cause an expensive-ish computation
255    let grid_template_columns = style.grid_template_columns();
256    let grid_template_rows = style.grid_template_rows();
257    let grid_auto_columns = style.grid_auto_columns();
258    let grid_auto_rows = style.grid_auto_rows();
259
260    debug_log!("parent_size", dbg:inputs.parent_size);
261    debug_log!("outer_node_size", dbg:outer_node_size);
262    debug_log!("inner_node_size", dbg:inner_node_size);
263
264    // Short-circuit layout if the container's size is fully determined by the container's size and the run mode
265    // is ComputeSize (and thus the container's size is all that we're interested in)
266    if run_mode == RunMode::ComputeSize {
267        if let Size { width: Some(width), height: Some(height) } = outer_node_size {
268            return LayoutOutput::from_outer_size(Size { width, height });
269        }
270
271        // We can also short-circuit if the width is known and only the width has been requested.
272        if inputs.axis == RequestedAxis::Horizontal {
273            if let Some(width) = outer_node_size.width {
274                return LayoutOutput::from_outer_size(Size { width, height: 0.0 });
275            }
276        }
277    }
278
279    // 2. Resolve the explicit grid
280
281    // This is very similar to the inner_node_size except if the inner_node_size is not definite but the node
282    // has a min- or max- size style then that will be used in it's place.
283    let auto_fit_container_size = outer_node_size
284        .or(max_size)
285        .or(min_size)
286        .maybe_clamp(min_size, max_size)
287        .maybe_max(padding_border_size)
288        .maybe_sub(content_box_inset.sum_axes());
289
290    // If the grid container has a definite size or max size in the relevant axis:
291    //   - then the number of repetitions is the largest possible positive integer that does not cause the grid to overflow the content
292    //     box of its grid container.
293    // Otherwise, if the grid container has a definite min size in the relevant axis:
294    //   - then the number of repetitions is the smallest possible positive integer that fulfills that minimum requirement
295    // Otherwise, the specified track list repeats only once.
296    let auto_repeat_fit_strategy = outer_node_size.or(max_size).map(|val| match val {
297        Some(_) => AutoRepeatStrategy::MaxRepetitionsThatDoNotOverflow,
298        None => AutoRepeatStrategy::MinRepetitionsThatDoOverflow,
299    });
300
301    // Compute the number of rows and columns in the explicit grid *template*
302    // (explicit tracks from grid_areas are computed separately below)
303    let (col_auto_repetition_count, grid_template_col_count) = compute_explicit_grid_size_in_axis(
304        &style,
305        auto_fit_container_size.width,
306        auto_repeat_fit_strategy.width,
307        |val, basis| tree.calc(val, basis),
308        AbsoluteAxis::Horizontal,
309    );
310    let (row_auto_repetition_count, grid_template_row_count) = compute_explicit_grid_size_in_axis(
311        &style,
312        auto_fit_container_size.height,
313        auto_repeat_fit_strategy.height,
314        |val, basis| tree.calc(val, basis),
315        AbsoluteAxis::Vertical,
316    );
317
318    // type CustomIdent<'a> = <<Tree as LayoutPartialTree>::CoreContainerStyle<'_> as CoreStyle>::CustomIdent;
319    let mut name_resolver = NamedLineResolver::new(&style, col_auto_repetition_count, row_auto_repetition_count);
320
321    // Clamp the explicit grid to MAX_GRID_TRACKS tracks in each axis
322    // https://www.w3.org/TR/css-grid-1/#overlarge-grids
323    let explicit_col_count = grid_template_col_count.max(name_resolver.area_column_count()).min(MAX_GRID_TRACKS);
324    let explicit_row_count = grid_template_row_count.max(name_resolver.area_row_count()).min(MAX_GRID_TRACKS);
325
326    name_resolver.set_explicit_column_count(explicit_col_count);
327    name_resolver.set_explicit_row_count(explicit_row_count);
328
329    // Build the per-line names of the explicit grid from the name resolver's collected pairs
330    let mut detailed_column_line_names = name_resolver.detailed_line_names(AbsoluteAxis::Horizontal);
331    let mut detailed_row_line_names = name_resolver.detailed_line_names(AbsoluteAxis::Vertical);
332
333    // 3. Create the grid items (in document order) and resolve the named lines in their `grid-row`/`grid-column` styles
334    // Absolutely positioned children do not take part in grid placement and do not create implicit tracks,
335    // so they are excluded here. Each child's style is read and its named lines resolved exactly once; the
336    // results drive both the grid size estimate and item placement below.
337    let child_count = tree.child_count(node);
338    let mut items: Vec<GridItem> = Vec::with_capacity(child_count);
339    let mut placements: Vec<ItemPlacement> = Vec::with_capacity(child_count);
340    for (index, child_node) in tree.child_ids(node).enumerate() {
341        let child_style = tree.get_grid_child_style(child_node);
342        if child_style.box_generation_mode() == BoxGenerationMode::None || child_style.position().is_out_of_flow() {
343            continue;
344        }
345        placements.push(InBothAbsAxis {
346            horizontal: name_resolver
347                .resolve_column_names(&child_style.grid_column())
348                .into_origin_zero(explicit_col_count),
349            vertical: name_resolver.resolve_row_names(&child_style.grid_row()).into_origin_zero(explicit_row_count),
350        });
351        items.push(GridItem::new_with_style_and_order(
352            child_node,
353            child_style,
354            align_items,
355            justify_items,
356            index as u16,
357        ));
358    }
359
360    // 4. Implicit Grid: Estimate Track Counts
361    // Estimate the number of rows and columns in the implicit grid (= the entire grid)
362    // This is necessary as part of placement. Doing it early here is a perf optimisation to reduce allocations.
363    let (est_col_counts, est_row_counts) =
364        compute_grid_size_estimate(explicit_col_count, explicit_row_count, &placements);
365
366    // 5. Grid Item Placement
367    // Match items (children) to a definite grid position (row start/end and column start/end position)
368    let mut cell_occupancy_matrix = CellOccupancyMatrix::with_track_counts(est_col_counts, est_row_counts);
369    // `items` is in document order from here on (placement only fills in each item's grid area). The track
370    // sizing and baseline passes sort references to the items rather than the items themselves.
371    place_grid_items(&mut cell_occupancy_matrix, &mut items, &placements, style.grid_auto_flow());
372
373    // Extract track counts from previous step (auto-placement can expand the number of tracks)
374    let final_col_counts = *cell_occupancy_matrix.track_counts(AbsoluteAxis::Horizontal);
375    let final_row_counts = *cell_occupancy_matrix.track_counts(AbsoluteAxis::Vertical);
376
377    // 5. Initialize Tracks
378    // Initialize (explicit and implicit) grid tracks (and gutters)
379    // This resolves the min and max track sizing functions for all tracks and gutters
380    let mut columns = GridTrackVec::new();
381    let mut rows = GridTrackVec::new();
382    initialize_grid_tracks(
383        &mut columns,
384        final_col_counts,
385        &style,
386        AbsoluteAxis::Horizontal,
387        col_auto_repetition_count,
388        |column_index| cell_occupancy_matrix.column_is_occupied(column_index),
389    );
390    initialize_grid_tracks(
391        &mut rows,
392        final_row_counts,
393        &style,
394        AbsoluteAxis::Vertical,
395        row_auto_repetition_count,
396        |row_index| cell_occupancy_matrix.row_is_occupied(row_index),
397    );
398
399    drop(grid_template_rows);
400    drop(grid_template_columns);
401    drop(grid_auto_rows);
402    drop(grid_auto_columns);
403    drop(style);
404
405    // 6. Track Sizing
406
407    // Convert grid placements in origin-zero coordinates to indexes into the GridTrack (rows and columns) vectors
408    // This computation is relatively trivial, but it requires the final number of negative (implicit) tracks in
409    // each axis, and doing it up-front here means we don't have to keep repeating that calculation
410    resolve_item_track_indexes(&mut items, final_col_counts, final_row_counts);
411    // For each item, and in each axis, determine whether the item crosses any flexible (fr) tracks
412    // Record this as a boolean (per-axis) on each item for later use in the track-sizing algorithm
413    determine_if_item_crosses_flexible_or_intrinsic_tracks(&mut items, &columns, &rows);
414
415    // Determine if the grid has any baseline aligned items
416    let has_baseline_aligned_item = items.iter().any(|item| item.participates_in_baseline_alignment());
417
418    // Run track sizing algorithm for Inline axis
419    track_sizing_algorithm(
420        tree,
421        AbstractAxis::Inline,
422        inner_min_size.get(AbstractAxis::Inline),
423        inner_max_size.get(AbstractAxis::Inline),
424        justify_content,
425        align_content,
426        available_grid_space,
427        inner_node_size,
428        &mut columns,
429        &mut rows,
430        &mut items,
431        |track: &GridTrack, parent_size: Option<f32>, tree: &Tree| {
432            track.max_track_sizing_function.definite_value(parent_size, |val, basis| tree.calc(val, basis))
433        },
434        has_baseline_aligned_item,
435    );
436    let initial_column_sum = columns.iter().map(|track| track.base_size).sum::<f32>();
437    inner_node_size.width = inner_node_size.width.or_else(|| initial_column_sum.into());
438
439    // If only the container's width has been requested then we can skip sizing the rows entirely (which avoids
440    // measuring the height of every item), as column sizing is not re-run when only computing the container's size.
441    // Note: the short-circuit above has already handled the case where the container's width is known.
442    if run_mode == RunMode::ComputeSize && inputs.axis == RequestedAxis::Horizontal {
443        let width = (initial_column_sum + content_box_inset.horizontal_axis_sum())
444            .maybe_clamp(min_size.width, max_size.width)
445            .max(padding_border_size.width);
446        return LayoutOutput::from_outer_size(Size { width, height: 0.0 });
447    }
448
449    items.iter_mut().for_each(|item| item.grid_area_size_cache = None);
450
451    // Run track sizing algorithm for Block axis
452    track_sizing_algorithm(
453        tree,
454        AbstractAxis::Block,
455        inner_min_size.get(AbstractAxis::Block),
456        inner_max_size.get(AbstractAxis::Block),
457        align_content,
458        justify_content,
459        available_grid_space,
460        inner_node_size,
461        &mut rows,
462        &mut columns,
463        &mut items,
464        |track: &GridTrack, _, _| Some(track.base_size),
465        false, // TODO: Support baseline alignment in the vertical axis
466    );
467    let initial_row_sum = rows.iter().map(|track| track.base_size).sum::<f32>();
468
469    debug_log!("initial_column_sum", dbg:initial_column_sum);
470    debug_log!(dbg: columns.iter().map(|track| track.base_size).collect::<Vec<_>>());
471    debug_log!("initial_row_sum", dbg:initial_row_sum);
472    debug_log!(dbg: rows.iter().map(|track| track.base_size).collect::<Vec<_>>());
473
474    // 6. Compute container size
475    let resolved_style_size = known_dimensions.or(preferred_size);
476    let mut container_border_box = Size {
477        width: resolved_style_size
478            .get(AbstractAxis::Inline)
479            .unwrap_or_else(|| initial_column_sum + content_box_inset.horizontal_axis_sum())
480            .maybe_clamp(min_size.width, max_size.width)
481            .max(padding_border_size.width),
482        height: resolved_style_size
483            .get(AbstractAxis::Block)
484            .unwrap_or_else(|| initial_row_sum + content_box_inset.vertical_axis_sum())
485            .maybe_clamp(min_size.height, max_size.height)
486            .max(padding_border_size.height),
487    };
488    let mut container_content_box = Size {
489        width: f32_max(0.0, container_border_box.width - content_box_inset.horizontal_axis_sum()),
490        height: f32_max(0.0, container_border_box.height - content_box_inset.vertical_axis_sum()),
491    };
492
493    // If only the container's size has been requested
494    if run_mode == RunMode::ComputeSize {
495        return LayoutOutput::from_outer_size(container_border_box);
496    }
497
498    // The container's size is now determined, so percentages resolve against its content box when re-running track sizing.
499    // This may be a no-op if the container's size was known upfront (in which case it already equals `inner_node_size`).
500    inner_node_size = container_content_box.map(Some);
501
502    // Column sizing must be re-run (once) if:
503    //   - The grid container's width was initially indefinite and there are any columns with percentage track sizing functions
504    //   - Any grid item crossing an intrinsically sized track's min content contribution width has changed
505    // TODO: Only rerun sizing for tracks that actually require it rather than for all tracks if any need it.
506    let mut rerun_column_sizing;
507    let mut intrinsic_column_contribution_changed = false;
508
509    let has_percentage_column = columns.iter().any(|track| track.uses_percentage());
510    let has_percentage_row = rows.iter().any(|track| track.uses_percentage());
511    let parent_width_indefinite = outer_node_size.width.is_none();
512    rerun_column_sizing = parent_width_indefinite && has_percentage_column;
513
514    if !rerun_column_sizing {
515        // Note: every item must be visited (no short-circuiting) as the closure updates each item's caches
516        intrinsic_column_contribution_changed =
517            items.iter_mut().filter(|item| item.crosses_intrinsic_column).fold(false, |any_changed, item| {
518                let grid_area_size = item.grid_area_size(
519                    AbstractAxis::Inline,
520                    &columns,
521                    &rows,
522                    inner_node_size,
523                    |track: &GridTrack, _| Some(track.base_size),
524                    &|val, basis| tree.calc(val, basis),
525                );
526                let available_space = grid_area_size.with(AbstractAxis::Inline, None);
527                let new_min_content_contribution =
528                    item.min_content_contribution(AbstractAxis::Inline, tree, grid_area_size, available_space);
529
530                let has_changed = Some(new_min_content_contribution) != item.min_content_contribution_cache.width;
531
532                item.grid_area_size_cache = Some(grid_area_size);
533                item.min_content_contribution_cache.width = Some(new_min_content_contribution);
534                item.max_content_contribution_cache.width = None;
535                item.minimum_contribution_cache.width = None;
536
537                any_changed | has_changed
538            });
539        rerun_column_sizing = intrinsic_column_contribution_changed;
540    } else {
541        // Clear intrinsic width caches
542        items.iter_mut().for_each(|item| {
543            item.grid_area_size_cache = None;
544            item.min_content_contribution_cache.width = None;
545            item.max_content_contribution_cache.width = None;
546            item.minimum_contribution_cache.width = None;
547        });
548    }
549
550    // Row sizing must be re-run (once) if:
551    //   - The grid container's height was initially indefinite and there are any rows with percentage track sizing functions
552    //   - Column sizing was re-run and any grid item crossing an intrinsically sized track's min content contribution height has changed
553    // TODO: Only rerun sizing for tracks that actually require it rather than for all tracks if any need it.
554    let parent_height_indefinite = outer_node_size.height.is_none();
555    let mut rerun_row_sizing = parent_height_indefinite && has_percentage_row;
556    let mut intrinsic_row_contribution_changed = false;
557
558    if rerun_row_sizing {
559        // Clear intrinsic height caches
560        items.iter_mut().for_each(|item| {
561            item.grid_area_size_cache = None;
562            item.min_content_contribution_cache.height = None;
563            item.max_content_contribution_cache.height = None;
564            item.minimum_contribution_cache.height = None;
565        });
566    }
567
568    if rerun_column_sizing {
569        // Re-run track sizing algorithm for Inline axis
570        track_sizing_algorithm(
571            tree,
572            AbstractAxis::Inline,
573            inner_min_size.get(AbstractAxis::Inline),
574            inner_max_size.get(AbstractAxis::Inline),
575            justify_content,
576            align_content,
577            available_grid_space,
578            inner_node_size,
579            &mut columns,
580            &mut rows,
581            &mut items,
582            |track: &GridTrack, _, _| Some(track.base_size),
583            has_baseline_aligned_item,
584        );
585
586        // The column widths may have changed, so the items' intrinsic height contributions need to be recomputed
587        // (unless row sizing is being re-run anyway, in which case the caches have already been cleared).
588        if !rerun_row_sizing {
589            // Note: every item must be visited (no short-circuiting) as the closure updates each item's caches
590            intrinsic_row_contribution_changed =
591                items.iter_mut().filter(|item| item.crosses_intrinsic_row).fold(false, |any_changed, item| {
592                    let grid_area_size = item.grid_area_size(
593                        AbstractAxis::Block,
594                        &rows,
595                        &columns,
596                        inner_node_size,
597                        |track: &GridTrack, _| Some(track.base_size),
598                        &|val, basis| tree.calc(val, basis),
599                    );
600                    let available_space = grid_area_size.with(AbstractAxis::Block, None);
601                    let new_min_content_contribution =
602                        item.min_content_contribution(AbstractAxis::Block, tree, grid_area_size, available_space);
603
604                    let has_changed = Some(new_min_content_contribution) != item.min_content_contribution_cache.height;
605
606                    item.grid_area_size_cache = Some(grid_area_size);
607                    item.min_content_contribution_cache.height = Some(new_min_content_contribution);
608                    item.max_content_contribution_cache.height = None;
609                    item.minimum_contribution_cache.height = None;
610
611                    any_changed | has_changed
612                });
613            rerun_row_sizing = intrinsic_row_contribution_changed;
614        }
615    }
616
617    if rerun_row_sizing {
618        // Re-run track sizing algorithm for Block axis
619        track_sizing_algorithm(
620            tree,
621            AbstractAxis::Block,
622            inner_min_size.get(AbstractAxis::Block),
623            inner_max_size.get(AbstractAxis::Block),
624            align_content,
625            justify_content,
626            available_grid_space,
627            inner_node_size,
628            &mut rows,
629            &mut columns,
630            &mut items,
631            |track: &GridTrack, _, _| Some(track.base_size),
632            false, // TODO: Support baseline alignment in the vertical axis
633        );
634    }
635
636    if (intrinsic_column_contribution_changed && !has_percentage_column)
637        || (intrinsic_row_contribution_changed && !has_percentage_row)
638    {
639        let final_column_sum = columns.iter().map(|track| track.base_size).sum::<f32>();
640        let final_row_sum = rows.iter().map(|track| track.base_size).sum::<f32>();
641
642        if intrinsic_column_contribution_changed && !has_percentage_column {
643            container_border_box.width = resolved_style_size
644                .get(AbstractAxis::Inline)
645                .unwrap_or_else(|| final_column_sum + content_box_inset.horizontal_axis_sum())
646                .maybe_clamp(min_size.width, max_size.width)
647                .max(padding_border_size.width);
648            container_content_box.width =
649                f32_max(0.0, container_border_box.width - content_box_inset.horizontal_axis_sum());
650        }
651
652        if intrinsic_row_contribution_changed && !has_percentage_row {
653            container_border_box.height = resolved_style_size
654                .get(AbstractAxis::Block)
655                .unwrap_or_else(|| final_row_sum + content_box_inset.vertical_axis_sum())
656                .maybe_clamp(min_size.height, max_size.height)
657                .max(padding_border_size.height);
658            container_content_box.height =
659                f32_max(0.0, container_border_box.height - content_box_inset.vertical_axis_sum());
660        }
661    }
662
663    // If only the container's size has been requested
664    if run_mode == RunMode::ComputeSize {
665        return LayoutOutput::from_outer_size(container_border_box);
666    }
667
668    // 8. Track Alignment
669
670    // Align columns
671    let inline_size_without_scrollbar = f32_max(container_border_box.width - padding_border_size.width, 0.0);
672    let inline_scrollbar_gutter_for_alignment = f32_min(scrollbar_gutter.x, inline_size_without_scrollbar);
673    align_tracks(
674        container_content_box.get(AbstractAxis::Inline),
675        Line {
676            start: padding.left + if direction.is_rtl() { inline_scrollbar_gutter_for_alignment } else { 0.0 },
677            end: padding.right + if direction.is_rtl() { 0.0 } else { inline_scrollbar_gutter_for_alignment },
678        },
679        Line { start: border.left, end: border.right },
680        &mut columns,
681        justify_content,
682        direction.is_rtl(),
683    );
684    // Align rows
685    align_tracks(
686        container_content_box.get(AbstractAxis::Block),
687        Line { start: padding.top, end: padding.bottom },
688        Line { start: border.top, end: border.bottom },
689        &mut rows,
690        align_content,
691        false,
692    );
693
694    // 9. Size, Align, and Position Grid Items
695
696    #[cfg_attr(not(feature = "content_size"), allow(unused_mut))]
697    let mut item_overflow_rect = Rect::ZERO;
698
699    // Out-of-flow candidates in document order: direct out-of-flow children of the grid
700    // interleaved with candidates bubbled from in-flow children's subtrees. These are laid out
701    // by the out-of-flow positioning pass (`compute_oof_layout`), which runs after this algorithm.
702    let mut oof_candidates = OofCandidates::new();
703
704    let container_alignment_styles = InBothAbsAxis { horizontal: justify_items, vertical: align_items };
705
706    // Position in-flow children (stored in items vector)
707    for (index, item) in items.iter_mut().enumerate() {
708        // Tracks are stored in logical order. In RTL the physical offsets are assigned
709        // right-to-left, so an item's physical left edge is derived from its logical end
710        // line and its physical right edge from its logical start line.
711        let grid_area = Rect {
712            top: rows[item.row_indexes.start as usize + 1].offset,
713            bottom: rows[item.row_indexes.end as usize].offset,
714            left: if direction.is_rtl() {
715                columns[item.column_indexes.end as usize - 1].offset
716            } else {
717                columns[item.column_indexes.start as usize + 1].offset
718            },
719            right: if direction.is_rtl() {
720                columns[item.column_indexes.start as usize].offset
721            } else {
722                columns[item.column_indexes.end as usize].offset
723            },
724        };
725        #[cfg_attr(not(feature = "content_size"), allow(unused_variables))]
726        let (overflow_contribution, y_position, height, baseline) = align_and_position_item(
727            tree,
728            item.node,
729            index as u32,
730            grid_area,
731            Size { width: grid_area.right - grid_area.left, height: grid_area.bottom - grid_area.top },
732            container_alignment_styles,
733            item.baseline_shim,
734            direction,
735            container_border_box.width,
736            border,
737            #[cfg(feature = "content_size")]
738            is_scroll_container,
739            &mut item.oof_candidates,
740        );
741        item.y_position = y_position;
742        item.height = height;
743        item.baseline = baseline;
744
745        #[cfg(feature = "content_size")]
746        {
747            item_overflow_rect = item_overflow_rect.union(overflow_contribution);
748        }
749    }
750
751    // Position hidden and absolutely positioned children, merging in the candidates bubbled out
752    // of in-flow children's subtrees (`items` is sorted by `source_order`, i.e. child index)
753    let mut order = items.len() as u32;
754    let mut in_flow_items = items.iter_mut().peekable();
755    (0..tree.child_count(node)).for_each(|index| {
756        if let Some(item) = in_flow_items.next_if(|item| item.source_order as usize == index) {
757            oof_candidates.append(&mut item.oof_candidates);
758            return;
759        }
760
761        let child = tree.get_child_id(node, index);
762        let child_style = tree.get_grid_child_style(child);
763
764        // Position hidden child
765        if child_style.box_generation_mode() == BoxGenerationMode::None {
766            drop(child_style);
767            tree.set_unrounded_layout(child, &Layout::with_order(order));
768            tree.perform_child_layout(
769                child,
770                Size::NONE,
771                Size::NONE,
772                Size::MAX_CONTENT,
773                SizingMode::InherentSize,
774                Line::FALSE,
775            );
776            order += 1;
777            return;
778        }
779
780        // Emit out-of-flow children as candidates for the out-of-flow positioning pass
781        // (`compute_oof_layout`), which lays out those for which this grid is the containing
782        // block after this algorithm has run (resolving their grid areas from the detailed grid
783        // info recorded below).
784        if child_style.position().is_out_of_flow() {
785            let position = child_style.position();
786            let item_direction = child_style.direction();
787            let justify_self = child_style.justify_self().unwrap_or(justify_items).resolve_self_relative(
788                item_direction,
789                direction,
790                true,
791            );
792            let align_self =
793                child_style.align_self().unwrap_or(align_items).resolve_self_relative(item_direction, direction, false);
794
795            // The static-position rectangle: the grid area determined by the grid-placement
796            // properties when this grid is the child's containing block, and otherwise the
797            // grid's content box, as if the child were the sole grid item in a grid area
798            // coinciding with the grid's content edges (grid placement does not apply when the
799            // grid is not the containing block).
800            // https://www.w3.org/TR/css-grid-1/#static-position
801            let area = if containing_block_claims.for_position(position) {
802                resolve_static_position_grid_area(
803                    &child_style,
804                    &name_resolver,
805                    final_row_counts,
806                    final_col_counts,
807                    &rows,
808                    &columns,
809                    direction,
810                    border,
811                    scrollbar_gutter,
812                    container_border_box,
813                )
814            } else {
815                Rect {
816                    left: border.left + padding.left + if direction.is_rtl() { scrollbar_gutter.x } else { 0.0 },
817                    right: container_border_box.width
818                        - border.right
819                        - padding.right
820                        - if direction.is_rtl() { 0.0 } else { scrollbar_gutter.x },
821                    top: border.top + padding.top,
822                    bottom: container_border_box.height - border.bottom - padding.bottom - scrollbar_gutter.y,
823                }
824            };
825            drop(child_style);
826
827            oof_candidates.push(OofCandidate {
828                node: child,
829                order,
830                position,
831                static_position: Point {
832                    x: AxisStaticPosition::from_alignment(
833                        justify_self,
834                        Line { start: area.left, end: area.right },
835                        direction.is_rtl(),
836                    ),
837                    y: AxisStaticPosition::from_alignment(
838                        align_self,
839                        Line { start: area.top, end: area.bottom },
840                        false,
841                    ),
842                },
843            });
844
845            order += 1;
846        }
847    });
848
849    let absolute_position_inset = border
850        + Rect {
851            left: if direction.is_rtl() { scrollbar_gutter.x } else { 0.0 },
852            right: if direction.is_rtl() { 0.0 } else { scrollbar_gutter.x },
853            top: 0.0,
854            bottom: scrollbar_gutter.y,
855        };
856    let absolute_position_area = container_border_box - absolute_position_inset.sum_axes();
857    let absolute_position_offset = Point { x: absolute_position_inset.left, y: absolute_position_inset.top };
858    // Store the detailed grid info before the out-of-flow positioning pass so that the pass can
859    // resolve the grid areas of out-of-flow boxes whose containing block is this grid
860    let areas =
861        name_resolver.populate_detailed_line_resolvers(&mut detailed_row_line_names, &mut detailed_column_line_names);
862    tree.set_detailed_grid_info(
863        node,
864        DetailedGridInfo {
865            rows: DetailedGridTracksInfo::from_grid_tracks_and_track_count(
866                final_row_counts,
867                rows,
868                detailed_row_line_names,
869            ),
870            columns: DetailedGridTracksInfo::from_grid_tracks_and_track_count(
871                final_col_counts,
872                columns,
873                detailed_column_line_names,
874            ),
875            items: items.iter().map(DetailedGridItemsInfo::from_grid_item).collect(),
876            areas,
877        },
878    );
879
880    let oof_positioning_area =
881        Some(OofPositioningArea { size: absolute_position_area, offset: absolute_position_offset });
882
883    // If there are no in-flow items then return the container size and the overflow (no baseline)
884    if items.is_empty() {
885        #[cfg(feature = "content_size")]
886        let mut output = {
887            let mut overflow_rect = item_overflow_rect;
888            if is_scroll_container {
889                overflow_rect.right += if direction.is_rtl() { padding.left } else { padding.right };
890                overflow_rect.bottom += padding.bottom;
891            }
892            LayoutOutput::from_sizes(container_border_box, overflow_rect)
893        };
894        #[cfg(not(feature = "content_size"))]
895        let mut output = LayoutOutput::from_outer_size(container_border_box);
896        output.oof_candidates = oof_candidates;
897        output.oof_positioning_area = oof_positioning_area;
898        return output;
899    }
900
901    // Determine the grid container baseline(s) (currently we only compute the first baseline)
902    // Layout containment suppresses the box's baseline for baseline-alignment purposes
903    let grid_container_baseline: Option<f32> = if contain.suppresses_baseline() {
904        None
905    } else {
906        // Get the row index of the first row containing items
907        let first_row = items.iter().map(|item| item.row_indexes.start).min().unwrap();
908
909        // Check if any items in *this row* participate in baseline alignment
910        // (items with an auto block-axis margin do not participate: https://www.w3.org/TR/css-align-3/#baseline-align-self)
911        let mut first_row_items = items.iter().filter(|item| item.row_indexes.start == first_row);
912        let first_item = first_row_items.next().unwrap();
913        let item = if first_item.participates_in_baseline_alignment() {
914            first_item
915        } else {
916            first_row_items.find(|item| item.participates_in_baseline_alignment()).unwrap_or(first_item)
917        };
918
919        Some(item.y_position + item.baseline.unwrap_or(item.height))
920    };
921
922    // A scroll container's own padding at the end of the content is part of its scrollable
923    // overflow region, so it is included in the in-flow overflow rect. Boxes that are not
924    // scroll containers do not extend their overflow region by their own padding.
925    #[cfg(feature = "content_size")]
926    let scrollable_overflow_rect = {
927        let mut overflow_rect = item_overflow_rect;
928        if is_scroll_container {
929            overflow_rect.right += if direction.is_rtl() { padding.left } else { padding.right };
930            overflow_rect.bottom += padding.bottom;
931        }
932        overflow_rect
933    };
934    #[cfg(not(feature = "content_size"))]
935    let scrollable_overflow_rect = item_overflow_rect;
936
937    let mut output = LayoutOutput::from_sizes_and_baselines(
938        container_border_box,
939        scrollable_overflow_rect,
940        Baselines::from_first(grid_container_baseline),
941    );
942    output.oof_candidates = oof_candidates;
943    output.oof_positioning_area = oof_positioning_area;
944    output
945}
946
947/// Resolve the static-position rectangle (the grid area determined by the grid-placement
948/// properties) for an out-of-flow child of a positioned grid container.
949///
950/// Content alignment (align-content/justify-content) may distribute free space before, between,
951/// or after tracks. Grid lines used by absolutely positioned items resolve to the edges of the
952/// tracks adjacent to the line rather than to the raw gutter offset:
953///   - As a start edge, a line resolves to the start of the track that follows it
954///   - As an end edge, a line resolves to the end of the track that precedes it
955#[allow(clippy::too_many_arguments)]
956fn resolve_static_position_grid_area<S: CheapCloneStr>(
957    child_style: &impl GridItemStyle<CustomIdent = S>,
958    name_resolver: &NamedLineResolver<S>,
959    final_row_counts: TrackCounts,
960    final_col_counts: TrackCounts,
961    rows: &[GridTrack],
962    columns: &[GridTrack],
963    direction: Direction,
964    border: Rect<f32>,
965    scrollbar_gutter: Point<f32>,
966    container_border_box: Size<f32>,
967) -> Rect<f32> {
968    // Convert grid-col-{start/end} into Option's of indexes into the columns vector
969    // The Option is None if the style property is Auto and an unresolvable Span
970    let maybe_col_indexes = name_resolver
971        .resolve_column_names(&child_style.grid_column())
972        .into_origin_zero(final_col_counts.explicit)
973        .resolve_absolutely_positioned_grid_tracks()
974        .map(|maybe_grid_line| {
975            maybe_grid_line.and_then(|line: OriginZeroLine| line.try_into_track_vec_index(final_col_counts))
976        });
977    // Convert grid-row-{start/end} into Option's of indexes into the row vector
978    // The Option is None if the style property is Auto and an unresolvable Span
979    let maybe_row_indexes = name_resolver
980        .resolve_row_names(&child_style.grid_row())
981        .into_origin_zero(final_row_counts.explicit)
982        .resolve_absolutely_positioned_grid_tracks()
983        .map(|maybe_grid_line| {
984            maybe_grid_line.and_then(|line: OriginZeroLine| line.try_into_track_vec_index(final_row_counts))
985        });
986
987    /// Resolve a grid line (by track vector index) used as a start edge to a position
988    fn line_as_start_edge(tracks: &[GridTrack], index: usize) -> f32 {
989        tracks.get(index + 1).unwrap_or(&tracks[index]).offset
990    }
991    /// Resolve a grid line (by track vector index) used as an end edge to a position
992    fn line_as_end_edge(tracks: &[GridTrack], index: usize) -> f32 {
993        if index == 0 {
994            tracks.get(1).unwrap_or(&tracks[0]).offset
995        } else {
996            tracks[index].offset
997        }
998    }
999    // In RTL, tracks remain in logical order but physical offsets are assigned
1000    // right-to-left: a line used as an inline-start edge resolves to the physical
1001    // *right* edge of the track that follows it (its gutter's offset), and a line
1002    // used as an inline-end edge resolves to the physical *left* edge (offset) of
1003    // the track that precedes it.
1004    /// Resolve a grid line used as an inline-start edge to a physical right x-position (RTL)
1005    fn rtl_line_as_start_edge(tracks: &[GridTrack], index: usize) -> f32 {
1006        if tracks.len() > index + 1 {
1007            // The gutter's offset is the physical right edge of the track that follows the line
1008            tracks[index].offset
1009        } else if index == 0 {
1010            tracks[0].offset
1011        } else {
1012            // No track follows the line: resolve to the line itself, which is the physical
1013            // left edge of the track that precedes it (the trailing gutter is assigned its
1014            // offset before any alignment offset is applied, so it cannot be used here)
1015            tracks[index - 1].offset
1016        }
1017    }
1018    /// Resolve a grid line used as an inline-end edge to a physical left x-position (RTL)
1019    fn rtl_line_as_end_edge(tracks: &[GridTrack], index: usize) -> f32 {
1020        if index == 0 {
1021            tracks[0].offset
1022        } else {
1023            tracks[index - 1].offset
1024        }
1025    }
1026
1027    // In RTL the item's physical left edge derives from its logical end line and its
1028    // physical right edge from its logical start line.
1029    let (grid_area_left, grid_area_right) = if direction.is_rtl() {
1030        (
1031            maybe_col_indexes
1032                .end
1033                .map(|index| rtl_line_as_end_edge(columns, index))
1034                .unwrap_or(border.left + scrollbar_gutter.x),
1035            maybe_col_indexes
1036                .start
1037                .map(|index| rtl_line_as_start_edge(columns, index))
1038                .unwrap_or(container_border_box.width - border.right),
1039        )
1040    } else {
1041        (
1042            maybe_col_indexes.start.map(|index| line_as_start_edge(columns, index)).unwrap_or(border.left),
1043            maybe_col_indexes
1044                .end
1045                .map(|index| line_as_end_edge(columns, index))
1046                .unwrap_or(container_border_box.width - border.right - scrollbar_gutter.x),
1047        )
1048    };
1049
1050    Rect {
1051        top: maybe_row_indexes.start.map(|index| line_as_start_edge(rows, index)).unwrap_or(border.top),
1052        bottom: maybe_row_indexes
1053            .end
1054            .map(|index| line_as_end_edge(rows, index))
1055            .unwrap_or(container_border_box.height - border.bottom - scrollbar_gutter.y),
1056        left: grid_area_left,
1057        right: grid_area_right,
1058    }
1059}
1060
1061/// Information from the computation of grid
1062#[derive(Debug, Clone, PartialEq)]
1063pub struct DetailedGridInfo<S: CheapCloneStr = DefaultCheapStr> {
1064    /// <https://drafts.csswg.org/css-grid-1/#grid-row>
1065    pub rows: DetailedGridTracksInfo<S>,
1066    /// <https://drafts.csswg.org/css-grid-1/#grid-column>
1067    pub columns: DetailedGridTracksInfo<S>,
1068    /// <https://drafts.csswg.org/css-grid-1/#grid-items>
1069    pub items: Vec<DetailedGridItemsInfo>,
1070    /// Grid area edge lookup used to resolve the grid placements of absolutely positioned children
1071    areas: GridAreasMap<S>,
1072}
1073
1074impl<S: CheapCloneStr> DetailedGridTracksInfo<S> {
1075    /// Resolve an absolute placement in this axis to physical start and end coordinates
1076    fn resolve_absolute_grid_axis(
1077        &self,
1078        placement: Line<GridPlacement<S>>,
1079        areas: &GridAreasMap<S>,
1080        axis: GridAreaAxis,
1081        padding_start: f32,
1082        padding_end: f32,
1083        is_reversed: bool,
1084    ) -> Line<f32> {
1085        let track_counts = TrackCounts {
1086            negative_implicit: self.negative_implicit_tracks,
1087            explicit: self.explicit_tracks,
1088            positive_implicit: self.positive_implicit_tracks,
1089        };
1090        let min_line = -(track_counts.negative_implicit as i16);
1091        let max_line = (track_counts.explicit + track_counts.positive_implicit) as i16;
1092        // Resolve the placement to a pair of (sorted) lines first, and only then treat lines
1093        // that fall outside the grid as `auto`. A placement whose lines end up swapped (e.g.
1094        // `grid-column: 3 / 1`, or a non-existent named line which resolves past the end of the
1095        // explicit grid) is normalized before the out-of-grid check, so e.g. `foo / 3` on a
1096        // 2-column grid resolves to the area between line 3 and the end padding edge.
1097        let placement = self
1098            .line_names
1099            .resolve_line_names(&placement, areas, axis, self.explicit_tracks)
1100            .into_origin_zero(self.explicit_tracks)
1101            .resolve_absolutely_positioned_grid_tracks()
1102            .map(|line| line.filter(|line| line.0 >= min_line && line.0 <= max_line))
1103            .map(|line| line.and_then(|line| line.try_into_track_vec_index(track_counts).map(|index| index / 2)));
1104        let start_position = placement
1105            .start
1106            .and_then(|line| {
1107                (line < self.positions.len())
1108                    .then(|| self.absolute_track_edge(line, true, is_reversed))
1109                    .or_else(|| self.positions.last().map(|track| if is_reversed { track.start } else { track.end }))
1110                    .or(self.empty_axis_line)
1111            })
1112            .unwrap_or(if is_reversed { padding_end } else { padding_start });
1113        let end_position = placement
1114            .end
1115            .and_then(|line| {
1116                line.checked_sub(1)
1117                    .filter(|&line| line < self.positions.len())
1118                    .map(|line| self.absolute_track_edge(line, false, is_reversed))
1119                    .or_else(|| self.positions.first().map(|track| if is_reversed { track.end } else { track.start }))
1120                    .or(self.empty_axis_line)
1121            })
1122            .unwrap_or(if is_reversed { padding_start } else { padding_end });
1123
1124        // An area whose lines all fall within a collapsed run is zero-sized and anchored at its
1125        // logical start edge (as in Chrome; Gecko anchors it before the shared gutter instead).
1126        if is_reversed {
1127            Line { start: f32_min(start_position, end_position), end: start_position }
1128        } else {
1129            Line { start: start_position, end: f32_max(start_position, end_position) }
1130        }
1131    }
1132
1133    /// Resolve a track edge, excluding gutters that overlap across a collapsed track run.
1134    fn absolute_track_edge(&self, index: usize, is_start: bool, is_reversed: bool) -> f32 {
1135        let range_index = self.collapsed_tracks.partition_point(|range| range.end <= index);
1136        let range = self.collapsed_tracks.get(range_index).filter(|range| range.contains(&index));
1137        let track = range
1138            .and_then(|range| {
1139                let adjacent = if is_start { Some(range.end) } else { range.start.checked_sub(1) };
1140                adjacent.and_then(|index| self.positions.get(index))
1141            })
1142            .unwrap_or(&self.positions[index]);
1143        // Collapsed gutters overlap: a start edge follows the shared gutter, while an end
1144        // edge precedes it.
1145        if is_start != is_reversed {
1146            track.start
1147        } else {
1148            track.end
1149        }
1150    }
1151}
1152
1153impl<S: CheapCloneStr> DetailedGridInfo<S> {
1154    /// Write the used row track sizes and line names to the passed writer in the resolved value
1155    /// format of the `grid-template-rows` property
1156    /// (see <https://www.w3.org/TR/css-grid-1/#resolved-track-list>)
1157    /// Track sizes are divided by `zoom` (the container's effective CSS `zoom`, or `1.0`) to give unzoomed px.
1158    pub fn write_grid_template_rows(&self, out: &mut impl core::fmt::Write, zoom: f32) -> core::fmt::Result {
1159        self.rows.write_track_list(out, zoom)
1160    }
1161
1162    /// Write the used column track sizes and line names to the passed writer in the resolved value
1163    /// format of the `grid-template-columns` property
1164    /// (see <https://www.w3.org/TR/css-grid-1/#resolved-track-list>)
1165    /// Track sizes are divided by `zoom` (the container's effective CSS `zoom`, or `1.0`) to give unzoomed px.
1166    pub fn write_grid_template_columns(&self, out: &mut impl core::fmt::Write, zoom: f32) -> core::fmt::Result {
1167        self.columns.write_track_list(out, zoom)
1168    }
1169
1170    /// Serialize the used row track sizes and line names in the resolved value format of the
1171    /// `grid-template-rows` property (see <https://www.w3.org/TR/css-grid-1/#resolved-track-list>)
1172    /// Track sizes are divided by `zoom` (the container's effective CSS `zoom`, or `1.0`) to give unzoomed px.
1173    pub fn grid_template_rows(&self, zoom: f32) -> String {
1174        self.rows.to_track_list_string(zoom)
1175    }
1176
1177    /// Serialize the used column track sizes and line names in the resolved value format of the
1178    /// `grid-template-columns` property (see <https://www.w3.org/TR/css-grid-1/#resolved-track-list>)
1179    /// Track sizes are divided by `zoom` (the container's effective CSS `zoom`, or `1.0`) to give unzoomed px.
1180    pub fn grid_template_columns(&self, zoom: f32) -> String {
1181        self.columns.to_track_list_string(zoom)
1182    }
1183
1184    /// Resolve the physical grid area for an absolutely positioned box from its grid placement.
1185    /// The padding box and returned area use coordinates relative to the grid container's border box.
1186    pub fn resolve_absolute_grid_area(
1187        &self,
1188        grid_row: Line<GridPlacement<S>>,
1189        grid_column: Line<GridPlacement<S>>,
1190        direction: Direction,
1191        padding_box: Rect<f32>,
1192    ) -> Rect<f32> {
1193        let columns = self.columns.resolve_absolute_grid_axis(
1194            grid_column,
1195            &self.areas,
1196            GridAreaAxis::Column,
1197            padding_box.left,
1198            padding_box.right,
1199            direction.is_rtl(),
1200        );
1201        let rows = self.rows.resolve_absolute_grid_axis(
1202            grid_row,
1203            &self.areas,
1204            GridAreaAxis::Row,
1205            padding_box.top,
1206            padding_box.bottom,
1207            false,
1208        );
1209        Rect { left: columns.start, right: columns.end, top: rows.start, bottom: rows.end }
1210    }
1211
1212    /// Compute the location and size of the grid area occupied by the item at `item_index` (an
1213    /// index into [`DetailedGridInfo::items`]), relative to the grid container's border box.
1214    ///
1215    /// The edges resolve to the edges of the tracks bounding the item's grid area (a start line
1216    /// resolves to the start of the track that follows it and an end line to the end of the track
1217    /// that precedes it), so the area excludes any gutter or content-alignment spacing around it.
1218    ///
1219    /// Returns `None` if `item_index` is out of bounds.
1220    pub fn item_grid_area(&self, item_index: usize) -> Option<(Point<f32>, Size<f32>)> {
1221        let item = self.items.get(item_index)?;
1222        let start_col = self.columns.positions[item.column_start as usize - 1];
1223        let end_col = self.columns.positions[item.column_end as usize - 2];
1224        let left = f32_min(start_col.start, end_col.start);
1225        let right = f32_max(start_col.end, end_col.end);
1226        let top = self.rows.positions[item.row_start as usize - 1].start;
1227        let bottom = self.rows.positions[item.row_end as usize - 2].end;
1228        Some((Point { x: left, y: top }, Size { width: right - left, height: bottom - top }))
1229    }
1230}
1231
1232/// Information from the computation of grids tracks
1233#[derive(Debug, Clone, PartialEq)]
1234pub struct DetailedGridTracksInfo<S: CheapCloneStr = DefaultCheapStr> {
1235    /// Number of leading implicit grid tracks
1236    pub negative_implicit_tracks: u16,
1237    /// Number of explicit grid tracks
1238    pub explicit_tracks: u16,
1239    /// Number of trailing implicit grid tracks
1240    pub positive_implicit_tracks: u16,
1241
1242    /// The start and end position of each track relative to the grid container's border box.
1243    /// These positions account for the container's border and padding, the `gap` property,
1244    /// content alignment (`align-content`/`justify-content`), and collapsed tracks.
1245    pub positions: Vec<Line<f32>>,
1246
1247    /// Sorted, disjoint ranges of collapsed `auto-fit` tracks (indices into `positions`), whose
1248    /// adjacent gutters overlap when resolving absolute grid-area edges. Empty (and never
1249    /// allocated) unless a track collapsed.
1250    pub collapsed_tracks: Vec<Range<usize>>,
1251
1252    /// The position of the axis' single grid line relative to the grid container's border box
1253    /// when the axis has no tracks (an empty grid still contains one grid line in each axis,
1254    /// positioned by content alignment). `None` when the axis has tracks.
1255    pub empty_axis_line: Option<f32>,
1256
1257    /// The names of each *explicit* grid line. Stored line `i` (0-indexed) bounds the start of
1258    /// explicit track `i`; use [`DetailedGridTracksInfo::names_for_line`] or
1259    /// [`DetailedGridTracksInfo::iter_line_names`] for indices relative to the full grid
1260    /// (including implicit tracks). Empty if the grid has no named lines.
1261    pub line_names: GridLineNames<S>,
1262}
1263
1264impl<S: CheapCloneStr> DetailedGridTracksInfo<S> {
1265    /// Get the start and end position of each track relative to the grid container's border box,
1266    /// along with the index ranges of any runs of collapsed `auto-fit` tracks
1267    fn positions_from_grid_track_layout(grid_tracks: &[GridTrack]) -> (Vec<Line<f32>>, Vec<Range<usize>>) {
1268        let mut positions = Vec::with_capacity(grid_tracks.len() / 2);
1269        let mut collapsed_tracks: Vec<Range<usize>> = Vec::new();
1270        for track in grid_tracks.iter().filter(|track| track.kind == GridTrackKind::Track) {
1271            if track.is_collapsed {
1272                let index = positions.len();
1273                match collapsed_tracks.last_mut() {
1274                    Some(range) if range.end == index => range.end += 1,
1275                    _ => collapsed_tracks.push(index..index + 1),
1276                }
1277            }
1278            positions.push(Line { start: track.offset, end: track.offset + track.base_size });
1279        }
1280        (positions, collapsed_tracks)
1281    }
1282
1283    /// Construct DetailedGridTracksInfo from TrackCounts and GridTracks
1284    fn from_grid_tracks_and_track_count(
1285        track_count: TrackCounts,
1286        grid_tracks: Vec<GridTrack>,
1287        line_names: GridLineNames<S>,
1288    ) -> Self {
1289        let (positions, collapsed_tracks) = DetailedGridTracksInfo::<S>::positions_from_grid_track_layout(&grid_tracks);
1290        // An axis with no tracks consists of a single gutter whose offset is where the axis'
1291        // single grid line was positioned by content alignment
1292        let empty_axis_line = if positions.is_empty() { grid_tracks.first().map(|track| track.offset) } else { None };
1293        DetailedGridTracksInfo {
1294            negative_implicit_tracks: track_count.negative_implicit,
1295            explicit_tracks: track_count.explicit,
1296            positive_implicit_tracks: track_count.positive_implicit,
1297            positions,
1298            collapsed_tracks,
1299            empty_axis_line,
1300            line_names,
1301        }
1302    }
1303
1304    /// The names of the grid line with the passed 0-indexed line index, where line `i` bounds
1305    /// the start of track `i` of the full grid (including implicit tracks).
1306    /// Returns an empty slice if the line has no names or the index is out of range.
1307    pub fn names_for_line(&self, line_index: usize) -> &[S] {
1308        match line_index.checked_sub(self.negative_implicit_tracks as usize) {
1309            Some(stored_index) => self.line_names.line(stored_index),
1310            None => &[],
1311        }
1312    }
1313
1314    /// Iterate over the name group (`&[S]`) of each grid line of the full grid (including
1315    /// implicit tracks) in line order, yielding empty groups for unnamed (implicit) lines.
1316    /// Yields nothing if the grid has no named lines.
1317    pub fn iter_line_names(&self) -> GridLineNamesIter<'_, S> {
1318        if self.line_names.is_empty() {
1319            return self.line_names.iter();
1320        }
1321        let total_line_count = self.positions.len() + 1;
1322        let leading_empty = self.negative_implicit_tracks as usize;
1323        let trailing_empty = total_line_count.saturating_sub(leading_empty + self.line_names.line_count());
1324        self.line_names.iter_padded(leading_empty, trailing_empty)
1325    }
1326
1327    /// Write the used track sizes and line names of this axis to the passed writer in the
1328    /// resolved value format of the `grid-template-rows`/`grid-template-columns` properties
1329    /// (see <https://www.w3.org/TR/css-grid-1/#resolved-track-list>)
1330    /// Track sizes are divided by `zoom` (the container's effective CSS `zoom`, or `1.0`) to give unzoomed px.
1331    pub fn write_track_list(&self, out: &mut impl core::fmt::Write, zoom: f32) -> core::fmt::Result {
1332        /// Write a bracketed line name group (e.g. `[foo bar]`)
1333        fn write_line_names<S: CheapCloneStr>(out: &mut impl core::fmt::Write, names: &[S]) -> core::fmt::Result {
1334            out.write_char('[')?;
1335            for (i, name) in names.iter().enumerate() {
1336                if i != 0 {
1337                    out.write_char(' ')?;
1338                }
1339                out.write_str(name.as_ref())?;
1340            }
1341            out.write_char(']')
1342        }
1343
1344        if self.positions.is_empty() {
1345            return out.write_str("none");
1346        }
1347
1348        let mut needs_space = false;
1349        for (track_index, position) in self.positions.iter().enumerate() {
1350            let names = self.names_for_line(track_index);
1351            if !names.is_empty() {
1352                if needs_space {
1353                    out.write_char(' ')?;
1354                }
1355                write_line_names(out, names)?;
1356                needs_space = true;
1357            }
1358            if needs_space {
1359                out.write_char(' ')?;
1360            }
1361            write!(out, "{}px", (position.end - position.start) / zoom)?;
1362            needs_space = true;
1363        }
1364        let trailing_names = self.names_for_line(self.positions.len());
1365        if !trailing_names.is_empty() {
1366            out.write_char(' ')?;
1367            write_line_names(out, trailing_names)?;
1368        }
1369        Ok(())
1370    }
1371
1372    /// Serialize the used track sizes and line names of this axis in the resolved value format of
1373    /// the `grid-template-rows`/`grid-template-columns` properties
1374    /// (see <https://www.w3.org/TR/css-grid-1/#resolved-track-list>)
1375    /// Track sizes are divided by `zoom` (the container's effective CSS `zoom`, or `1.0`) to give unzoomed px.
1376    pub fn to_track_list_string(&self, zoom: f32) -> String {
1377        let mut out = String::new();
1378        self.write_track_list(&mut out, zoom).expect("writing to a String cannot fail");
1379        out
1380    }
1381}
1382
1383/// Grid area information from the placement algorithm
1384///
1385/// The values is 1-indexed grid line numbers bounding the area.
1386/// This matches the Chrome and Firefox's format as of 2nd Jan 2024.
1387#[derive(Debug, Clone, PartialEq)]
1388pub struct DetailedGridItemsInfo {
1389    /// row-start with 1-indexed grid line numbers
1390    pub row_start: u16,
1391    /// row-end with 1-indexed grid line numbers
1392    pub row_end: u16,
1393    /// column-start with 1-indexed grid line numbers
1394    pub column_start: u16,
1395    /// column-end with 1-indexed grid line numbers
1396    pub column_end: u16,
1397}
1398
1399/// Grid area information from the placement algorithm
1400impl DetailedGridItemsInfo {
1401    /// Construct from GridItems
1402    #[inline(always)]
1403    fn from_grid_item(grid_item: &GridItem) -> Self {
1404        /// Conversion from the indexes of Vec<GridTrack> into 1-indexed grid line numbers. See [`GridItem::row_indexes`] or [`GridItem::column_indexes`]
1405        #[inline(always)]
1406        fn to_one_indexed_grid_line(grid_track_index: u16) -> u16 {
1407            grid_track_index / 2 + 1
1408        }
1409
1410        DetailedGridItemsInfo {
1411            row_start: to_one_indexed_grid_line(grid_item.row_indexes.start),
1412            row_end: to_one_indexed_grid_line(grid_item.row_indexes.end),
1413            column_start: to_one_indexed_grid_line(grid_item.column_indexes.start),
1414            column_end: to_one_indexed_grid_line(grid_item.column_indexes.end),
1415        }
1416    }
1417}