Skip to main content

taffy/compute/grid/types/
named.rs

1//! Code for resolving name grid lines and areas
2
3use crate::{
4    CheapCloneStr, GenericGridTemplateComponent, GenericRepetition as _, GridAreaAxis, GridAreaEnd, GridContainerStyle,
5    GridPlacement, GridTemplateArea, Line, NonNamedGridPlacement, RepetitionCount,
6};
7use core::{borrow::Borrow, cmp::Ordering, fmt::Debug};
8
9use super::{GridLine, MAX_GRID_TRACKS};
10#[cfg(feature = "detailed_layout_info")]
11use crate::geometry::AbsoluteAxis;
12#[cfg(feature = "detailed_layout_info")]
13use crate::sys::DefaultCheapStr;
14// use alloc::fmt::format;
15use crate::sys::{format, Map, Vec};
16use smallvec::{smallvec, SmallVec};
17
18/// Wrap an `AsRef<str>` type with a type which implements Hash by first
19/// deferring to the underlying `&str`'s implementation of Hash.
20#[derive(Debug, Clone)]
21pub(crate) struct StrHasher<T: CheapCloneStr>(pub T);
22impl<T: CheapCloneStr> PartialOrd for StrHasher<T> {
23    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
24        Some(self.cmp(other))
25    }
26}
27impl<T: CheapCloneStr> Ord for StrHasher<T> {
28    fn cmp(&self, other: &Self) -> Ordering {
29        self.0.as_ref().cmp(other.0.as_ref())
30    }
31}
32impl<T: CheapCloneStr> PartialEq for StrHasher<T> {
33    fn eq(&self, other: &Self) -> bool {
34        other.0.as_ref() == self.0.as_ref()
35    }
36}
37impl<T: CheapCloneStr> Eq for StrHasher<T> {}
38#[cfg(feature = "std")]
39impl<T: CheapCloneStr> std::hash::Hash for StrHasher<T> {
40    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
41        self.0.as_ref().hash(state)
42    }
43}
44impl<T: CheapCloneStr> Borrow<str> for StrHasher<T> {
45    fn borrow(&self) -> &str {
46        self.0.as_ref()
47    }
48}
49
50/// The one-indexed line positions of a single grid line name. Inline capacity of 4 keeps the
51/// type the same size as `Vec<u32>` (24 bytes) while avoiding a heap allocation for names
52/// mapping to at most 4 lines (the common case)
53pub(crate) type LinePositions = SmallVec<[u32; 4]>;
54
55/// Map from a grid line name to its one-indexed line positions
56type NamedGridLinesMap<S> = Map<StrHasher<S>, LinePositions>;
57
58/// Resolver for named placements in one grid axis
59struct NamedLineResolverAxis<'a, S: CheapCloneStr> {
60    /// Named lines and their one-indexed positions
61    lines: &'a NamedGridLinesMap<S>,
62    /// Number of explicit tracks in this axis
63    explicit_track_count: u16,
64}
65
66/// Resolver that takes grid lines names and area names as input and can then be used to
67/// resolve line names of grid placement properties into line numbers.
68pub(crate) struct NamedLineResolver<S: CheapCloneStr> {
69    /// Map of row line names to line numbers. Each line name may correspond to multiple lines
70    /// so we store a `SmallVec`
71    row_lines: NamedGridLinesMap<S>,
72    /// Map of column line names to line numbers. Each line name may correspond to multiple lines
73    /// so we store a `SmallVec`
74    column_lines: NamedGridLinesMap<S>,
75    /// Map of area names to area definitions (start and end lines numbers in each axis)
76    areas: Map<StrHasher<S>, GridTemplateArea<S>>,
77    /// Number of columns implied by grid area definitions
78    area_column_count: u16,
79    /// Number of rows implied by grid area definitions
80    area_row_count: u16,
81    /// The number of explicit columns in the grid. This is an *input* to the `NamedLineResolver` and is
82    /// used when computing the fallback line when a non-existent named line is specified.
83    explicit_column_count: u16,
84    /// The number of explicit rows in the grid. This is an *input* to the `NamedLineResolver` and is
85    /// used when computing the fallback line when a non-existent named line is specified.
86    explicit_row_count: u16,
87    /// The (1-indexed line number, name) pairs of every named column line, in source order
88    /// (template names before `grid-template-areas`-generated names)
89    #[cfg(feature = "detailed_layout_info")]
90    column_line_name_pairs: Vec<(u32, S)>,
91    /// The (1-indexed line number, name) pairs of every named row line, in source order
92    /// (template names before `grid-template-areas`-generated names)
93    #[cfg(feature = "detailed_layout_info")]
94    row_line_name_pairs: Vec<(u32, S)>,
95}
96
97/// Utility function to create or update an entry in a line name map
98fn upsert_line_name_map<S: CheapCloneStr>(map: &mut NamedGridLinesMap<S>, key: S, value: u32) {
99    map.entry(StrHasher(key)).and_modify(|lines| lines.push(value)).or_insert_with(|| smallvec![value]);
100}
101
102impl<S: CheapCloneStr> NamedLineResolverAxis<'_, S> {
103    /// Resolve named lines and spans into numeric placements
104    fn resolve_line_names(&self, line: &Line<GridPlacement<S>>) -> Line<NonNamedGridPlacement> {
105        let start_holder;
106        let start_line_resolved = if let GridPlacement::NamedLine(name, idx) = &line.start {
107            start_holder =
108                GridPlacement::Line(self.find_line_index(name, *idx as i32, GridAreaEnd::Start, &|lines| lines));
109            &start_holder
110        } else {
111            &line.start
112        };
113
114        let end_holder;
115        let end_line_resolved = if let GridPlacement::NamedLine(name, idx) = &line.end {
116            end_holder = GridPlacement::Line(self.find_line_index(name, *idx as i32, GridAreaEnd::End, &|lines| lines));
117            &end_holder
118        } else {
119            &line.end
120        };
121
122        // If both the *-start and *-end values of its grid-placement properties specify a line, its grid span is implicit.
123        // If it has an explicit span value, its grid span is explicit.
124        // Otherwise, its grid span is automatic:
125        //   - if it is subgridded in that axis, its grid span is determined from its <line-name-list>;
126        //   - otherwise its grid span is 1.
127        //
128        // <https://drafts.csswg.org/css-grid-2/#grid-span>
129        match (&start_line_resolved, &end_line_resolved) {
130            (GridPlacement::Line(start_line), GridPlacement::NamedSpan(name, idx)) => {
131                let normalized_start_line = if start_line.as_i16() > 0 {
132                    start_line.as_i16() as u32
133                } else {
134                    (self.explicit_track_count as i32 + 1 + start_line.as_i16() as i32).max(0) as u32
135                };
136                let end_line = self.find_line_index(name, *idx as i32, GridAreaEnd::End, &|lines| {
137                    let point = lines.partition_point(|line| *line <= normalized_start_line);
138                    &lines[point..]
139                });
140                Line { start: NonNamedGridPlacement::Line(*start_line), end: NonNamedGridPlacement::Line(end_line) }
141            }
142            (GridPlacement::NamedSpan(name, idx), GridPlacement::Line(end_line)) => {
143                let normalized_end_line = if end_line.as_i16() > 0 {
144                    end_line.as_i16() as u32
145                } else {
146                    (self.explicit_track_count as i32 + 1 + end_line.as_i16() as i32).max(0) as u32
147                };
148                let start_line = self.find_line_index(name, *idx as i32, GridAreaEnd::Start, &|lines| {
149                    let point = lines.partition_point(|line| *line < normalized_end_line);
150                    &lines[..point]
151                });
152                Line { start: NonNamedGridPlacement::Line(start_line), end: NonNamedGridPlacement::Line(*end_line) }
153            }
154            (start, end) => Line {
155                start: match start {
156                    GridPlacement::Auto => NonNamedGridPlacement::Auto,
157                    GridPlacement::Line(grid_line) => NonNamedGridPlacement::Line(*grid_line),
158                    GridPlacement::Span(span) => NonNamedGridPlacement::Span(*span),
159                    GridPlacement::NamedSpan(_, _) => NonNamedGridPlacement::Span(1),
160                    _ => unreachable!(),
161                },
162                end: match end {
163                    GridPlacement::Auto => NonNamedGridPlacement::Auto,
164                    GridPlacement::Line(grid_line) => NonNamedGridPlacement::Line(*grid_line),
165                    GridPlacement::Span(span) => NonNamedGridPlacement::Span(*span),
166                    GridPlacement::NamedSpan(_, _) => NonNamedGridPlacement::Span(1),
167                    _ => unreachable!(),
168                },
169            },
170        }
171    }
172
173    /// Resolve the grid line for a named grid line or span
174    fn find_line_index(
175        &self,
176        name: &S,
177        idx: i32,
178        end: GridAreaEnd,
179        filter_lines: &dyn Fn(&[u32]) -> &[u32],
180    ) -> GridLine {
181        let name = name.as_ref();
182        let mut idx = idx;
183        let explicit_track_count = self.explicit_track_count as i32;
184
185        // An index of 0 is used to represent "no index specified".
186        if idx == 0 {
187            idx = 1;
188        }
189
190        fn get_line(lines: &[u32], explicit_track_count: i32, idx: i32) -> i16 {
191            let abs_idx = idx.unsigned_abs() as usize;
192            let line = if abs_idx <= lines.len() {
193                if idx > 0 {
194                    lines[abs_idx - 1] as i64
195                } else {
196                    lines[lines.len() - abs_idx] as i64
197                }
198            } else {
199                let remaining_lines = (abs_idx - lines.len()) as i64 * idx.signum() as i64;
200                if idx > 0 {
201                    explicit_track_count as i64 + 1 + remaining_lines
202                } else {
203                    -(explicit_track_count as i64 + 1 + remaining_lines)
204                }
205            };
206            line.clamp(i16::MIN as i64, i16::MAX as i64) as i16
207        }
208
209        // Lookup lines
210        if let Some(lines) = self.lines.get(name) {
211            return GridLine::from(get_line(filter_lines(lines), explicit_track_count, idx));
212        }
213
214        // TODO: eliminate string allocations
215        let implicit_name = match end {
216            GridAreaEnd::Start => format!("{name}-start"),
217            GridAreaEnd::End => format!("{name}-end"),
218        };
219        if let Some(lines) = self.lines.get(&*implicit_name) {
220            return GridLine::from(get_line(filter_lines(lines), explicit_track_count, idx));
221        }
222
223        // The CSS Grid specification has a weird quirk where it matches non-existent line names
224        // to the first (positive) implicit line in the grid
225        //
226        // We add/subtract 2 to the explicit track count because (in each axis) a grid has one more explicit
227        // grid line than it has tracks. And the fallback line is the line *after* that.
228        //
229        // See: <https://github.com/w3c/csswg-drafts/issues/966#issuecomment-277042153>
230        let line = if idx > 0 {
231            explicit_track_count as i64 + 1 + idx as i64
232        } else {
233            -(explicit_track_count as i64 + 1 + idx as i64)
234        };
235        GridLine::from(line.clamp(i16::MIN as i64, i16::MAX as i64) as i16)
236    }
237}
238
239impl<S: CheapCloneStr> NamedLineResolver<S> {
240    /// Create and initialise a new `NamedLineResolver`
241    pub(crate) fn new(
242        style: &impl GridContainerStyle<CustomIdent = S>,
243        column_auto_repetitions: u16,
244        row_auto_repetitions: u16,
245    ) -> Self {
246        let mut areas: Map<StrHasher<S>, GridTemplateArea<_>> = Map::new();
247        let mut column_lines: NamedGridLinesMap<S> = Map::new();
248        let mut row_lines: NamedGridLinesMap<S> = Map::new();
249
250        #[cfg(feature = "detailed_layout_info")]
251        let mut column_line_name_pairs: Vec<(u32, S)> = Vec::new();
252        #[cfg(feature = "detailed_layout_info")]
253        let mut row_line_name_pairs: Vec<(u32, S)> = Vec::new();
254
255        let mut current_line = 0;
256        if let Some(mut column_tracks) = style.grid_template_columns() {
257            if let Some(column_line_names_iter) = style.grid_template_column_names() {
258                for line_names in column_line_names_iter {
259                    current_line += 1;
260                    for line_name in line_names.into_iter() {
261                        #[cfg(feature = "detailed_layout_info")]
262                        column_line_name_pairs.push((current_line, line_name.clone()));
263                        upsert_line_name_map(&mut column_lines, line_name.clone(), current_line);
264                    }
265
266                    if let Some(GenericGridTemplateComponent::Repeat(repeat)) = column_tracks.next() {
267                        let repeat_count = match repeat.count() {
268                            RepetitionCount::Count(count) => count,
269                            RepetitionCount::AutoFill | RepetitionCount::AutoFit => column_auto_repetitions,
270                        };
271
272                        // Line name sets are positional: set `i` names the `i`th line of each
273                        // repetition, and the final line name set of each repetition collapses
274                        // with the first line name set of the following one. An empty list means
275                        // the repetition's lines are unnamed; any other length must be exactly
276                        // `track_count + 1` (one set per line, including both edge lines).
277                        let line_name_set_count = repeat.lines_names().len() as u32;
278                        let lines_per_repetition = repeat.track_count() as u32;
279                        assert!(
280                            line_name_set_count == 0 || line_name_set_count == lines_per_repetition + 1,
281                            "grid template repetition must have no line name sets or exactly track count + 1 of them ({} tracks but {} line name sets)",
282                            lines_per_repetition,
283                            line_name_set_count,
284                        );
285
286                        for _ in 0..repeat_count {
287                            for (line, line_name_set) in (current_line..).zip(repeat.lines_names()) {
288                                for line_name in line_name_set {
289                                    #[cfg(feature = "detailed_layout_info")]
290                                    column_line_name_pairs.push((line, line_name.clone()));
291                                    upsert_line_name_map(&mut column_lines, line_name.clone(), line);
292                                }
293                            }
294                            current_line += lines_per_repetition;
295
296                            // Names for lines beyond the maximum track limit are never resolvable:
297                            // stop generating them (the explicit grid is clamped to MAX_GRID_TRACKS)
298                            if current_line > MAX_GRID_TRACKS as u32 {
299                                break;
300                            }
301                        }
302                        // Last line name set collapses with following line name set
303                        if repeat_count > 0 {
304                            current_line = current_line.saturating_sub(1);
305                        }
306                    }
307                }
308            }
309        }
310
311        let mut current_line = 0;
312        if let Some(mut row_tracks) = style.grid_template_rows() {
313            if let Some(row_line_names_iter) = style.grid_template_row_names() {
314                for line_names in row_line_names_iter {
315                    current_line += 1;
316                    for line_name in line_names.into_iter() {
317                        #[cfg(feature = "detailed_layout_info")]
318                        row_line_name_pairs.push((current_line, line_name.clone()));
319                        upsert_line_name_map(&mut row_lines, line_name.clone(), current_line);
320                    }
321
322                    if let Some(GenericGridTemplateComponent::Repeat(repeat)) = row_tracks.next() {
323                        let repeat_count = match repeat.count() {
324                            RepetitionCount::Count(count) => count,
325                            RepetitionCount::AutoFill | RepetitionCount::AutoFit => row_auto_repetitions,
326                        };
327
328                        // Line name sets are positional: set `i` names the `i`th line of each
329                        // repetition, and the final line name set of each repetition collapses
330                        // with the first line name set of the following one. An empty list means
331                        // the repetition's lines are unnamed; any other length must be exactly
332                        // `track_count + 1` (one set per line, including both edge lines).
333                        let line_name_set_count = repeat.lines_names().len() as u32;
334                        let lines_per_repetition = repeat.track_count() as u32;
335                        assert!(
336                            line_name_set_count == 0 || line_name_set_count == lines_per_repetition + 1,
337                            "grid template repetition must have no line name sets or exactly track count + 1 of them ({} tracks but {} line name sets)",
338                            lines_per_repetition,
339                            line_name_set_count,
340                        );
341
342                        for _ in 0..repeat_count {
343                            for (line, line_name_set) in (current_line..).zip(repeat.lines_names()) {
344                                for line_name in line_name_set {
345                                    #[cfg(feature = "detailed_layout_info")]
346                                    row_line_name_pairs.push((line, line_name.clone()));
347                                    upsert_line_name_map(&mut row_lines, line_name.clone(), line);
348                                }
349                            }
350                            current_line += lines_per_repetition;
351
352                            // Names for lines beyond the maximum track limit are never resolvable:
353                            // stop generating them (the explicit grid is clamped to MAX_GRID_TRACKS)
354                            if current_line > MAX_GRID_TRACKS as u32 {
355                                break;
356                            }
357                        }
358                        // Last line name set collapses with following line name set
359                        if repeat_count > 0 {
360                            current_line = current_line.saturating_sub(1);
361                        }
362                    }
363                }
364            }
365        }
366        // The size of the area template may be larger than the extents of the named areas
367        // due to unnamed (`.`) cells, so it is taken from the style rather than being derived
368        // from the areas themselves.
369        let area_column_count = style.grid_template_area_column_count();
370        let area_row_count = style.grid_template_area_row_count();
371        if let Some(area_iter) = style.grid_template_areas() {
372            for area in area_iter.into_iter() {
373                // TODO: Investigate eliminating clones
374                areas.insert(StrHasher(area.name.clone()), area.clone());
375
376                let col_start_name = S::from(format!("{}-start", area.name.as_ref()));
377                #[cfg(feature = "detailed_layout_info")]
378                column_line_name_pairs.push((area.column_start as u32, col_start_name.clone()));
379                upsert_line_name_map(&mut column_lines, col_start_name, area.column_start as u32);
380                let col_end_name = S::from(format!("{}-end", area.name.as_ref()));
381                #[cfg(feature = "detailed_layout_info")]
382                column_line_name_pairs.push((area.column_end as u32, col_end_name.clone()));
383                upsert_line_name_map(&mut column_lines, col_end_name, area.column_end as u32);
384                let row_start_name = S::from(format!("{}-start", area.name.as_ref()));
385                #[cfg(feature = "detailed_layout_info")]
386                row_line_name_pairs.push((area.row_start as u32, row_start_name.clone()));
387                upsert_line_name_map(&mut row_lines, row_start_name, area.row_start as u32);
388                let row_end_name = S::from(format!("{}-end", area.name.as_ref()));
389                #[cfg(feature = "detailed_layout_info")]
390                row_line_name_pairs.push((area.row_end as u32, row_end_name.clone()));
391                upsert_line_name_map(&mut row_lines, row_end_name, area.row_end as u32);
392            }
393        }
394
395        // Sort and dedup lines for each column name
396        for lines in column_lines.values_mut() {
397            lines.sort_unstable();
398            lines.dedup();
399        }
400        // Sort and dedup lines for each row name
401        for lines in row_lines.values_mut() {
402            lines.sort_unstable();
403            lines.dedup();
404        }
405
406        Self {
407            area_column_count,
408            area_row_count,
409            explicit_column_count: 0, // Overwritten later
410            explicit_row_count: 0,    // Overwritten later
411            areas,
412            row_lines,
413            column_lines,
414            #[cfg(feature = "detailed_layout_info")]
415            column_line_name_pairs,
416            #[cfg(feature = "detailed_layout_info")]
417            row_line_name_pairs,
418        }
419    }
420
421    /// Build the per-line name groups of the explicit grid in the passed axis as a
422    /// [`GridLineNames`], with `repeat()`s expanded and including the implicit
423    /// `<name>-start`/`<name>-end` names generated by `grid-template-areas`.
424    ///
425    /// Line indices are relative to the explicit grid (line 0 = start of the first explicit track).
426    #[cfg(feature = "detailed_layout_info")]
427    pub(crate) fn detailed_line_names(&self, axis: AbsoluteAxis) -> GridLineNames<S> {
428        let (pairs, explicit_track_count) = match axis {
429            AbsoluteAxis::Horizontal => (&self.column_line_name_pairs, self.explicit_column_count),
430            AbsoluteAxis::Vertical => (&self.row_line_name_pairs, self.explicit_row_count),
431        };
432
433        if pairs.is_empty() {
434            return GridLineNames::default();
435        }
436
437        // Stable sort by line number preserves the source order of names within each line
438        // (template names before area-generated names)
439        let mut sorted_pairs: Vec<&(u32, S)> = pairs.iter().collect();
440        sorted_pairs.sort_by_key(|(line, _)| *line);
441
442        let line_count = explicit_track_count as usize + 1;
443        let mut line_names = GridLineNames::with_capacity(sorted_pairs.len(), line_count + 1);
444        let mut pair_iter = sorted_pairs.into_iter().peekable();
445        for line in 1..=(line_count as u32) {
446            line_names.start_line();
447            while let Some(&&(pair_line, ref name)) = pair_iter.peek() {
448                if pair_line != line {
449                    break;
450                }
451                pair_iter.next();
452                if !line_names.current_line_contains(name.as_ref()) {
453                    line_names.push_name(name.clone());
454                }
455            }
456        }
457        line_names
458    }
459
460    /// Resolve named lines for both the `start` and `end` of a row-axis grid placement
461    #[inline(always)]
462    pub(crate) fn resolve_row_names(&self, line: &Line<GridPlacement<S>>) -> Line<NonNamedGridPlacement> {
463        self.resolve_line_names(line, GridAreaAxis::Row)
464    }
465
466    /// Resolve named lines for both the `start` and `end` of a column-axis grid placement
467    #[inline(always)]
468    pub(crate) fn resolve_column_names(&self, line: &Line<GridPlacement<S>>) -> Line<NonNamedGridPlacement> {
469        self.resolve_line_names(line, GridAreaAxis::Column)
470    }
471
472    /// Resolve named lines for both the `start` and `end` of a grid placement
473    #[inline(always)]
474    pub(crate) fn resolve_line_names(
475        &self,
476        line: &Line<GridPlacement<S>>,
477        axis: GridAreaAxis,
478    ) -> Line<NonNamedGridPlacement> {
479        match axis {
480            GridAreaAxis::Row => {
481                NamedLineResolverAxis { lines: &self.row_lines, explicit_track_count: self.explicit_row_count }
482            }
483            GridAreaAxis::Column => {
484                NamedLineResolverAxis { lines: &self.column_lines, explicit_track_count: self.explicit_column_count }
485            }
486        }
487        .resolve_line_names(line)
488    }
489
490    /// Move the row and column line-name maps into the detailed grid information
491    #[cfg(feature = "detailed_layout_info")]
492    pub(crate) fn populate_detailed_line_resolvers(self, rows: &mut GridLineNames<S>, columns: &mut GridLineNames<S>) {
493        rows.resolver = self.row_lines;
494        columns.resolver = self.column_lines;
495    }
496
497    /// Get the number of columns defined by the grid areas
498    pub(crate) fn area_column_count(&self) -> u16 {
499        self.area_column_count
500    }
501
502    /// Get the number of rows defined by the grid areas
503    pub(crate) fn area_row_count(&self) -> u16 {
504        self.area_row_count
505    }
506
507    /// Set the number of columns in the explicit grid
508    pub(crate) fn set_explicit_column_count(&mut self, count: u16) {
509        self.explicit_column_count = count;
510    }
511
512    /// Set the number of rows in the explicit grid
513    pub(crate) fn set_explicit_row_count(&mut self, count: u16) {
514        self.explicit_row_count = count;
515    }
516}
517
518impl<S: CheapCloneStr> Debug for NamedLineResolver<S> {
519    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
520        writeln!(f, "Grid Areas:")?;
521        for area in self.areas.values() {
522            writeln!(
523                f,
524                "{}: row:{}/{} col: {}/{}",
525                area.name.as_ref(),
526                area.row_start,
527                area.row_end,
528                area.column_start,
529                area.column_end
530            )?;
531        }
532
533        writeln!(f, "Grid Rows:")?;
534        for (name, lines) in self.row_lines.iter() {
535            write!(f, "{}: ", name.0.as_ref())?;
536            for line in lines {
537                write!(f, "{line}  ")?;
538            }
539            writeln!(f)?;
540        }
541
542        writeln!(f, "Grid Columns:")?;
543        for (name, lines) in self.column_lines.iter() {
544            write!(f, "{}: ", name.0.as_ref())?;
545            for line in lines {
546                write!(f, "{line}  ")?;
547            }
548            writeln!(f)?;
549        }
550
551        Ok(())
552    }
553}
554
555/// The names of each explicit grid line in a single axis, stored in CSR (compressed sparse
556/// row) format: a single flat `Vec` of names plus a `Vec` of offsets into it (one more offset
557/// than there are lines). The names of line `i` (0-indexed) are `names[offsets[i]..offsets[i + 1]]`.
558///
559/// Line indices here are relative to the explicit grid;
560/// [`DetailedGridTracksInfo`](crate::DetailedGridTracksInfo) provides accessors with indices
561/// relative to the full grid (including implicit tracks).
562///
563/// Iterate over per-line name groups with [`GridLineNames::iter`], or access a single line's
564/// names with [`GridLineNames::line`]. A grid with no named lines is represented by two empty
565/// `Vec`s (see [`GridLineNames::is_empty`]).
566#[derive(Debug, Clone, PartialEq, Default)]
567#[cfg(feature = "detailed_layout_info")]
568pub struct GridLineNames<S: CheapCloneStr = DefaultCheapStr> {
569    /// The names of every grid line in the axis, concatenated in line order
570    names: Vec<S>,
571    /// Offsets into `names`: line `i`'s names are `names[offsets[i]..offsets[i + 1]]`.
572    /// Either empty (no named lines) or of length `line count + 1`.
573    offsets: Vec<u32>,
574    /// Named line lookup used to resolve arbitrary grid placements
575    resolver: NamedGridLinesMap<S>,
576}
577
578#[cfg(feature = "detailed_layout_info")]
579impl<S: CheapCloneStr> GridLineNames<S> {
580    /// Create an empty `GridLineNames` with pre-allocated capacity
581    pub(crate) fn with_capacity(name_capacity: usize, offset_capacity: usize) -> Self {
582        let mut offsets = Vec::with_capacity(offset_capacity);
583        offsets.push(0);
584        Self { names: Vec::with_capacity(name_capacity), offsets, resolver: Map::new() }
585    }
586
587    /// Start a new (initially empty) line
588    pub(crate) fn start_line(&mut self) {
589        self.offsets.push(self.names.len() as u32);
590    }
591
592    /// Append a name to the current (last) line
593    pub(crate) fn push_name(&mut self, name: S) {
594        self.names.push(name);
595        *self.offsets.last_mut().unwrap() = self.names.len() as u32;
596    }
597
598    /// Whether the current (last) line already contains the passed name
599    pub(crate) fn current_line_contains(&self, name: &str) -> bool {
600        self.line(self.line_count().wrapping_sub(1)).iter().any(|n| n.as_ref() == name)
601    }
602
603    /// Resolve named lines and spans using the retained line-name map
604    pub(crate) fn resolve_line_names(
605        &self,
606        line: &Line<GridPlacement<S>>,
607        explicit_track_count: u16,
608    ) -> Line<NonNamedGridPlacement> {
609        NamedLineResolverAxis { lines: &self.resolver, explicit_track_count }.resolve_line_names(line)
610    }
611
612    /// Whether the axis has any named lines at all
613    pub fn is_empty(&self) -> bool {
614        self.names.is_empty()
615    }
616
617    /// The number of grid lines represented (zero if the grid has no named lines)
618    pub fn line_count(&self) -> usize {
619        self.offsets.len().saturating_sub(1)
620    }
621
622    /// The names of the line with the passed 0-indexed line index.
623    /// Returns an empty slice if the line has no names or the index is out of range.
624    pub fn line(&self, line_index: usize) -> &[S] {
625        match (self.offsets.get(line_index), self.offsets.get(line_index + 1)) {
626            (Some(&start), Some(&end)) => &self.names[start as usize..end as usize],
627            _ => &[],
628        }
629    }
630
631    /// Iterate over the name group (`&[S]`) of each grid line in line order
632    pub fn iter(&self) -> GridLineNamesIter<'_, S> {
633        self.iter_padded(0, 0)
634    }
635
636    /// Iterate over the name group (`&[S]`) of each grid line in line order, additionally
637    /// yielding `leading_empty` empty groups before the stored lines and `trailing_empty`
638    /// empty groups after them (representing unnamed implicit grid lines)
639    pub(crate) fn iter_padded(&self, leading_empty: usize, trailing_empty: usize) -> GridLineNamesIter<'_, S> {
640        GridLineNamesIter { names: &self.names, offsets: self.offsets.windows(2), leading_empty, trailing_empty }
641    }
642}
643
644#[cfg(feature = "detailed_layout_info")]
645impl<'a, S: CheapCloneStr> IntoIterator for &'a GridLineNames<S> {
646    type Item = &'a [S];
647    type IntoIter = GridLineNamesIter<'a, S>;
648    fn into_iter(self) -> Self::IntoIter {
649        self.iter()
650    }
651}
652
653/// Iterator over the per-line name groups of a [`GridLineNames`]. Yields one `&[S]` per grid
654/// line (which is empty for unnamed lines)
655#[derive(Debug, Clone)]
656#[cfg(feature = "detailed_layout_info")]
657pub struct GridLineNamesIter<'a, S: CheapCloneStr> {
658    /// The flat name storage being iterated over
659    names: &'a [S],
660    /// Iterator over adjacent pairs of offsets into `names`
661    offsets: core::slice::Windows<'a, u32>,
662    /// Number of (unnamed, implicit) lines remaining before the stored lines
663    leading_empty: usize,
664    /// Number of (unnamed, implicit) lines remaining after the stored lines
665    trailing_empty: usize,
666}
667
668#[cfg(feature = "detailed_layout_info")]
669impl<'a, S: CheapCloneStr> Iterator for GridLineNamesIter<'a, S> {
670    type Item = &'a [S];
671
672    fn next(&mut self) -> Option<Self::Item> {
673        if self.leading_empty > 0 {
674            self.leading_empty -= 1;
675            return Some(&[]);
676        }
677        if let Some(window) = self.offsets.next() {
678            return Some(&self.names[window[0] as usize..window[1] as usize]);
679        }
680        if self.trailing_empty > 0 {
681            self.trailing_empty -= 1;
682            return Some(&[]);
683        }
684        None
685    }
686
687    fn size_hint(&self) -> (usize, Option<usize>) {
688        let len = self.leading_empty + self.offsets.len() + self.trailing_empty;
689        (len, Some(len))
690    }
691}
692
693#[cfg(feature = "detailed_layout_info")]
694impl<S: CheapCloneStr> ExactSizeIterator for GridLineNamesIter<'_, S> {}
695
696#[cfg(test)]
697mod tests {
698    use super::*;
699    use crate::style::GenericGridPlacement;
700    use crate::sys::DefaultCheapStr;
701    use crate::GridTemplateAreas;
702    use crate::Style;
703
704    fn resolver(explicit_track_count: u16) -> NamedLineResolver<DefaultCheapStr> {
705        let mut resolver = NamedLineResolver::new(&Style::DEFAULT, 0, 0);
706        resolver.set_explicit_column_count(explicit_track_count);
707        resolver
708    }
709
710    fn resolved_start_line(
711        resolver: &NamedLineResolver<DefaultCheapStr>,
712        placement: GridPlacement<DefaultCheapStr>,
713    ) -> i16 {
714        let resolved = resolver.resolve_column_names(&Line { start: placement, end: GridPlacement::Auto });
715        match resolved.start {
716            GenericGridPlacement::Line(line) => line.as_i16(),
717            _ => panic!("expected a resolved line"),
718        }
719    }
720
721    #[test]
722    fn extreme_missing_named_line_indices_do_not_overflow() {
723        let resolver = resolver(10_000);
724        assert_eq!(
725            resolved_start_line(&resolver, GridPlacement::NamedLine(DefaultCheapStr::from("missing"), i16::MAX)),
726            i16::MAX
727        );
728        assert_eq!(
729            resolved_start_line(&resolver, GridPlacement::NamedLine(DefaultCheapStr::from("missing"), i16::MIN)),
730            22_767
731        );
732    }
733
734    #[test]
735    fn large_named_span_does_not_wrap_negative() {
736        let resolver = resolver(10_000);
737        let resolved = resolver.resolve_column_names(&Line {
738            start: GridPlacement::Line(GridLine::from(1)),
739            end: GridPlacement::NamedSpan(DefaultCheapStr::from("missing"), u16::MAX),
740        });
741        match resolved.end {
742            GenericGridPlacement::Line(line) => assert_eq!(line.as_i16(), i16::MAX),
743            _ => panic!("expected a resolved line"),
744        }
745    }
746
747    #[test]
748    fn area_lines_saturate_when_converted_to_grid_lines() {
749        let style = Style {
750            grid_template_areas: Some(GridTemplateAreas {
751                areas: vec![GridTemplateArea {
752                    name: DefaultCheapStr::from("area"),
753                    row_start: 1,
754                    row_end: 2,
755                    column_start: u16::MAX,
756                    column_end: u16::MAX,
757                }],
758                row_count: 1,
759                column_count: u16::MAX,
760            }),
761            ..Style::DEFAULT
762        };
763        let resolver = NamedLineResolver::new(&style, 0, 0);
764        assert_eq!(
765            resolved_start_line(&resolver, GridPlacement::NamedLine(DefaultCheapStr::from("area-start"), 1)),
766            i16::MAX
767        );
768    }
769}