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