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#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
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::codegen::format::FormatAsRustCode for ColumnCount {
195    fn format_as_rust_code(&self, _tabs: usize) -> String {
196        match self {
197            Self::Auto => String::from("ColumnCount::Auto"),
198            Self::Integer(i) => format!("ColumnCount::Integer({i})"),
199        }
200    }
201}
202
203impl crate::codegen::format::FormatAsRustCode for ColumnWidth {
204    fn format_as_rust_code(&self, _tabs: usize) -> String {
205        match self {
206            Self::Auto => String::from("ColumnWidth::Auto"),
207            Self::Length(px) => format!(
208                "ColumnWidth::Length({})",
209                crate::codegen::format::format_pixel_value(px)
210            ),
211        }
212    }
213}
214
215impl crate::codegen::format::FormatAsRustCode for ColumnSpan {
216    fn format_as_rust_code(&self, _tabs: usize) -> String {
217        match self {
218            Self::None => String::from("ColumnSpan::None"),
219            Self::All => String::from("ColumnSpan::All"),
220        }
221    }
222}
223
224impl crate::codegen::format::FormatAsRustCode for ColumnFill {
225    fn format_as_rust_code(&self, _tabs: usize) -> String {
226        match self {
227            Self::Auto => String::from("ColumnFill::Auto"),
228            Self::Balance => String::from("ColumnFill::Balance"),
229        }
230    }
231}
232
233impl crate::codegen::format::FormatAsRustCode for ColumnRuleWidth {
234    fn format_as_rust_code(&self, _tabs: usize) -> String {
235        format!(
236            "ColumnRuleWidth {{ inner: {} }}",
237            crate::codegen::format::format_pixel_value(&self.inner)
238        )
239    }
240}
241
242impl crate::codegen::format::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::codegen::format::FormatAsRustCode for ColumnRuleColor {
252    fn format_as_rust_code(&self, _tabs: usize) -> String {
253        format!(
254            "ColumnRuleColor {{ inner: {} }}",
255            crate::codegen::format::format_color_value(&self.inner)
256        )
257    }
258}
259
260// --- PARSERS ---
261
262#[cfg(feature = "parser")]
263pub mod parser {
264    #[allow(clippy::wildcard_imports)] // parser submodule reuses the parent module's value types
265    use super::*;
266    use crate::corety::AzString;
267
268    // -- ColumnCount parser
269
270    #[derive(Clone, PartialEq, Eq)]
271    pub enum ColumnCountParseError<'a> {
272        InvalidValue(&'a str),
273        ParseInt(ParseIntError),
274    }
275
276    impl_debug_as_display!(ColumnCountParseError<'a>);
277    impl_display! { ColumnCountParseError<'a>, {
278        InvalidValue(v) => format!("Invalid column-count value: \"{}\"", v),
279        ParseInt(e) => format!("Invalid integer for column-count: {}", e),
280    }}
281
282    #[derive(Debug, Clone, PartialEq, Eq)]
283    #[repr(C, u8)]
284    pub enum ColumnCountParseErrorOwned {
285        InvalidValue(AzString),
286        ParseInt(AzString),
287    }
288
289    impl ColumnCountParseError<'_> {
290        #[must_use] pub fn to_contained(&self) -> ColumnCountParseErrorOwned {
291            match self {
292                Self::InvalidValue(s) => ColumnCountParseErrorOwned::InvalidValue((*s).to_string().into()),
293                Self::ParseInt(e) => ColumnCountParseErrorOwned::ParseInt(e.to_string().into()),
294            }
295        }
296    }
297
298    impl ColumnCountParseErrorOwned {
299        #[must_use] pub fn to_shared(&self) -> ColumnCountParseError<'_> {
300            match self {
301                Self::InvalidValue(s) => ColumnCountParseError::InvalidValue(s),
302                // ParseIntError cannot be reconstructed from its Display string,
303                // so we fall back to a generic message. The original error text
304                // is preserved in the owned `AzString` but not round-trippable.
305                Self::ParseInt(_) => ColumnCountParseError::InvalidValue("invalid integer"),
306            }
307        }
308    }
309
310    /// # Errors
311    ///
312    /// Returns an error if `input` is not a valid CSS `column-count` value.
313    pub fn parse_column_count(
314        input: &str,
315    ) -> Result<ColumnCount, ColumnCountParseError<'_>> {
316        let trimmed = input.trim();
317        if trimmed == "auto" {
318            return Ok(ColumnCount::Auto);
319        }
320        let val: u32 = trimmed
321            .parse()
322            .map_err(ColumnCountParseError::ParseInt)?;
323        Ok(ColumnCount::Integer(val))
324    }
325
326    // -- ColumnWidth parser
327
328    #[derive(Clone, PartialEq, Eq)]
329    pub enum ColumnWidthParseError<'a> {
330        InvalidValue(&'a str),
331        PixelValue(CssPixelValueParseError<'a>),
332    }
333
334    impl_debug_as_display!(ColumnWidthParseError<'a>);
335    impl_display! { ColumnWidthParseError<'a>, {
336        InvalidValue(v) => format!("Invalid column-width value: \"{}\"", v),
337        PixelValue(e) => format!("{}", e),
338    }}
339    impl_from! { CssPixelValueParseError<'a>, ColumnWidthParseError::PixelValue }
340
341    #[derive(Debug, Clone, PartialEq, Eq)]
342    #[repr(C, u8)]
343    pub enum ColumnWidthParseErrorOwned {
344        InvalidValue(AzString),
345        PixelValue(CssPixelValueParseErrorOwned),
346    }
347
348    impl ColumnWidthParseError<'_> {
349        #[must_use] pub fn to_contained(&self) -> ColumnWidthParseErrorOwned {
350            match self {
351                Self::InvalidValue(s) => ColumnWidthParseErrorOwned::InvalidValue((*s).to_string().into()),
352                Self::PixelValue(e) => ColumnWidthParseErrorOwned::PixelValue(e.to_contained()),
353            }
354        }
355    }
356
357    impl ColumnWidthParseErrorOwned {
358        #[must_use] pub fn to_shared(&self) -> ColumnWidthParseError<'_> {
359            match self {
360                Self::InvalidValue(s) => ColumnWidthParseError::InvalidValue(s),
361                Self::PixelValue(e) => ColumnWidthParseError::PixelValue(e.to_shared()),
362            }
363        }
364    }
365
366    /// # Errors
367    ///
368    /// Returns an error if `input` is not a valid CSS `column-width` value.
369    pub fn parse_column_width(
370        input: &str,
371    ) -> Result<ColumnWidth, ColumnWidthParseError<'_>> {
372        let trimmed = input.trim();
373        if trimmed == "auto" {
374            return Ok(ColumnWidth::Auto);
375        }
376        Ok(ColumnWidth::Length(parse_pixel_value(trimmed)?))
377    }
378
379    // -- Other column parsers...
380    macro_rules! define_simple_column_parser {
381        (
382            $fn_name:ident,
383            $struct_name:ident,
384            $error_name:ident,
385            $error_owned_name:ident,
386            $prop_name:expr,
387            $($val:expr => $variant:path),+
388        ) => {
389            #[derive(Clone, PartialEq, Eq)]
390            pub enum $error_name<'a> {
391                InvalidValue(&'a str),
392            }
393
394            impl_debug_as_display!($error_name<'a>);
395            impl_display! { $error_name<'a>, {
396                InvalidValue(v) => format!("Invalid {} value: \"{}\"", $prop_name, v),
397            }}
398
399            #[derive(Debug, Clone, PartialEq, Eq)]
400            #[repr(C, u8)]
401            pub enum $error_owned_name {
402                InvalidValue(AzString),
403            }
404
405            impl $error_name<'_> {
406                #[must_use] pub fn to_contained(&self) -> $error_owned_name {
407                    match self {
408                        Self::InvalidValue(s) => $error_owned_name::InvalidValue(s.to_string().into()),
409                    }
410                }
411            }
412
413            impl $error_owned_name {
414                #[must_use] pub fn to_shared(&self) -> $error_name<'_> {
415                    match self {
416                        Self::InvalidValue(s) => $error_name::InvalidValue(s.as_str()),
417                    }
418                }
419            }
420
421            /// # Errors
422            ///
423            /// Returns an error if `input` is not a valid CSS value for this property.
424            pub fn $fn_name(input: &str) -> Result<$struct_name, $error_name<'_>> {
425                match input.trim() {
426                    $( $val => Ok($variant), )+
427                    _ => Err($error_name::InvalidValue(input)),
428                }
429            }
430        };
431    }
432
433    define_simple_column_parser!(
434        parse_column_span,
435        ColumnSpan,
436        ColumnSpanParseError,
437        ColumnSpanParseErrorOwned,
438        "column-span",
439        "none" => ColumnSpan::None,
440        "all" => ColumnSpan::All
441    );
442
443    define_simple_column_parser!(
444        parse_column_fill,
445        ColumnFill,
446        ColumnFillParseError,
447        ColumnFillParseErrorOwned,
448        "column-fill",
449        "auto" => ColumnFill::Auto,
450        "balance" => ColumnFill::Balance
451    );
452
453    // Parsers for column-rule-*
454
455    #[derive(Clone, PartialEq, Eq)]
456    pub enum ColumnRuleWidthParseError<'a> {
457        Pixel(CssPixelValueParseError<'a>),
458    }
459    impl_debug_as_display!(ColumnRuleWidthParseError<'a>);
460    impl_display! { ColumnRuleWidthParseError<'a>, { Pixel(e) => format!("{}", e) }}
461    impl_from! { CssPixelValueParseError<'a>, ColumnRuleWidthParseError::Pixel }
462    #[derive(Debug, Clone, PartialEq, Eq)]
463    #[repr(C, u8)]
464    pub enum ColumnRuleWidthParseErrorOwned {
465        Pixel(CssPixelValueParseErrorOwned),
466    }
467    impl ColumnRuleWidthParseError<'_> {
468        #[must_use] pub fn to_contained(&self) -> ColumnRuleWidthParseErrorOwned {
469            match self {
470                ColumnRuleWidthParseError::Pixel(e) => {
471                    ColumnRuleWidthParseErrorOwned::Pixel(e.to_contained())
472                }
473            }
474        }
475    }
476    impl ColumnRuleWidthParseErrorOwned {
477        #[must_use] pub fn to_shared(&self) -> ColumnRuleWidthParseError<'_> {
478            match self {
479                Self::Pixel(e) => {
480                    ColumnRuleWidthParseError::Pixel(e.to_shared())
481                }
482            }
483        }
484    }
485    /// # Errors
486    ///
487    /// Returns an error if `input` is not a valid CSS `column-rule-width` value.
488    pub fn parse_column_rule_width(
489        input: &str,
490    ) -> Result<ColumnRuleWidth, ColumnRuleWidthParseError<'_>> {
491        Ok(ColumnRuleWidth {
492            inner: parse_pixel_value(input)?,
493        })
494    }
495
496    #[derive(Clone, PartialEq, Eq)]
497    pub enum ColumnRuleStyleParseError<'a> {
498        Style(CssBorderStyleParseError<'a>),
499    }
500    impl_debug_as_display!(ColumnRuleStyleParseError<'a>);
501    impl_display! { ColumnRuleStyleParseError<'a>, { Style(e) => format!("{}", e) }}
502    impl_from! { CssBorderStyleParseError<'a>, ColumnRuleStyleParseError::Style }
503    #[derive(Debug, Clone, PartialEq, Eq)]
504    #[repr(C, u8)]
505    pub enum ColumnRuleStyleParseErrorOwned {
506        Style(CssBorderStyleParseErrorOwned),
507    }
508    impl ColumnRuleStyleParseError<'_> {
509        #[must_use] pub fn to_contained(&self) -> ColumnRuleStyleParseErrorOwned {
510            match self {
511                ColumnRuleStyleParseError::Style(e) => {
512                    ColumnRuleStyleParseErrorOwned::Style(e.to_contained())
513                }
514            }
515        }
516    }
517    impl ColumnRuleStyleParseErrorOwned {
518        #[must_use] pub fn to_shared(&self) -> ColumnRuleStyleParseError<'_> {
519            match self {
520                Self::Style(e) => {
521                    ColumnRuleStyleParseError::Style(e.to_shared())
522                }
523            }
524        }
525    }
526    /// # Errors
527    ///
528    /// Returns an error if `input` is not a valid CSS `column-rule-style` value.
529    pub fn parse_column_rule_style(
530        input: &str,
531    ) -> Result<ColumnRuleStyle, ColumnRuleStyleParseError<'_>> {
532        Ok(ColumnRuleStyle {
533            inner: parse_border_style(input)?,
534        })
535    }
536
537    #[derive(Clone, PartialEq)]
538    pub enum ColumnRuleColorParseError<'a> {
539        Color(CssColorParseError<'a>),
540    }
541    impl_debug_as_display!(ColumnRuleColorParseError<'a>);
542    impl_display! { ColumnRuleColorParseError<'a>, { Color(e) => format!("{}", e) }}
543    impl_from! { CssColorParseError<'a>, ColumnRuleColorParseError::Color }
544    #[derive(Debug, Clone, PartialEq)]
545    #[repr(C, u8)]
546    pub enum ColumnRuleColorParseErrorOwned {
547        Color(CssColorParseErrorOwned),
548    }
549    impl ColumnRuleColorParseError<'_> {
550        #[must_use] pub fn to_contained(&self) -> ColumnRuleColorParseErrorOwned {
551            match self {
552                ColumnRuleColorParseError::Color(e) => {
553                    ColumnRuleColorParseErrorOwned::Color(e.to_contained())
554                }
555            }
556        }
557    }
558    impl ColumnRuleColorParseErrorOwned {
559        #[must_use] pub fn to_shared(&self) -> ColumnRuleColorParseError<'_> {
560            match self {
561                Self::Color(e) => {
562                    ColumnRuleColorParseError::Color(e.to_shared())
563                }
564            }
565        }
566    }
567    /// # Errors
568    ///
569    /// Returns an error if `input` is not a valid CSS `column-rule-color` value.
570    pub fn parse_column_rule_color(
571        input: &str,
572    ) -> Result<ColumnRuleColor, ColumnRuleColorParseError<'_>> {
573        Ok(ColumnRuleColor {
574            inner: parse_css_color(input)?,
575        })
576    }
577}
578
579#[cfg(feature = "parser")]
580pub use parser::*;
581
582#[cfg(all(test, feature = "parser"))]
583mod tests {
584    use super::*;
585
586    #[test]
587    fn test_parse_column_count() {
588        assert_eq!(parse_column_count("auto").unwrap(), ColumnCount::Auto);
589        assert_eq!(parse_column_count("3").unwrap(), ColumnCount::Integer(3));
590        assert!(parse_column_count("none").is_err());
591        assert!(parse_column_count("2.5").is_err());
592    }
593
594    #[test]
595    fn test_parse_column_width() {
596        assert_eq!(parse_column_width("auto").unwrap(), ColumnWidth::Auto);
597        assert_eq!(
598            parse_column_width("200px").unwrap(),
599            ColumnWidth::Length(PixelValue::px(200.0))
600        );
601        assert_eq!(
602            parse_column_width("15em").unwrap(),
603            ColumnWidth::Length(PixelValue::em(15.0))
604        );
605        assert!(parse_column_width("50%").is_ok()); // Percentage is valid for column-width
606    }
607
608    #[test]
609    fn test_parse_column_span() {
610        assert_eq!(parse_column_span("none").unwrap(), ColumnSpan::None);
611        assert_eq!(parse_column_span("all").unwrap(), ColumnSpan::All);
612        assert!(parse_column_span("2").is_err());
613    }
614
615    #[test]
616    fn test_parse_column_fill() {
617        assert_eq!(parse_column_fill("auto").unwrap(), ColumnFill::Auto);
618        assert_eq!(parse_column_fill("balance").unwrap(), ColumnFill::Balance);
619        assert!(parse_column_fill("none").is_err());
620    }
621
622    #[test]
623    fn test_parse_column_rule() {
624        assert_eq!(
625            parse_column_rule_width("5px").unwrap().inner,
626            PixelValue::px(5.0)
627        );
628        assert_eq!(
629            parse_column_rule_style("dotted").unwrap().inner,
630            BorderStyle::Dotted
631        );
632        assert_eq!(parse_column_rule_color("blue").unwrap().inner, ColorU::BLUE);
633    }
634}