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