Skip to main content

azul_css/props/layout/
column.rs

1//! CSS properties for multi-column layout.
2//!
3//! Covers `column-count`, `column-width`, `column-span`, `column-fill`,
4//! `column-rule-width`, `column-rule-style`, and `column-rule-color`.
5//! Types are consumed via the `CssProperty` enum in the CSS property system.
6
7use alloc::string::{String, ToString};
8use core::num::ParseIntError;
9
10use crate::props::{
11    basic::{
12        color::{parse_css_color, ColorU, CssColorParseError, CssColorParseErrorOwned},
13        pixel::{
14            parse_pixel_value, CssPixelValueParseError, CssPixelValueParseErrorOwned, PixelValue,
15        },
16    },
17    formatter::PrintAsCssValue,
18    style::border::{
19        parse_border_style, BorderStyle, CssBorderStyleParseError, CssBorderStyleParseErrorOwned,
20    },
21};
22
23// --- column-count ---
24
25/// CSS `column-count` property: specifies the number of columns in a multi-column layout.
26///
27/// Values: `auto` or a positive integer.
28#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
29#[repr(C, u8)]
30#[derive(Default)]
31pub enum ColumnCount {
32    #[default]
33    Auto,
34    Integer(u32),
35}
36
37
38impl PrintAsCssValue for ColumnCount {
39    fn print_as_css_value(&self) -> String {
40        match self {
41            Self::Auto => "auto".to_string(),
42            Self::Integer(i) => i.to_string(),
43        }
44    }
45}
46
47// --- column-width ---
48
49/// CSS `column-width` property: specifies the optimal width of columns.
50///
51/// Values: `auto` or a length value (e.g. `200px`, `15em`).
52#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
53#[repr(C, u8)]
54#[derive(Default)]
55pub enum ColumnWidth {
56    #[default]
57    Auto,
58    Length(PixelValue),
59}
60
61
62impl PrintAsCssValue for ColumnWidth {
63    fn print_as_css_value(&self) -> String {
64        match self {
65            Self::Auto => "auto".to_string(),
66            Self::Length(px) => px.print_as_css_value(),
67        }
68    }
69}
70
71// --- column-span ---
72
73/// CSS `column-span` property: whether an element spans across all columns.
74///
75/// Values: `none` (default) or `all`.
76#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
77#[repr(C)]
78#[derive(Default)]
79pub enum ColumnSpan {
80    #[default]
81    None,
82    All,
83}
84
85
86impl PrintAsCssValue for ColumnSpan {
87    fn print_as_css_value(&self) -> String {
88        String::from(match self {
89            Self::None => "none",
90            Self::All => "all",
91        })
92    }
93}
94
95// --- column-fill ---
96
97/// CSS `column-fill` property: how content is distributed across columns.
98///
99/// Values: `balance` (default) or `auto`.
100#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
101#[repr(C)]
102#[derive(Default)]
103pub enum ColumnFill {
104    Auto,
105    #[default]
106    Balance,
107}
108
109
110impl PrintAsCssValue for ColumnFill {
111    fn print_as_css_value(&self) -> String {
112        String::from(match self {
113            Self::Auto => "auto",
114            Self::Balance => "balance",
115        })
116    }
117}
118
119// --- column-rule ---
120
121/// CSS `column-rule-width` property: the width of the rule between columns.
122///
123/// Defaults to `medium` (3px).
124#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
125#[repr(C)]
126pub struct ColumnRuleWidth {
127    pub inner: PixelValue,
128}
129
130impl Default for ColumnRuleWidth {
131    fn default() -> Self {
132        Self {
133            inner: PixelValue::const_px(3),
134        }
135    }
136}
137
138impl PrintAsCssValue for ColumnRuleWidth {
139    fn print_as_css_value(&self) -> String {
140        self.inner.print_as_css_value()
141    }
142}
143
144/// CSS `column-rule-style` property: the style of the rule between columns.
145///
146/// Uses `BorderStyle` values (e.g. `none`, `solid`, `dotted`).
147#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
148#[repr(C)]
149pub struct ColumnRuleStyle {
150    pub inner: BorderStyle,
151}
152
153impl Default for ColumnRuleStyle {
154    fn default() -> Self {
155        Self {
156            inner: BorderStyle::None,
157        }
158    }
159}
160
161impl PrintAsCssValue for ColumnRuleStyle {
162    fn print_as_css_value(&self) -> String {
163        self.inner.print_as_css_value()
164    }
165}
166
167/// CSS `column-rule-color` property: the color of the rule between columns.
168///
169/// Per the CSS spec this should default to `currentcolor`, but currently
170/// defaults to black as `currentcolor` requires a resolved-value pass at
171/// layout time.
172#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
173#[repr(C)]
174pub struct ColumnRuleColor {
175    pub inner: ColorU,
176}
177
178impl Default for ColumnRuleColor {
179    fn default() -> Self {
180        // NOTE: should be `currentcolor` per CSS spec, see doc comment on type
181        Self {
182            inner: ColorU::BLACK,
183        }
184    }
185}
186
187impl PrintAsCssValue for ColumnRuleColor {
188    fn print_as_css_value(&self) -> String {
189        self.inner.to_hash()
190    }
191}
192
193// Formatting to Rust code
194impl crate::format_rust_code::FormatAsRustCode for ColumnCount {
195    fn format_as_rust_code(&self, _tabs: usize) -> String {
196        match self {
197            ColumnCount::Auto => String::from("ColumnCount::Auto"),
198            ColumnCount::Integer(i) => format!("ColumnCount::Integer({})", i),
199        }
200    }
201}
202
203impl crate::format_rust_code::FormatAsRustCode for ColumnWidth {
204    fn format_as_rust_code(&self, _tabs: usize) -> String {
205        match self {
206            ColumnWidth::Auto => String::from("ColumnWidth::Auto"),
207            ColumnWidth::Length(px) => format!(
208                "ColumnWidth::Length({})",
209                crate::format_rust_code::format_pixel_value(px)
210            ),
211        }
212    }
213}
214
215impl crate::format_rust_code::FormatAsRustCode for ColumnSpan {
216    fn format_as_rust_code(&self, _tabs: usize) -> String {
217        match self {
218            ColumnSpan::None => String::from("ColumnSpan::None"),
219            ColumnSpan::All => String::from("ColumnSpan::All"),
220        }
221    }
222}
223
224impl crate::format_rust_code::FormatAsRustCode for ColumnFill {
225    fn format_as_rust_code(&self, _tabs: usize) -> String {
226        match self {
227            ColumnFill::Auto => String::from("ColumnFill::Auto"),
228            ColumnFill::Balance => String::from("ColumnFill::Balance"),
229        }
230    }
231}
232
233impl crate::format_rust_code::FormatAsRustCode for ColumnRuleWidth {
234    fn format_as_rust_code(&self, _tabs: usize) -> String {
235        format!(
236            "ColumnRuleWidth {{ inner: {} }}",
237            crate::format_rust_code::format_pixel_value(&self.inner)
238        )
239    }
240}
241
242impl crate::format_rust_code::FormatAsRustCode for ColumnRuleStyle {
243    fn format_as_rust_code(&self, tabs: usize) -> String {
244        format!(
245            "ColumnRuleStyle {{ inner: {} }}",
246            self.inner.format_as_rust_code(tabs)
247        )
248    }
249}
250
251impl crate::format_rust_code::FormatAsRustCode for ColumnRuleColor {
252    fn format_as_rust_code(&self, _tabs: usize) -> String {
253        format!(
254            "ColumnRuleColor {{ inner: {} }}",
255            crate::format_rust_code::format_color_value(&self.inner)
256        )
257    }
258}
259
260// --- PARSERS ---
261
262#[cfg(feature = "parser")]
263pub mod parser {
264    use super::*;
265    use crate::corety::AzString;
266
267    // -- ColumnCount parser
268
269    #[derive(Clone, PartialEq)]
270    pub enum ColumnCountParseError<'a> {
271        InvalidValue(&'a str),
272        ParseInt(ParseIntError),
273    }
274
275    impl_debug_as_display!(ColumnCountParseError<'a>);
276    impl_display! { ColumnCountParseError<'a>, {
277        InvalidValue(v) => format!("Invalid column-count value: \"{}\"", v),
278        ParseInt(e) => format!("Invalid integer for column-count: {}", e),
279    }}
280
281    #[derive(Debug, Clone, PartialEq)]
282    #[repr(C, u8)]
283    pub enum ColumnCountParseErrorOwned {
284        InvalidValue(AzString),
285        ParseInt(AzString),
286    }
287
288    impl<'a> ColumnCountParseError<'a> {
289        pub fn to_contained(&self) -> ColumnCountParseErrorOwned {
290            match self {
291                Self::InvalidValue(s) => ColumnCountParseErrorOwned::InvalidValue(s.to_string().into()),
292                Self::ParseInt(e) => ColumnCountParseErrorOwned::ParseInt(e.to_string().into()),
293            }
294        }
295    }
296
297    impl ColumnCountParseErrorOwned {
298        pub fn to_shared<'a>(&'a self) -> ColumnCountParseError<'a> {
299            match self {
300                Self::InvalidValue(s) => ColumnCountParseError::InvalidValue(s),
301                // ParseIntError cannot be reconstructed from its Display string,
302                // so we fall back to a generic message. The original error text
303                // is preserved in the owned `AzString` but not round-trippable.
304                Self::ParseInt(_) => ColumnCountParseError::InvalidValue("invalid integer"),
305            }
306        }
307    }
308
309    pub fn parse_column_count<'a>(
310        input: &'a str,
311    ) -> Result<ColumnCount, ColumnCountParseError<'a>> {
312        let trimmed = input.trim();
313        if trimmed == "auto" {
314            return Ok(ColumnCount::Auto);
315        }
316        let val: u32 = trimmed
317            .parse()
318            .map_err(ColumnCountParseError::ParseInt)?;
319        Ok(ColumnCount::Integer(val))
320    }
321
322    // -- ColumnWidth parser
323
324    #[derive(Clone, PartialEq)]
325    pub enum ColumnWidthParseError<'a> {
326        InvalidValue(&'a str),
327        PixelValue(CssPixelValueParseError<'a>),
328    }
329
330    impl_debug_as_display!(ColumnWidthParseError<'a>);
331    impl_display! { ColumnWidthParseError<'a>, {
332        InvalidValue(v) => format!("Invalid column-width value: \"{}\"", v),
333        PixelValue(e) => format!("{}", e),
334    }}
335    impl_from! { CssPixelValueParseError<'a>, ColumnWidthParseError::PixelValue }
336
337    #[derive(Debug, Clone, PartialEq)]
338    #[repr(C, u8)]
339    pub enum ColumnWidthParseErrorOwned {
340        InvalidValue(AzString),
341        PixelValue(CssPixelValueParseErrorOwned),
342    }
343
344    impl<'a> ColumnWidthParseError<'a> {
345        pub fn to_contained(&self) -> ColumnWidthParseErrorOwned {
346            match self {
347                Self::InvalidValue(s) => ColumnWidthParseErrorOwned::InvalidValue(s.to_string().into()),
348                Self::PixelValue(e) => ColumnWidthParseErrorOwned::PixelValue(e.to_contained()),
349            }
350        }
351    }
352
353    impl ColumnWidthParseErrorOwned {
354        pub fn to_shared<'a>(&'a self) -> ColumnWidthParseError<'a> {
355            match self {
356                Self::InvalidValue(s) => ColumnWidthParseError::InvalidValue(s),
357                Self::PixelValue(e) => ColumnWidthParseError::PixelValue(e.to_shared()),
358            }
359        }
360    }
361
362    pub fn parse_column_width<'a>(
363        input: &'a str,
364    ) -> Result<ColumnWidth, ColumnWidthParseError<'a>> {
365        let trimmed = input.trim();
366        if trimmed == "auto" {
367            return Ok(ColumnWidth::Auto);
368        }
369        Ok(ColumnWidth::Length(parse_pixel_value(trimmed)?))
370    }
371
372    // -- Other column parsers...
373    macro_rules! define_simple_column_parser {
374        (
375            $fn_name:ident,
376            $struct_name:ident,
377            $error_name:ident,
378            $error_owned_name:ident,
379            $prop_name:expr,
380            $($val:expr => $variant:path),+
381        ) => {
382            #[derive(Clone, PartialEq)]
383            pub enum $error_name<'a> {
384                InvalidValue(&'a str),
385            }
386
387            impl_debug_as_display!($error_name<'a>);
388            impl_display! { $error_name<'a>, {
389                InvalidValue(v) => format!("Invalid {} value: \"{}\"", $prop_name, v),
390            }}
391
392            #[derive(Debug, Clone, PartialEq)]
393            #[repr(C, u8)]
394            pub enum $error_owned_name {
395                InvalidValue(AzString),
396            }
397
398            impl<'a> $error_name<'a> {
399                pub fn to_contained(&self) -> $error_owned_name {
400                    match self {
401                        Self::InvalidValue(s) => $error_owned_name::InvalidValue(s.to_string().into()),
402                    }
403                }
404            }
405
406            impl $error_owned_name {
407                pub fn to_shared<'a>(&'a self) -> $error_name<'a> {
408                    match self {
409                        Self::InvalidValue(s) => $error_name::InvalidValue(s.as_str()),
410                    }
411                }
412            }
413
414            pub fn $fn_name<'a>(input: &'a str) -> Result<$struct_name, $error_name<'a>> {
415                match input.trim() {
416                    $( $val => Ok($variant), )+
417                    _ => Err($error_name::InvalidValue(input)),
418                }
419            }
420        };
421    }
422
423    define_simple_column_parser!(
424        parse_column_span,
425        ColumnSpan,
426        ColumnSpanParseError,
427        ColumnSpanParseErrorOwned,
428        "column-span",
429        "none" => ColumnSpan::None,
430        "all" => ColumnSpan::All
431    );
432
433    define_simple_column_parser!(
434        parse_column_fill,
435        ColumnFill,
436        ColumnFillParseError,
437        ColumnFillParseErrorOwned,
438        "column-fill",
439        "auto" => ColumnFill::Auto,
440        "balance" => ColumnFill::Balance
441    );
442
443    // Parsers for column-rule-*
444
445    #[derive(Clone, PartialEq)]
446    pub enum ColumnRuleWidthParseError<'a> {
447        Pixel(CssPixelValueParseError<'a>),
448    }
449    impl_debug_as_display!(ColumnRuleWidthParseError<'a>);
450    impl_display! { ColumnRuleWidthParseError<'a>, { Pixel(e) => format!("{}", e) }}
451    impl_from! { CssPixelValueParseError<'a>, ColumnRuleWidthParseError::Pixel }
452    #[derive(Debug, Clone, PartialEq)]
453    #[repr(C, u8)]
454    pub enum ColumnRuleWidthParseErrorOwned {
455        Pixel(CssPixelValueParseErrorOwned),
456    }
457    impl<'a> ColumnRuleWidthParseError<'a> {
458        pub fn to_contained(&self) -> ColumnRuleWidthParseErrorOwned {
459            match self {
460                ColumnRuleWidthParseError::Pixel(e) => {
461                    ColumnRuleWidthParseErrorOwned::Pixel(e.to_contained())
462                }
463            }
464        }
465    }
466    impl ColumnRuleWidthParseErrorOwned {
467        pub fn to_shared<'a>(&'a self) -> ColumnRuleWidthParseError<'a> {
468            match self {
469                ColumnRuleWidthParseErrorOwned::Pixel(e) => {
470                    ColumnRuleWidthParseError::Pixel(e.to_shared())
471                }
472            }
473        }
474    }
475    pub fn parse_column_rule_width<'a>(
476        input: &'a str,
477    ) -> Result<ColumnRuleWidth, ColumnRuleWidthParseError<'a>> {
478        Ok(ColumnRuleWidth {
479            inner: parse_pixel_value(input)?,
480        })
481    }
482
483    #[derive(Clone, PartialEq)]
484    pub enum ColumnRuleStyleParseError<'a> {
485        Style(CssBorderStyleParseError<'a>),
486    }
487    impl_debug_as_display!(ColumnRuleStyleParseError<'a>);
488    impl_display! { ColumnRuleStyleParseError<'a>, { Style(e) => format!("{}", e) }}
489    impl_from! { CssBorderStyleParseError<'a>, ColumnRuleStyleParseError::Style }
490    #[derive(Debug, Clone, PartialEq)]
491    #[repr(C, u8)]
492    pub enum ColumnRuleStyleParseErrorOwned {
493        Style(CssBorderStyleParseErrorOwned),
494    }
495    impl<'a> ColumnRuleStyleParseError<'a> {
496        pub fn to_contained(&self) -> ColumnRuleStyleParseErrorOwned {
497            match self {
498                ColumnRuleStyleParseError::Style(e) => {
499                    ColumnRuleStyleParseErrorOwned::Style(e.to_contained())
500                }
501            }
502        }
503    }
504    impl ColumnRuleStyleParseErrorOwned {
505        pub fn to_shared<'a>(&'a self) -> ColumnRuleStyleParseError<'a> {
506            match self {
507                ColumnRuleStyleParseErrorOwned::Style(e) => {
508                    ColumnRuleStyleParseError::Style(e.to_shared())
509                }
510            }
511        }
512    }
513    pub fn parse_column_rule_style<'a>(
514        input: &'a str,
515    ) -> Result<ColumnRuleStyle, ColumnRuleStyleParseError<'a>> {
516        Ok(ColumnRuleStyle {
517            inner: parse_border_style(input)?,
518        })
519    }
520
521    #[derive(Clone, PartialEq)]
522    pub enum ColumnRuleColorParseError<'a> {
523        Color(CssColorParseError<'a>),
524    }
525    impl_debug_as_display!(ColumnRuleColorParseError<'a>);
526    impl_display! { ColumnRuleColorParseError<'a>, { Color(e) => format!("{}", e) }}
527    impl_from! { CssColorParseError<'a>, ColumnRuleColorParseError::Color }
528    #[derive(Debug, Clone, PartialEq)]
529    #[repr(C, u8)]
530    pub enum ColumnRuleColorParseErrorOwned {
531        Color(CssColorParseErrorOwned),
532    }
533    impl<'a> ColumnRuleColorParseError<'a> {
534        pub fn to_contained(&self) -> ColumnRuleColorParseErrorOwned {
535            match self {
536                ColumnRuleColorParseError::Color(e) => {
537                    ColumnRuleColorParseErrorOwned::Color(e.to_contained())
538                }
539            }
540        }
541    }
542    impl ColumnRuleColorParseErrorOwned {
543        pub fn to_shared<'a>(&'a self) -> ColumnRuleColorParseError<'a> {
544            match self {
545                ColumnRuleColorParseErrorOwned::Color(e) => {
546                    ColumnRuleColorParseError::Color(e.to_shared())
547                }
548            }
549        }
550    }
551    pub fn parse_column_rule_color<'a>(
552        input: &'a str,
553    ) -> Result<ColumnRuleColor, ColumnRuleColorParseError<'a>> {
554        Ok(ColumnRuleColor {
555            inner: parse_css_color(input)?,
556        })
557    }
558}
559
560#[cfg(feature = "parser")]
561pub use parser::*;
562
563#[cfg(all(test, feature = "parser"))]
564mod tests {
565    use super::*;
566
567    #[test]
568    fn test_parse_column_count() {
569        assert_eq!(parse_column_count("auto").unwrap(), ColumnCount::Auto);
570        assert_eq!(parse_column_count("3").unwrap(), ColumnCount::Integer(3));
571        assert!(parse_column_count("none").is_err());
572        assert!(parse_column_count("2.5").is_err());
573    }
574
575    #[test]
576    fn test_parse_column_width() {
577        assert_eq!(parse_column_width("auto").unwrap(), ColumnWidth::Auto);
578        assert_eq!(
579            parse_column_width("200px").unwrap(),
580            ColumnWidth::Length(PixelValue::px(200.0))
581        );
582        assert_eq!(
583            parse_column_width("15em").unwrap(),
584            ColumnWidth::Length(PixelValue::em(15.0))
585        );
586        assert!(parse_column_width("50%").is_ok()); // Percentage is valid for column-width
587    }
588
589    #[test]
590    fn test_parse_column_span() {
591        assert_eq!(parse_column_span("none").unwrap(), ColumnSpan::None);
592        assert_eq!(parse_column_span("all").unwrap(), ColumnSpan::All);
593        assert!(parse_column_span("2").is_err());
594    }
595
596    #[test]
597    fn test_parse_column_fill() {
598        assert_eq!(parse_column_fill("auto").unwrap(), ColumnFill::Auto);
599        assert_eq!(parse_column_fill("balance").unwrap(), ColumnFill::Balance);
600        assert!(parse_column_fill("none").is_err());
601    }
602
603    #[test]
604    fn test_parse_column_rule() {
605        assert_eq!(
606            parse_column_rule_width("5px").unwrap().inner,
607            PixelValue::px(5.0)
608        );
609        assert_eq!(
610            parse_column_rule_style("dotted").unwrap().inner,
611            BorderStyle::Dotted
612        );
613        assert_eq!(parse_column_rule_color("blue").unwrap().inner, ColorU::BLUE);
614    }
615}