Skip to main content

azul_css/props/layout/
grid.rs

1//! CSS properties for CSS Grid layout.
2
3use alloc::{
4    boxed::Box,
5    string::{String, ToString},
6    vec::Vec,
7};
8
9use crate::{
10    corety::AzString,
11    format_rust_code::FormatAsRustCode,
12    impl_vec, impl_vec_clone, impl_vec_debug, impl_vec_eq, impl_vec_hash, impl_vec_mut,
13    impl_vec_ord, impl_vec_partialeq, impl_vec_partialord,
14    props::{basic::pixel::PixelValue, formatter::PrintAsCssValue},
15};
16
17// --- grid-template-columns / grid-template-rows ---
18
19/// Wrapper for minmax(min, max) to satisfy repr(C) (enum variants can only have 1 field)
20#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
21#[repr(C)]
22pub struct GridMinMax {
23    pub min: Box<GridTrackSizing>,
24    pub max: Box<GridTrackSizing>,
25}
26
27impl core::fmt::Debug for GridMinMax {
28    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
29        write!(
30            f,
31            "minmax({}, {})",
32            self.min.print_as_css_value(),
33            self.max.print_as_css_value()
34        )
35    }
36}
37
38/// Represents a single track sizing function for grid
39#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
40#[repr(C, u8)]
41#[derive(Default)]
42pub enum GridTrackSizing {
43    /// Fixed pixel/percent size
44    Fixed(PixelValue),
45    /// fr units (value multiplied by `FR_SCALING_FACTOR` to allow fractional
46    /// values while satisfying Eq/Ord/Hash — e.g. `1fr` = `Fr(100)`, `0.5fr` = `Fr(50)`)
47    Fr(i32),
48    /// min-content
49    MinContent,
50    /// max-content
51    MaxContent,
52    /// auto
53    #[default]
54    Auto,
55    /// minmax(min, max) - uses GridMinMax which contains Box<GridTrackSizing> for each bound
56    MinMax(GridMinMax),
57    /// fit-content(size)
58    FitContent(PixelValue),
59}
60
61impl_option!(
62    GridTrackSizing,
63    OptionGridTrackSizing,
64    copy = false,
65    [Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
66);
67
68impl core::fmt::Debug for GridTrackSizing {
69    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
70        write!(f, "{}", self.print_as_css_value())
71    }
72}
73
74
75impl PrintAsCssValue for GridTrackSizing {
76    fn print_as_css_value(&self) -> String {
77        match self {
78            GridTrackSizing::Fixed(px) => px.print_as_css_value(),
79            GridTrackSizing::Fr(f) => format!("{}fr", f),
80            GridTrackSizing::MinContent => "min-content".to_string(),
81            GridTrackSizing::MaxContent => "max-content".to_string(),
82            GridTrackSizing::Auto => "auto".to_string(),
83            GridTrackSizing::MinMax(minmax) => {
84                format!(
85                    "minmax({}, {})",
86                    minmax.min.print_as_css_value(),
87                    minmax.max.print_as_css_value()
88                )
89            }
90            GridTrackSizing::FitContent(size) => {
91                format!("fit-content({})", size.print_as_css_value())
92            }
93        }
94    }
95}
96
97// C-compatible Vec for GridTrackSizing
98impl_vec!(GridTrackSizing, GridTrackSizingVec, GridTrackSizingVecDestructor, GridTrackSizingVecDestructorType, GridTrackSizingVecSlice, OptionGridTrackSizing);
99impl_vec_clone!(
100    GridTrackSizing,
101    GridTrackSizingVec,
102    GridTrackSizingVecDestructor
103);
104impl_vec_debug!(GridTrackSizing, GridTrackSizingVec);
105impl_vec_partialeq!(GridTrackSizing, GridTrackSizingVec);
106impl_vec_eq!(GridTrackSizing, GridTrackSizingVec);
107impl_vec_partialord!(GridTrackSizing, GridTrackSizingVec);
108impl_vec_ord!(GridTrackSizing, GridTrackSizingVec);
109impl_vec_hash!(GridTrackSizing, GridTrackSizingVec);
110impl_vec_mut!(GridTrackSizing, GridTrackSizingVec);
111
112/// Represents `grid-template-columns` or `grid-template-rows`
113#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
114#[repr(C)]
115pub struct GridTemplate {
116    pub tracks: GridTrackSizingVec,
117}
118
119impl core::fmt::Debug for GridTemplate {
120    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
121        write!(f, "{}", self.print_as_css_value())
122    }
123}
124
125impl Default for GridTemplate {
126    fn default() -> Self {
127        GridTemplate {
128            tracks: GridTrackSizingVec::from_vec(Vec::new()),
129        }
130    }
131}
132
133impl PrintAsCssValue for GridTemplate {
134    fn print_as_css_value(&self) -> String {
135        let tracks_slice = self.tracks.as_ref();
136        if tracks_slice.is_empty() {
137            "none".to_string()
138        } else {
139            tracks_slice
140                .iter()
141                .map(|t| t.print_as_css_value())
142                .collect::<Vec<_>>()
143                .join(" ")
144        }
145    }
146}
147
148// --- grid-auto-columns / grid-auto-rows ---
149
150/// Represents `grid-auto-columns` or `grid-auto-rows`
151/// Structurally identical to GridTemplate but semantically different
152#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
153#[repr(C)]
154pub struct GridAutoTracks {
155    pub tracks: GridTrackSizingVec,
156}
157
158impl core::fmt::Debug for GridAutoTracks {
159    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
160        write!(f, "{}", self.print_as_css_value())
161    }
162}
163
164impl Default for GridAutoTracks {
165    fn default() -> Self {
166        GridAutoTracks {
167            tracks: GridTrackSizingVec::from_vec(Vec::new()),
168        }
169    }
170}
171
172impl PrintAsCssValue for GridAutoTracks {
173    fn print_as_css_value(&self) -> String {
174        let tracks_slice = self.tracks.as_ref();
175        if tracks_slice.is_empty() {
176            "auto".to_string()
177        } else {
178            tracks_slice
179                .iter()
180                .map(|t| t.print_as_css_value())
181                .collect::<Vec<_>>()
182                .join(" ")
183        }
184    }
185}
186
187impl From<GridTemplate> for GridAutoTracks {
188    fn from(template: GridTemplate) -> Self {
189        GridAutoTracks {
190            tracks: template.tracks,
191        }
192    }
193}
194
195// --- grid-row / grid-column (grid line placement) ---
196
197/// Named grid line with optional span count (FFI-safe wrapper)
198#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
199#[repr(C)]
200pub struct NamedGridLine {
201    pub grid_line_name: AzString,
202    /// Span count, 0 means no span specified
203    pub span_count: i32,
204}
205
206impl NamedGridLine {
207    pub fn create(name: AzString, span: Option<i32>) -> Self {
208        Self {
209            grid_line_name: name,
210            span_count: span.unwrap_or(0),
211        }
212    }
213
214    pub fn span(&self) -> Option<i32> {
215        if self.span_count == 0 {
216            None
217        } else {
218            Some(self.span_count)
219        }
220    }
221}
222
223/// Represents a grid line position (start or end)
224#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
225#[repr(C, u8)]
226#[derive(Default)]
227pub enum GridLine {
228    /// auto
229    #[default]
230    Auto,
231    /// Line number (1-based, negative for counting from end)
232    Line(i32),
233    /// Named line with optional span count
234    Named(NamedGridLine),
235    /// span N
236    Span(i32),
237}
238
239impl core::fmt::Debug for GridLine {
240    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
241        write!(f, "{}", self.print_as_css_value())
242    }
243}
244
245
246impl PrintAsCssValue for GridLine {
247    fn print_as_css_value(&self) -> String {
248        match self {
249            GridLine::Auto => "auto".to_string(),
250            GridLine::Line(n) => n.to_string(),
251            GridLine::Named(named) => {
252                if named.span_count == 0 {
253                    named.grid_line_name.as_str().to_string()
254                } else {
255                    format!("{} {}", named.grid_line_name.as_str(), named.span_count)
256                }
257            }
258            GridLine::Span(n) => format!("span {}", n),
259        }
260    }
261}
262
263/// Represents `grid-row` or `grid-column` (start / end)
264#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
265#[repr(C)]
266pub struct GridPlacement {
267    pub grid_start: GridLine,
268    pub grid_end: GridLine,
269}
270
271impl core::fmt::Debug for GridPlacement {
272    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
273        write!(f, "{}", self.print_as_css_value())
274    }
275}
276
277impl Default for GridPlacement {
278    fn default() -> Self {
279        GridPlacement {
280            grid_start: GridLine::Auto,
281            grid_end: GridLine::Auto,
282        }
283    }
284}
285
286impl PrintAsCssValue for GridPlacement {
287    fn print_as_css_value(&self) -> String {
288        if self.grid_end == GridLine::Auto {
289            self.grid_start.print_as_css_value()
290        } else {
291            format!(
292                "{} / {}",
293                self.grid_start.print_as_css_value(),
294                self.grid_end.print_as_css_value()
295            )
296        }
297    }
298}
299
300#[cfg(feature = "parser")]
301#[derive(Clone, PartialEq)]
302pub enum GridParseError<'a> {
303    InvalidValue(&'a str),
304}
305
306#[cfg(feature = "parser")]
307impl_debug_as_display!(GridParseError<'a>);
308#[cfg(feature = "parser")]
309impl_display! { GridParseError<'a>, {
310    InvalidValue(e) => format!("Invalid grid value: \"{}\"", e),
311}}
312
313#[cfg(feature = "parser")]
314#[derive(Debug, Clone, PartialEq)]
315#[repr(C, u8)]
316pub enum GridParseErrorOwned {
317    InvalidValue(AzString),
318}
319
320#[cfg(feature = "parser")]
321impl<'a> GridParseError<'a> {
322    pub fn to_contained(&self) -> GridParseErrorOwned {
323        match self {
324            GridParseError::InvalidValue(s) => GridParseErrorOwned::InvalidValue(s.to_string().into()),
325        }
326    }
327}
328
329#[cfg(feature = "parser")]
330impl GridParseErrorOwned {
331    pub fn to_shared<'a>(&'a self) -> GridParseError<'a> {
332        match self {
333            GridParseErrorOwned::InvalidValue(s) => GridParseError::InvalidValue(s.as_str()),
334        }
335    }
336}
337
338#[cfg(feature = "parser")]
339fn split_respecting_parens(input: &str) -> Result<Vec<String>, ()> {
340    let mut parts = Vec::new();
341    let mut current = String::new();
342    let mut paren_depth: i32 = 0;
343
344    for ch in input.chars() {
345        match ch {
346            '(' => { paren_depth += 1; current.push(ch); }
347            ')' => { paren_depth -= 1; if paren_depth < 0 { return Err(()); } current.push(ch); }
348            ' ' if paren_depth == 0 => {
349                if !current.trim().is_empty() {
350                    parts.push(current.trim().to_string());
351                    current.clear();
352                }
353            }
354            _ => current.push(ch),
355        }
356    }
357    if !current.trim().is_empty() {
358        parts.push(current.trim().to_string());
359    }
360    Ok(parts)
361}
362
363#[cfg(feature = "parser")]
364pub fn parse_grid_template<'a>(input: &'a str) -> Result<GridTemplate, GridParseError<'a>> {
365    use crate::props::basic::pixel::parse_pixel_value;
366
367    let input = input.trim();
368
369    if input == "none" {
370        return Ok(GridTemplate::default());
371    }
372
373    let parts = split_respecting_parens(input)
374        .map_err(|_| GridParseError::InvalidValue(input))?;
375
376    let mut tracks = Vec::new();
377    for part in &parts {
378        parse_grid_track_or_repeat(part, &mut tracks)
379            .map_err(|_| GridParseError::InvalidValue(input))?;
380    }
381
382    Ok(GridTemplate {
383        tracks: GridTrackSizingVec::from_vec(tracks),
384    })
385}
386
387/// Parse a single grid track token, which may be `repeat(N, track)` or a plain track.
388/// For `repeat(N, track_list)`, the tracks are expanded inline.
389#[cfg(feature = "parser")]
390fn parse_grid_track_or_repeat(input: &str, tracks: &mut Vec<GridTrackSizing>) -> Result<(), ()> {
391    let input = input.trim();
392
393    // Handle repeat(N, track_list)
394    if input.starts_with("repeat(") && input.ends_with(')') {
395        let content = &input[7..input.len() - 1];
396        // Find the first comma that separates the count from the track list
397        let comma_pos = content.find(',').ok_or(())?;
398        let count_str = content[..comma_pos].trim();
399        let track_list_str = content[comma_pos + 1..].trim();
400
401        let count: usize = count_str.parse().map_err(|_| ())?;
402        const MAX_GRID_REPEAT_COUNT: usize = 10_000;
403        if count == 0 || count > MAX_GRID_REPEAT_COUNT {
404            return Err(());
405        }
406
407        // Parse the track list (may contain multiple space-separated tracks)
408        let parts = split_respecting_parens(track_list_str)?;
409        let repeat_tracks: Vec<GridTrackSizing> = parts
410            .iter()
411            .map(|p| parse_grid_track_owned(p))
412            .collect::<Result<Vec<_>, _>>()?;
413
414        // Expand: repeat N times
415        for _ in 0..count {
416            tracks.extend(repeat_tracks.iter().cloned());
417        }
418        return Ok(());
419    }
420
421    // Plain single track
422    tracks.push(parse_grid_track_owned(input)?);
423    Ok(())
424}
425
426#[cfg(feature = "parser")]
427fn parse_grid_track_owned(input: &str) -> Result<GridTrackSizing, ()> {
428    use crate::props::basic::pixel::parse_pixel_value;
429
430    let input = input.trim();
431
432    if input == "auto" {
433        return Ok(GridTrackSizing::Auto);
434    }
435
436    if input == "min-content" {
437        return Ok(GridTrackSizing::MinContent);
438    }
439
440    if input == "max-content" {
441        return Ok(GridTrackSizing::MaxContent);
442    }
443
444    if input.ends_with("fr") {
445        /// Fr values are stored as integers scaled by this factor (e.g. `1fr` = 100, `0.5fr` = 50).
446        const FR_SCALING_FACTOR: f32 = 100.0;
447        let num_str = &input[..input.len() - 2].trim();
448        if let Ok(num) = num_str.parse::<f32>() {
449            let scaled = num * FR_SCALING_FACTOR;
450            if scaled.is_nan() || scaled < (i32::MIN as f32) || scaled > (i32::MAX as f32) {
451                return Err(());
452            }
453            return Ok(GridTrackSizing::Fr(scaled as i32));
454        }
455        return Err(());
456    }
457
458    if input.starts_with("minmax(") && input.ends_with(')') {
459        let content = &input[7..input.len() - 1];
460        let parts: Vec<&str> = content.split(',').collect();
461        if parts.len() == 2 {
462            let min = parse_grid_track_owned(parts[0].trim())?;
463            let max = parse_grid_track_owned(parts[1].trim())?;
464            return Ok(GridTrackSizing::MinMax(GridMinMax {
465                min: Box::new(min),
466                max: Box::new(max),
467            }));
468        }
469        return Err(());
470    }
471
472    if input.starts_with("fit-content(") && input.ends_with(')') {
473        let size_str = &input[12..input.len() - 1].trim();
474        if let Ok(size) = parse_pixel_value(size_str) {
475            return Ok(GridTrackSizing::FitContent(size));
476        }
477        return Err(());
478    }
479
480    // Try to parse as pixel value
481    if let Ok(px) = parse_pixel_value(input) {
482        return Ok(GridTrackSizing::Fixed(px));
483    }
484
485    Err(())
486}
487
488#[cfg(feature = "parser")]
489pub fn parse_grid_placement<'a>(input: &'a str) -> Result<GridPlacement, GridParseError<'a>> {
490    let input = input.trim();
491
492    if input == "auto" {
493        return Ok(GridPlacement::default());
494    }
495
496    // Split by "/"
497    let parts: Vec<&str> = input.split('/').map(|s| s.trim()).collect();
498
499    let grid_start =
500        parse_grid_line_owned(parts[0]).map_err(|_| GridParseError::InvalidValue(input))?;
501    let grid_end = if parts.len() > 1 {
502        parse_grid_line_owned(parts[1]).map_err(|_| GridParseError::InvalidValue(input))?
503    } else {
504        GridLine::Auto
505    };
506
507    Ok(GridPlacement {
508        grid_start,
509        grid_end,
510    })
511}
512
513// --- grid-auto-flow ---
514
515/// Represents the `grid-auto-flow` property
516#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
517#[repr(C)]
518#[derive(Default)]
519pub enum LayoutGridAutoFlow {
520    #[default]
521    Row,
522    Column,
523    RowDense,
524    ColumnDense,
525}
526
527
528impl crate::props::formatter::PrintAsCssValue for LayoutGridAutoFlow {
529    fn print_as_css_value(&self) -> alloc::string::String {
530        match self {
531            LayoutGridAutoFlow::Row => "row".to_string(),
532            LayoutGridAutoFlow::Column => "column".to_string(),
533            LayoutGridAutoFlow::RowDense => "row dense".to_string(),
534            LayoutGridAutoFlow::ColumnDense => "column dense".to_string(),
535        }
536    }
537}
538
539#[cfg(feature = "parser")]
540#[derive(Clone, PartialEq)]
541pub enum GridAutoFlowParseError<'a> {
542    InvalidValue(&'a str),
543}
544
545#[cfg(feature = "parser")]
546impl_debug_as_display!(GridAutoFlowParseError<'a>);
547#[cfg(feature = "parser")]
548impl_display! { GridAutoFlowParseError<'a>, {
549    InvalidValue(e) => format!("Invalid grid-auto-flow value: \"{}\"", e),
550}}
551
552#[cfg(feature = "parser")]
553#[derive(Debug, Clone, PartialEq)]
554#[repr(C, u8)]
555pub enum GridAutoFlowParseErrorOwned {
556    InvalidValue(AzString),
557}
558
559#[cfg(feature = "parser")]
560impl<'a> GridAutoFlowParseError<'a> {
561    pub fn to_contained(&self) -> GridAutoFlowParseErrorOwned {
562        match self {
563            GridAutoFlowParseError::InvalidValue(s) => {
564                GridAutoFlowParseErrorOwned::InvalidValue(s.to_string().into())
565            }
566        }
567    }
568}
569
570#[cfg(feature = "parser")]
571impl GridAutoFlowParseErrorOwned {
572    pub fn to_shared<'a>(&'a self) -> GridAutoFlowParseError<'a> {
573        match self {
574            GridAutoFlowParseErrorOwned::InvalidValue(s) => {
575                GridAutoFlowParseError::InvalidValue(s.as_str())
576            }
577        }
578    }
579}
580
581#[cfg(feature = "parser")]
582pub fn parse_layout_grid_auto_flow<'a>(
583    input: &'a str,
584) -> Result<LayoutGridAutoFlow, GridAutoFlowParseError<'a>> {
585    match input.trim() {
586        "row" => Ok(LayoutGridAutoFlow::Row),
587        "column" => Ok(LayoutGridAutoFlow::Column),
588        "row dense" => Ok(LayoutGridAutoFlow::RowDense),
589        "column dense" => Ok(LayoutGridAutoFlow::ColumnDense),
590        "dense" => Ok(LayoutGridAutoFlow::RowDense),
591        _ => Err(GridAutoFlowParseError::InvalidValue(input)),
592    }
593}
594
595// --- justify-self / justify-items ---
596
597/// Represents `justify-self` for grid items
598#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
599#[repr(C)]
600#[derive(Default)]
601pub enum LayoutJustifySelf {
602    #[default]
603    Auto,
604    Start,
605    End,
606    Center,
607    Stretch,
608}
609
610
611impl crate::props::formatter::PrintAsCssValue for LayoutJustifySelf {
612    fn print_as_css_value(&self) -> alloc::string::String {
613        match self {
614            LayoutJustifySelf::Auto => "auto".to_string(),
615            LayoutJustifySelf::Start => "start".to_string(),
616            LayoutJustifySelf::End => "end".to_string(),
617            LayoutJustifySelf::Center => "center".to_string(),
618            LayoutJustifySelf::Stretch => "stretch".to_string(),
619        }
620    }
621}
622
623#[cfg(feature = "parser")]
624#[derive(Clone, PartialEq)]
625pub enum JustifySelfParseError<'a> {
626    InvalidValue(&'a str),
627}
628
629#[cfg(feature = "parser")]
630#[derive(Debug, Clone, PartialEq)]
631#[repr(C, u8)]
632pub enum JustifySelfParseErrorOwned {
633    InvalidValue(AzString),
634}
635
636#[cfg(feature = "parser")]
637impl<'a> JustifySelfParseError<'a> {
638    pub fn to_contained(&self) -> JustifySelfParseErrorOwned {
639        match self {
640            JustifySelfParseError::InvalidValue(s) => {
641                JustifySelfParseErrorOwned::InvalidValue(s.to_string().into())
642            }
643        }
644    }
645}
646
647#[cfg(feature = "parser")]
648impl JustifySelfParseErrorOwned {
649    pub fn to_shared<'a>(&'a self) -> JustifySelfParseError<'a> {
650        match self {
651            JustifySelfParseErrorOwned::InvalidValue(s) => {
652                JustifySelfParseError::InvalidValue(s.as_str())
653            }
654        }
655    }
656}
657
658#[cfg(feature = "parser")]
659impl_debug_as_display!(JustifySelfParseError<'a>);
660#[cfg(feature = "parser")]
661impl_display! { JustifySelfParseError<'a>, {
662    InvalidValue(e) => format!("Invalid justify-self value: \"{}\"", e),
663}}
664
665#[cfg(feature = "parser")]
666pub fn parse_layout_justify_self<'a>(
667    input: &'a str,
668) -> Result<LayoutJustifySelf, JustifySelfParseError<'a>> {
669    match input.trim() {
670        "auto" => Ok(LayoutJustifySelf::Auto),
671        "start" | "flex-start" => Ok(LayoutJustifySelf::Start),
672        "end" | "flex-end" => Ok(LayoutJustifySelf::End),
673        "center" => Ok(LayoutJustifySelf::Center),
674        "stretch" => Ok(LayoutJustifySelf::Stretch),
675        _ => Err(JustifySelfParseError::InvalidValue(input)),
676    }
677}
678
679/// Represents `justify-items` for grid containers
680#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
681#[repr(C)]
682#[derive(Default)]
683pub enum LayoutJustifyItems {
684    Start,
685    End,
686    Center,
687    #[default]
688    Stretch,
689}
690
691
692impl crate::props::formatter::PrintAsCssValue for LayoutJustifyItems {
693    fn print_as_css_value(&self) -> alloc::string::String {
694        match self {
695            LayoutJustifyItems::Start => "start".to_string(),
696            LayoutJustifyItems::End => "end".to_string(),
697            LayoutJustifyItems::Center => "center".to_string(),
698            LayoutJustifyItems::Stretch => "stretch".to_string(),
699        }
700    }
701}
702
703#[cfg(feature = "parser")]
704#[derive(Clone, PartialEq)]
705pub enum JustifyItemsParseError<'a> {
706    InvalidValue(&'a str),
707}
708
709#[cfg(feature = "parser")]
710#[derive(Debug, Clone, PartialEq)]
711#[repr(C, u8)]
712pub enum JustifyItemsParseErrorOwned {
713    InvalidValue(AzString),
714}
715
716#[cfg(feature = "parser")]
717impl<'a> JustifyItemsParseError<'a> {
718    pub fn to_contained(&self) -> JustifyItemsParseErrorOwned {
719        match self {
720            JustifyItemsParseError::InvalidValue(s) => {
721                JustifyItemsParseErrorOwned::InvalidValue(s.to_string().into())
722            }
723        }
724    }
725}
726
727#[cfg(feature = "parser")]
728impl JustifyItemsParseErrorOwned {
729    pub fn to_shared<'a>(&'a self) -> JustifyItemsParseError<'a> {
730        match self {
731            JustifyItemsParseErrorOwned::InvalidValue(s) => {
732                JustifyItemsParseError::InvalidValue(s.as_str())
733            }
734        }
735    }
736}
737
738#[cfg(feature = "parser")]
739impl_debug_as_display!(JustifyItemsParseError<'a>);
740#[cfg(feature = "parser")]
741impl_display! { JustifyItemsParseError<'a>, {
742    InvalidValue(e) => format!("Invalid justify-items value: \"{}\"", e),
743}}
744
745#[cfg(feature = "parser")]
746pub fn parse_layout_justify_items<'a>(
747    input: &'a str,
748) -> Result<LayoutJustifyItems, JustifyItemsParseError<'a>> {
749    match input.trim() {
750        "start" => Ok(LayoutJustifyItems::Start),
751        "end" => Ok(LayoutJustifyItems::End),
752        "center" => Ok(LayoutJustifyItems::Center),
753        "stretch" => Ok(LayoutJustifyItems::Stretch),
754        _ => Err(JustifyItemsParseError::InvalidValue(input)),
755    }
756}
757
758// --- gap (single value type) ---
759
760#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
761#[repr(C)]
762pub struct LayoutGap {
763    pub inner: crate::props::basic::pixel::PixelValue,
764}
765
766impl core::fmt::Debug for LayoutGap {
767    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
768        write!(f, "{}", self.inner)
769    }
770}
771
772impl crate::props::formatter::PrintAsCssValue for LayoutGap {
773    fn print_as_css_value(&self) -> alloc::string::String {
774        self.inner.print_as_css_value()
775    }
776}
777
778// Implement FormatAsRustCode for the new types so they can be emitted by the
779// code generator.
780impl FormatAsRustCode for LayoutGridAutoFlow {
781    fn format_as_rust_code(&self, _tabs: usize) -> String {
782        format!(
783            "LayoutGridAutoFlow::{}",
784            match self {
785                LayoutGridAutoFlow::Row => "Row",
786                LayoutGridAutoFlow::Column => "Column",
787                LayoutGridAutoFlow::RowDense => "RowDense",
788                LayoutGridAutoFlow::ColumnDense => "ColumnDense",
789            }
790        )
791    }
792}
793
794impl FormatAsRustCode for LayoutJustifySelf {
795    fn format_as_rust_code(&self, _tabs: usize) -> String {
796        format!(
797            "LayoutJustifySelf::{}",
798            match self {
799                LayoutJustifySelf::Auto => "Auto",
800                LayoutJustifySelf::Start => "Start",
801                LayoutJustifySelf::End => "End",
802                LayoutJustifySelf::Center => "Center",
803                LayoutJustifySelf::Stretch => "Stretch",
804            }
805        )
806    }
807}
808
809impl FormatAsRustCode for LayoutJustifyItems {
810    fn format_as_rust_code(&self, _tabs: usize) -> String {
811        format!(
812            "LayoutJustifyItems::{}",
813            match self {
814                LayoutJustifyItems::Start => "Start",
815                LayoutJustifyItems::End => "End",
816                LayoutJustifyItems::Center => "Center",
817                LayoutJustifyItems::Stretch => "Stretch",
818            }
819        )
820    }
821}
822
823impl FormatAsRustCode for LayoutGap {
824    fn format_as_rust_code(&self, _tabs: usize) -> String {
825        use crate::format_rust_code::format_pixel_value;
826        format!("LayoutGap {{ inner: {} }}", format_pixel_value(&self.inner))
827    }
828}
829
830impl FormatAsRustCode for GridTrackSizing {
831    fn format_as_rust_code(&self, tabs: usize) -> String {
832        use crate::format_rust_code::format_pixel_value;
833        match self {
834            GridTrackSizing::Fixed(pv) => {
835                format!("GridTrackSizing::Fixed({})", format_pixel_value(pv))
836            }
837            GridTrackSizing::Fr(f) => format!("GridTrackSizing::Fr({})", f),
838            GridTrackSizing::MinContent => "GridTrackSizing::MinContent".to_string(),
839            GridTrackSizing::MaxContent => "GridTrackSizing::MaxContent".to_string(),
840            GridTrackSizing::Auto => "GridTrackSizing::Auto".to_string(),
841            GridTrackSizing::MinMax(minmax) => {
842                format!(
843                    "GridTrackSizing::MinMax(GridMinMax {{ min: Box::new({}), max: Box::new({}) }})",
844                    minmax.min.format_as_rust_code(tabs),
845                    minmax.max.format_as_rust_code(tabs)
846                )
847            }
848            GridTrackSizing::FitContent(pv) => {
849                format!("GridTrackSizing::FitContent({})", format_pixel_value(pv))
850            }
851        }
852    }
853}
854
855impl FormatAsRustCode for GridAutoTracks {
856    fn format_as_rust_code(&self, tabs: usize) -> String {
857        let tracks: Vec<String> = self
858            .tracks
859            .as_ref()
860            .iter()
861            .map(|t| t.format_as_rust_code(tabs))
862            .collect();
863        format!(
864            "GridAutoTracks {{ tracks: GridTrackSizingVec::from_vec(vec![{}]) }}",
865            tracks.join(", ")
866        )
867    }
868}
869
870impl FormatAsRustCode for GridTemplateAreas {
871    fn format_as_rust_code(&self, _tabs: usize) -> String {
872        format!("GridTemplateAreas {{ areas: GridAreaDefinitionVec::from_vec(vec!{:?}) }}", self.areas.as_ref())
873    }
874}
875
876#[cfg(feature = "parser")]
877pub fn parse_layout_gap<'a>(
878    input: &'a str,
879) -> Result<LayoutGap, crate::props::basic::pixel::CssPixelValueParseError<'a>> {
880    crate::props::basic::pixel::parse_pixel_value(input).map(|p| LayoutGap { inner: p })
881}
882
883#[cfg(feature = "parser")]
884pub fn parse_grid_line_owned(input: &str) -> Result<GridLine, ()> {
885    let input = input.trim();
886
887    if input == "auto" {
888        return Ok(GridLine::Auto);
889    }
890
891    if input.starts_with("span ") {
892        let num_str = &input[5..].trim();
893        if let Ok(num) = num_str.parse::<i32>() {
894            return Ok(GridLine::Span(num));
895        }
896        return Err(());
897    }
898
899    // Try to parse as line number
900    if let Ok(num) = input.parse::<i32>() {
901        return Ok(GridLine::Line(num));
902    }
903
904    // Otherwise treat as named line
905    Ok(GridLine::Named(NamedGridLine::create(
906        input.to_string().into(),
907        None,
908    )))
909}
910
911#[cfg(all(test, feature = "parser"))]
912mod tests {
913    use super::*;
914
915    // Grid template tests
916    #[test]
917    fn test_parse_grid_template_none() {
918        let result = parse_grid_template("none").unwrap();
919        assert_eq!(result.tracks.len(), 0);
920    }
921
922    #[test]
923    fn test_parse_grid_template_single_px() {
924        let result = parse_grid_template("100px").unwrap();
925        assert_eq!(result.tracks.len(), 1);
926        assert!(matches!(
927            result.tracks.as_ref()[0],
928            GridTrackSizing::Fixed(_)
929        ));
930    }
931
932    #[test]
933    fn test_parse_grid_template_multiple_tracks() {
934        let result = parse_grid_template("100px 200px 1fr").unwrap();
935        assert_eq!(result.tracks.len(), 3);
936    }
937
938    #[test]
939    fn test_parse_grid_template_fr_units() {
940        let result = parse_grid_template("1fr 2fr 1fr").unwrap();
941        assert_eq!(result.tracks.len(), 3);
942        assert!(matches!(
943            result.tracks.as_ref()[0],
944            GridTrackSizing::Fr(100)
945        ));
946        assert!(matches!(
947            result.tracks.as_ref()[1],
948            GridTrackSizing::Fr(200)
949        ));
950    }
951
952    #[test]
953    fn test_parse_grid_template_fractional_fr() {
954        let result = parse_grid_template("0.5fr 1.5fr").unwrap();
955        assert_eq!(result.tracks.len(), 2);
956        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::Fr(50)));
957        assert!(matches!(
958            result.tracks.as_ref()[1],
959            GridTrackSizing::Fr(150)
960        ));
961    }
962
963    #[test]
964    fn test_parse_grid_template_auto() {
965        let result = parse_grid_template("auto 100px auto").unwrap();
966        assert_eq!(result.tracks.len(), 3);
967        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::Auto));
968        assert!(matches!(result.tracks.as_ref()[2], GridTrackSizing::Auto));
969    }
970
971    #[test]
972    fn test_parse_grid_template_min_max_content() {
973        let result = parse_grid_template("min-content max-content auto").unwrap();
974        assert_eq!(result.tracks.len(), 3);
975        assert!(matches!(
976            result.tracks.as_ref()[0],
977            GridTrackSizing::MinContent
978        ));
979        assert!(matches!(
980            result.tracks.as_ref()[1],
981            GridTrackSizing::MaxContent
982        ));
983    }
984
985    #[test]
986    fn test_parse_grid_template_minmax() {
987        let result = parse_grid_template("minmax(100px, 1fr)").unwrap();
988        assert_eq!(result.tracks.len(), 1);
989        assert!(matches!(
990            result.tracks.as_ref()[0],
991            GridTrackSizing::MinMax(_)
992        ));
993    }
994
995    #[test]
996    fn test_parse_grid_template_minmax_complex() {
997        let result = parse_grid_template("minmax(min-content, max-content)").unwrap();
998        assert_eq!(result.tracks.len(), 1);
999    }
1000
1001    #[test]
1002    fn test_parse_grid_template_fit_content() {
1003        let result = parse_grid_template("fit-content(200px)").unwrap();
1004        assert_eq!(result.tracks.len(), 1);
1005        assert!(matches!(
1006            result.tracks.as_ref()[0],
1007            GridTrackSizing::FitContent(_)
1008        ));
1009    }
1010
1011    #[test]
1012    fn test_parse_grid_template_mixed() {
1013        let result = parse_grid_template("100px minmax(100px, 1fr) auto 2fr").unwrap();
1014        assert_eq!(result.tracks.len(), 4);
1015    }
1016
1017    #[test]
1018    fn test_parse_grid_template_percent() {
1019        let result = parse_grid_template("25% 50% 25%").unwrap();
1020        assert_eq!(result.tracks.len(), 3);
1021    }
1022
1023    #[test]
1024    fn test_parse_grid_template_em_units() {
1025        let result = parse_grid_template("10em 20em 1fr").unwrap();
1026        assert_eq!(result.tracks.len(), 3);
1027    }
1028
1029    // Grid placement tests
1030    #[test]
1031    fn test_parse_grid_placement_auto() {
1032        let result = parse_grid_placement("auto").unwrap();
1033        assert!(matches!(result.grid_start, GridLine::Auto));
1034        assert!(matches!(result.grid_end, GridLine::Auto));
1035    }
1036
1037    #[test]
1038    fn test_parse_grid_placement_line_number() {
1039        let result = parse_grid_placement("1").unwrap();
1040        assert!(matches!(result.grid_start, GridLine::Line(1)));
1041        assert!(matches!(result.grid_end, GridLine::Auto));
1042    }
1043
1044    #[test]
1045    fn test_parse_grid_placement_negative_line() {
1046        let result = parse_grid_placement("-1").unwrap();
1047        assert!(matches!(result.grid_start, GridLine::Line(-1)));
1048    }
1049
1050    #[test]
1051    fn test_parse_grid_placement_span() {
1052        let result = parse_grid_placement("span 2").unwrap();
1053        assert!(matches!(result.grid_start, GridLine::Span(2)));
1054    }
1055
1056    #[test]
1057    fn test_parse_grid_placement_start_end() {
1058        let result = parse_grid_placement("1 / 3").unwrap();
1059        assert!(matches!(result.grid_start, GridLine::Line(1)));
1060        assert!(matches!(result.grid_end, GridLine::Line(3)));
1061    }
1062
1063    #[test]
1064    fn test_parse_grid_placement_span_end() {
1065        let result = parse_grid_placement("1 / span 2").unwrap();
1066        assert!(matches!(result.grid_start, GridLine::Line(1)));
1067        assert!(matches!(result.grid_end, GridLine::Span(2)));
1068    }
1069
1070    #[test]
1071    fn test_parse_grid_placement_named_line() {
1072        let result = parse_grid_placement("header-start").unwrap();
1073        assert!(matches!(result.grid_start, GridLine::Named(_)));
1074    }
1075
1076    #[test]
1077    fn test_parse_grid_placement_named_start_end() {
1078        let result = parse_grid_placement("header-start / header-end").unwrap();
1079        assert!(matches!(result.grid_start, GridLine::Named(_)));
1080        assert!(matches!(result.grid_end, GridLine::Named(_)));
1081    }
1082
1083    // Edge cases
1084    #[test]
1085    fn test_parse_grid_template_whitespace() {
1086        let result = parse_grid_template("  100px   200px  ").unwrap();
1087        assert_eq!(result.tracks.len(), 2);
1088    }
1089
1090    #[test]
1091    fn test_parse_grid_placement_whitespace() {
1092        let result = parse_grid_placement("  1  /  3  ").unwrap();
1093        assert!(matches!(result.grid_start, GridLine::Line(1)));
1094        assert!(matches!(result.grid_end, GridLine::Line(3)));
1095    }
1096
1097    #[test]
1098    fn test_parse_grid_template_zero_fr() {
1099        let result = parse_grid_template("0fr").unwrap();
1100        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::Fr(0)));
1101    }
1102
1103    #[test]
1104    fn test_parse_grid_placement_zero_line() {
1105        let result = parse_grid_placement("0").unwrap();
1106        assert!(matches!(result.grid_start, GridLine::Line(0)));
1107    }
1108
1109    // repeat() tests
1110    #[test]
1111    fn test_parse_grid_template_repeat_fr() {
1112        let result = parse_grid_template("repeat(3, 1fr)").unwrap();
1113        assert_eq!(result.tracks.len(), 3);
1114        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::Fr(100)));
1115        assert!(matches!(result.tracks.as_ref()[1], GridTrackSizing::Fr(100)));
1116        assert!(matches!(result.tracks.as_ref()[2], GridTrackSizing::Fr(100)));
1117    }
1118
1119    #[test]
1120    fn test_parse_grid_template_repeat_px() {
1121        let result = parse_grid_template("repeat(2, 100px)").unwrap();
1122        assert_eq!(result.tracks.len(), 2);
1123        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::Fixed(_)));
1124        assert!(matches!(result.tracks.as_ref()[1], GridTrackSizing::Fixed(_)));
1125    }
1126
1127    #[test]
1128    fn test_parse_grid_template_repeat_multiple_tracks() {
1129        // repeat(2, 100px 1fr) should expand to [100px, 1fr, 100px, 1fr]
1130        let result = parse_grid_template("repeat(2, 100px 1fr)").unwrap();
1131        assert_eq!(result.tracks.len(), 4);
1132        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::Fixed(_)));
1133        assert!(matches!(result.tracks.as_ref()[1], GridTrackSizing::Fr(100)));
1134        assert!(matches!(result.tracks.as_ref()[2], GridTrackSizing::Fixed(_)));
1135        assert!(matches!(result.tracks.as_ref()[3], GridTrackSizing::Fr(100)));
1136    }
1137
1138    #[test]
1139    fn test_parse_grid_template_repeat_with_other_tracks() {
1140        // "100px repeat(2, 1fr) auto" should produce [100px, 1fr, 1fr, auto]
1141        let result = parse_grid_template("100px repeat(2, 1fr) auto").unwrap();
1142        assert_eq!(result.tracks.len(), 4);
1143        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::Fixed(_)));
1144        assert!(matches!(result.tracks.as_ref()[1], GridTrackSizing::Fr(100)));
1145        assert!(matches!(result.tracks.as_ref()[2], GridTrackSizing::Fr(100)));
1146        assert!(matches!(result.tracks.as_ref()[3], GridTrackSizing::Auto));
1147    }
1148
1149    #[test]
1150    fn test_parse_grid_template_repeat_minmax() {
1151        let result = parse_grid_template("repeat(3, minmax(100px, 1fr))").unwrap();
1152        assert_eq!(result.tracks.len(), 3);
1153        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::MinMax(_)));
1154        assert!(matches!(result.tracks.as_ref()[1], GridTrackSizing::MinMax(_)));
1155        assert!(matches!(result.tracks.as_ref()[2], GridTrackSizing::MinMax(_)));
1156    }
1157}
1158
1159// --- grid-template-areas ---
1160
1161/// A single named grid area with its row/column bounds (1-based grid line numbers).
1162/// This matches taffy's `GridTemplateArea<String>`.
1163#[repr(C)]
1164#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1165pub struct GridAreaDefinition {
1166    pub name: AzString,
1167    pub row_start: u16,
1168    pub row_end: u16,
1169    pub column_start: u16,
1170    pub column_end: u16,
1171}
1172
1173impl_option!(
1174    GridAreaDefinition,
1175    OptionGridAreaDefinition,
1176    copy = false,
1177    [Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1178);
1179
1180impl_vec!(GridAreaDefinition, GridAreaDefinitionVec, GridAreaDefinitionVecDestructor, GridAreaDefinitionVecDestructorType, GridAreaDefinitionVecSlice, OptionGridAreaDefinition);
1181impl_vec_clone!(
1182    GridAreaDefinition,
1183    GridAreaDefinitionVec,
1184    GridAreaDefinitionVecDestructor
1185);
1186impl_vec_debug!(GridAreaDefinition, GridAreaDefinitionVec);
1187impl_vec_partialeq!(GridAreaDefinition, GridAreaDefinitionVec);
1188impl_vec_eq!(GridAreaDefinition, GridAreaDefinitionVec);
1189impl_vec_partialord!(GridAreaDefinition, GridAreaDefinitionVec);
1190impl_vec_ord!(GridAreaDefinition, GridAreaDefinitionVec);
1191impl_vec_hash!(GridAreaDefinition, GridAreaDefinitionVec);
1192impl_vec_mut!(GridAreaDefinition, GridAreaDefinitionVec);
1193
1194/// Represents the parsed value of `grid-template-areas`.
1195///
1196/// Example CSS:
1197/// ```css
1198/// grid-template-areas:
1199///     "header header header"
1200///     "sidebar main aside"
1201///     "footer footer footer";
1202/// ```
1203#[repr(C)]
1204#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1205pub struct GridTemplateAreas {
1206    pub areas: GridAreaDefinitionVec,
1207}
1208
1209impl Default for GridTemplateAreas {
1210    fn default() -> Self {
1211        GridTemplateAreas { areas: GridAreaDefinitionVec::from_vec(Vec::new()) }
1212    }
1213}
1214
1215impl PrintAsCssValue for GridTemplateAreas {
1216    fn print_as_css_value(&self) -> String {
1217        let areas_slice = self.areas.as_ref();
1218        if areas_slice.is_empty() {
1219            return "none".to_string();
1220        }
1221        // Reconstruct the row strings from the area definitions
1222        let max_row = areas_slice.iter().map(|a| a.row_end).max().unwrap_or(1);
1223        let max_col = areas_slice.iter().map(|a| a.column_end).max().unwrap_or(1);
1224        let num_rows = (max_row - 1) as usize;
1225        let num_cols = (max_col - 1) as usize;
1226        let mut grid: Vec<Vec<String>> = vec![vec![".".to_string(); num_cols]; num_rows];
1227        for area in areas_slice {
1228            for r in (area.row_start as usize - 1)..(area.row_end as usize - 1) {
1229                for c in (area.column_start as usize - 1)..(area.column_end as usize - 1) {
1230                    if r < num_rows && c < num_cols {
1231                        grid[r][c] = area.name.as_str().to_string();
1232                    }
1233                }
1234            }
1235        }
1236        grid.iter()
1237            .map(|row| format!("\"{}\"", row.join(" ")))
1238            .collect::<Vec<_>>()
1239            .join(" ")
1240    }
1241}
1242
1243/// Parse `grid-template-areas` CSS value.
1244///
1245/// Accepts quoted row strings like:
1246///   `"header header header" "sidebar main aside" "footer footer footer"`
1247///
1248/// Returns a `GridTemplateAreas` with deduplicated named areas and their
1249/// computed row/column line boundaries (1-based, as taffy expects).
1250#[cfg(feature = "parser")]
1251pub fn parse_grid_template_areas(input: &str) -> Result<GridTemplateAreas, ()> {
1252    let input = input.trim();
1253    if input == "none" {
1254        return Ok(GridTemplateAreas::default());
1255    }
1256
1257    // Extract quoted strings: each one is a row
1258    let mut rows: Vec<Vec<String>> = Vec::new();
1259    let mut i = 0;
1260    let bytes = input.as_bytes();
1261    while i < bytes.len() {
1262        if bytes[i] == b'"' || bytes[i] == b'\'' {
1263            let quote = bytes[i];
1264            i += 1;
1265            let start = i;
1266            while i < bytes.len() && bytes[i] != quote {
1267                i += 1;
1268            }
1269            if i >= bytes.len() {
1270                return Err(());
1271            }
1272            let row_str = &input[start..i];
1273            let cells: Vec<String> = row_str.split_whitespace().map(|s| s.to_string()).collect();
1274            if cells.is_empty() {
1275                return Err(());
1276            }
1277            rows.push(cells);
1278            i += 1; // skip closing quote
1279        } else {
1280            i += 1;
1281        }
1282    }
1283
1284    if rows.is_empty() {
1285        return Err(());
1286    }
1287
1288    // Validate: all rows must have the same number of columns
1289    let num_cols = rows[0].len();
1290    for row in &rows {
1291        if row.len() != num_cols {
1292            return Err(());
1293        }
1294    }
1295
1296    // Build area map: name -> (min_row, max_row, min_col, max_col) in 0-based indices
1297    use alloc::collections::BTreeMap;
1298    let mut area_map: BTreeMap<String, (usize, usize, usize, usize)> = BTreeMap::new();
1299
1300    for (row_idx, row) in rows.iter().enumerate() {
1301        for (col_idx, cell) in row.iter().enumerate() {
1302            if cell == "." {
1303                continue; // skip null cell tokens
1304            }
1305            let entry = area_map.entry(cell.clone()).or_insert((row_idx, row_idx, col_idx, col_idx));
1306            entry.0 = entry.0.min(row_idx);
1307            entry.1 = entry.1.max(row_idx);
1308            entry.2 = entry.2.min(col_idx);
1309            entry.3 = entry.3.max(col_idx);
1310        }
1311    }
1312
1313    // Convert to 1-based grid line numbers (taffy convention)
1314    let mut areas = Vec::new();
1315    for (name, (min_row, max_row, min_col, max_col)) in area_map {
1316        areas.push(GridAreaDefinition {
1317            name: name.into(),
1318            row_start: (min_row + 1) as u16,
1319            row_end: (max_row + 2) as u16,   // end line is one past the last cell
1320            column_start: (min_col + 1) as u16,
1321            column_end: (max_col + 2) as u16,
1322        });
1323    }
1324
1325    Ok(GridTemplateAreas { areas: GridAreaDefinitionVec::from_vec(areas) })
1326}