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}
635
636#[cfg(all(test, feature = "parser"))]
637#[allow(clippy::float_cmp)] // parsed values are compared against the exact source literals
638mod autotest_generated {
639    use super::*;
640    use crate::{codegen::format::FormatAsRustCode, corety::AzString, props::basic::SizeMetric};
641
642    // A long-but-not-pathological input size for the "does not hang" cases.
643    const LONG: usize = 1_000_000;
644
645    // -----------------------------------------------------------------
646    // parse_column_count
647    // -----------------------------------------------------------------
648
649    #[test]
650    fn column_count_valid_minimal_and_trimming() {
651        assert_eq!(parse_column_count("auto").unwrap(), ColumnCount::Auto);
652        assert_eq!(parse_column_count("1").unwrap(), ColumnCount::Integer(1));
653        // The parser trims, so surrounding whitespace must not change the value.
654        assert_eq!(parse_column_count("  auto\t\n").unwrap(), ColumnCount::Auto);
655        assert_eq!(parse_column_count(" \n 12 \t").unwrap(), ColumnCount::Integer(12));
656    }
657
658    #[test]
659    fn column_count_rejects_empty_and_whitespace_only() {
660        assert!(parse_column_count("").is_err());
661        assert!(parse_column_count("   ").is_err());
662        assert!(parse_column_count("\t\n\r ").is_err());
663    }
664
665    #[test]
666    fn column_count_rejects_garbage_without_panicking() {
667        for bad in [
668            "none", "auto auto", "3px", "3;garbage", "3 4", "2.5", "0x10", "1_000", "--3", "+-3",
669            "\0", "3\0", "١٢٣", "٣", "3", "NaN", "inf", "-inf", "e5", "1e3",
670        ] {
671            assert!(
672                parse_column_count(bad).is_err(),
673                "column-count accepted garbage: {bad:?}"
674            );
675        }
676    }
677
678    #[test]
679    fn column_count_case_sensitivity_is_exact() {
680        // NOTE: CSS keywords are ASCII-case-insensitive; this parser is not.
681        // Documented here as the *current* contract - it must at least not panic.
682        assert!(parse_column_count("AUTO").is_err());
683        assert!(parse_column_count("Auto").is_err());
684    }
685
686    #[test]
687    fn column_count_u32_boundaries_saturate_into_err_not_wrap() {
688        // Lower bound: 0 is accepted even though CSS requires a positive integer.
689        assert_eq!(parse_column_count("0").unwrap(), ColumnCount::Integer(0));
690        // `u32::from_str` accepts a leading '+'.
691        assert_eq!(parse_column_count("+7").unwrap(), ColumnCount::Integer(7));
692        // Exact u32 ceiling parses; one above must be a clean Err, never a wrap to 0.
693        assert_eq!(
694            parse_column_count("4294967295").unwrap(),
695            ColumnCount::Integer(u32::MAX)
696        );
697        assert!(parse_column_count("4294967296").is_err());
698        // i64::MAX / u64::MAX / a 40-digit number all overflow u32 -> Err, no wraparound.
699        assert!(parse_column_count("9223372036854775807").is_err());
700        assert!(parse_column_count("18446744073709551615").is_err());
701        assert!(parse_column_count("9999999999999999999999999999999999999999").is_err());
702        // Negatives (including -0) are not representable in u32.
703        assert!(parse_column_count("-1").is_err());
704        assert!(parse_column_count("-0").is_err());
705    }
706
707    #[test]
708    fn column_count_extremely_long_input_terminates() {
709        let huge = "9".repeat(LONG);
710        assert!(parse_column_count(&huge).is_err());
711        let huge_keyword = "auto".repeat(LONG / 4);
712        assert!(parse_column_count(&huge_keyword).is_err());
713        // Whitespace-padded huge input: trimming must not be quadratic or panic.
714        let padded = format!("{}{}{}", " ".repeat(10_000), "7", " ".repeat(10_000));
715        assert_eq!(parse_column_count(&padded).unwrap(), ColumnCount::Integer(7));
716    }
717
718    #[test]
719    fn column_count_deeply_nested_input_does_not_stack_overflow() {
720        let nested = "(".repeat(10_000);
721        assert!(parse_column_count(&nested).is_err());
722        let balanced = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
723        assert!(parse_column_count(&balanced).is_err());
724    }
725
726    #[test]
727    fn column_count_unicode_input_does_not_panic() {
728        for bad in [
729            "\u{1F600}",
730            "auto\u{1F600}",
731            "e\u{301}",       // combining acute accent
732            "\u{202E}3",      // RTL override
733            "\u{FEFF}auto",   // BOM (not whitespace -> not trimmed)
734            "au\u{0000}to",
735        ] {
736            assert!(
737                parse_column_count(bad).is_err(),
738                "column-count accepted unicode junk: {bad:?}"
739            );
740        }
741    }
742
743    #[test]
744    fn column_count_roundtrips_through_print_as_css_value() {
745        for value in [
746            ColumnCount::Auto,
747            ColumnCount::Integer(0),
748            ColumnCount::Integer(1),
749            ColumnCount::Integer(u32::MAX),
750        ] {
751            let printed = value.print_as_css_value();
752            assert_eq!(
753                parse_column_count(&printed).unwrap(),
754                value,
755                "round-trip failed for {value:?} (printed as {printed:?})"
756            );
757        }
758    }
759
760    // -----------------------------------------------------------------
761    // ColumnCountParseError::to_contained / ColumnCountParseErrorOwned::to_shared
762    // -----------------------------------------------------------------
763
764    #[test]
765    fn column_count_error_invalid_value_roundtrips_losslessly() {
766        for s in ["", "x", "  padded  ", "\u{1F600}"] {
767            let shared = ColumnCountParseError::InvalidValue(s);
768            let owned = shared.to_contained();
769            assert_eq!(owned, ColumnCountParseErrorOwned::InvalidValue(AzString::from(s)));
770            assert_eq!(owned.to_shared(), shared);
771        }
772    }
773
774    #[test]
775    fn column_count_error_parse_int_roundtrip_is_lossy_but_safe() {
776        let err = parse_column_count("abc").unwrap_err();
777        assert_eq!(
778            err,
779            ColumnCountParseError::ParseInt("abc".parse::<u32>().unwrap_err())
780        );
781
782        let owned = err.to_contained();
783        match &owned {
784            ColumnCountParseErrorOwned::ParseInt(msg) => {
785                assert!(!msg.as_str().is_empty(), "ParseInt message was dropped");
786            }
787            other => panic!("expected ParseInt, got {other:?}"),
788        }
789
790        // Documented lossy path: a ParseIntError cannot be rebuilt from its Display
791        // string, so to_shared() degrades to a generic InvalidValue instead of panicking.
792        assert_eq!(
793            owned.to_shared(),
794            ColumnCountParseError::InvalidValue("invalid integer")
795        );
796    }
797
798    #[test]
799    fn column_count_error_debug_equals_display_and_keeps_input() {
800        let err = ColumnCountParseError::InvalidValue("weird\u{1F600}input");
801        assert_eq!(format!("{err:?}"), format!("{err}"));
802        assert!(format!("{err}").contains("weird\u{1F600}input"));
803
804        // Overflow errors must render without panicking on an empty/huge instance.
805        let overflow = parse_column_count("4294967296").unwrap_err();
806        assert!(!format!("{overflow}").is_empty());
807        assert!(!format!("{:?}", overflow.to_contained()).is_empty());
808    }
809
810    // -----------------------------------------------------------------
811    // parse_column_width
812    // -----------------------------------------------------------------
813
814    #[test]
815    fn column_width_valid_minimal_and_trimming() {
816        assert_eq!(parse_column_width("auto").unwrap(), ColumnWidth::Auto);
817        assert_eq!(parse_column_width("  auto  ").unwrap(), ColumnWidth::Auto);
818        assert_eq!(
819            parse_column_width("\t1px\n").unwrap(),
820            ColumnWidth::Length(PixelValue::px(1.0))
821        );
822    }
823
824    #[test]
825    fn column_width_rejects_empty_whitespace_and_garbage() {
826        assert!(parse_column_width("").is_err());
827        assert!(parse_column_width("   ").is_err());
828        assert!(parse_column_width("\t\n").is_err());
829        for bad in [
830            "px", "em", "%", "ten-px", "200px;garbage", "200 px extra", "#200px", "\u{1F600}",
831            "20\u{301}px", "AUTO",
832        ] {
833            assert!(
834                parse_column_width(bad).is_err(),
835                "column-width accepted garbage: {bad:?}"
836            );
837        }
838    }
839
840    #[test]
841    fn column_width_non_finite_numbers_are_stored_finite() {
842        // f32::from_str accepts "NaN"/"inf", so these reach FloatValue::new().
843        // The isize-backed FloatValue must clamp them - a NaN/inf leaking into
844        // layout would poison every downstream computation.
845        let nan = parse_column_width("NaN").unwrap();
846        assert_eq!(nan, ColumnWidth::Length(PixelValue::px(0.0)));
847
848        for input in ["inf", "Infinity", "-inf", "-Infinity", "1e40px", "-1e40px"] {
849            let parsed = parse_column_width(input).unwrap();
850            match parsed {
851                ColumnWidth::Length(px) => assert!(
852                    px.number.get().is_finite(),
853                    "{input:?} produced a non-finite PixelValue"
854                ),
855                ColumnWidth::Auto => panic!("{input:?} unexpectedly parsed as auto"),
856            }
857        }
858    }
859
860    #[test]
861    fn column_width_zero_and_subnormal_boundaries() {
862        assert_eq!(
863            parse_column_width("0").unwrap(),
864            ColumnWidth::Length(PixelValue::px(0.0))
865        );
866        // -0.0 must normalize to the same stored value as +0.0 (FloatValue is an isize).
867        assert_eq!(
868            parse_column_width("-0").unwrap(),
869            parse_column_width("0").unwrap()
870        );
871        assert_eq!(
872            parse_column_width("-0px").unwrap(),
873            ColumnWidth::Length(PixelValue::px(0.0))
874        );
875        // Subnormal f32: 1e-45 * 1000 truncates to 0 rather than panicking.
876        assert_eq!(
877            parse_column_width("1e-45px").unwrap(),
878            ColumnWidth::Length(PixelValue::px(0.0))
879        );
880        // Negative lengths are (currently) accepted by the parser; must not wrap sign.
881        match parse_column_width("-10px").unwrap() {
882            ColumnWidth::Length(px) => assert!(px.number.get() < 0.0),
883            ColumnWidth::Auto => panic!("-10px parsed as auto"),
884        }
885    }
886
887    #[test]
888    fn column_width_extremely_long_input_terminates() {
889        let huge_digits = format!("{}px", "9".repeat(LONG));
890        let parsed = parse_column_width(&huge_digits).unwrap();
891        match parsed {
892            ColumnWidth::Length(px) => assert!(px.number.get().is_finite()),
893            ColumnWidth::Auto => panic!("digit soup parsed as auto"),
894        }
895        assert!(parse_column_width(&"a".repeat(LONG)).is_err());
896        assert!(parse_column_width(&"auto".repeat(LONG / 4)).is_err());
897    }
898
899    #[test]
900    fn column_width_deeply_nested_input_does_not_stack_overflow() {
901        assert!(parse_column_width(&"(".repeat(10_000)).is_err());
902        let balanced = format!("calc{}{}", "(".repeat(10_000), ")".repeat(10_000));
903        assert!(parse_column_width(&balanced).is_err());
904    }
905
906    #[test]
907    fn column_width_roundtrips_through_print_as_css_value() {
908        // Only exactly-representable numbers: FloatValue truncates to 1/1000ths,
909        // so e.g. 2.54cm is *not* expected to survive a print/parse cycle.
910        let values = [
911            ColumnWidth::Auto,
912            ColumnWidth::Length(PixelValue::px(0.0)),
913            ColumnWidth::Length(PixelValue::px(200.0)),
914            ColumnWidth::Length(PixelValue::px(-12.5)),
915            ColumnWidth::Length(PixelValue::em(1.5)),
916            ColumnWidth::Length(PixelValue::rem(2.0)),
917            ColumnWidth::Length(PixelValue::pt(-20.0)),
918            ColumnWidth::Length(PixelValue::percent(50.0)),
919            ColumnWidth::Length(PixelValue::inch(1.0)),
920            ColumnWidth::Length(PixelValue::cm(3.0)),
921            ColumnWidth::Length(PixelValue::mm(10.0)),
922            ColumnWidth::Length(PixelValue::from_metric(SizeMetric::Vw, 10.0)),
923            ColumnWidth::Length(PixelValue::from_metric(SizeMetric::Vh, 10.0)),
924            ColumnWidth::Length(PixelValue::from_metric(SizeMetric::Vmax, 10.0)),
925        ];
926        for value in values {
927            let printed = value.print_as_css_value();
928            assert_eq!(
929                parse_column_width(&printed).unwrap(),
930                value,
931                "round-trip failed for {value:?} (printed as {printed:?})"
932            );
933        }
934    }
935
936    // -----------------------------------------------------------------
937    // ColumnWidthParseError::to_contained / ColumnWidthParseErrorOwned::to_shared
938    // -----------------------------------------------------------------
939
940    #[test]
941    fn column_width_error_roundtrips_through_owned() {
942        // InvalidValue is only reachable by hand - the parser always delegates to
943        // the pixel parser - but the conversion still has to be lossless.
944        for s in ["", "bogus", "\u{1F600}"] {
945            let shared = ColumnWidthParseError::InvalidValue(s);
946            let owned = shared.to_contained();
947            assert_eq!(owned, ColumnWidthParseErrorOwned::InvalidValue(AzString::from(s)));
948            assert_eq!(owned.to_shared(), shared);
949        }
950
951        // The variants the parser actually produces.
952        for bad in ["", "ten-px", "px", "%"] {
953            let err = parse_column_width(bad).unwrap_err();
954            assert!(
955                matches!(err, ColumnWidthParseError::PixelValue(_)),
956                "{bad:?} produced an unexpected error variant: {err:?}"
957            );
958            let owned = err.to_contained();
959            assert_eq!(owned.to_shared(), err, "lossy round-trip for {bad:?}");
960            assert_eq!(format!("{err:?}"), format!("{err}"));
961        }
962    }
963
964    // -----------------------------------------------------------------
965    // parse_column_span / parse_column_fill
966    // -----------------------------------------------------------------
967
968    #[test]
969    fn column_span_and_fill_accept_only_their_keywords() {
970        assert_eq!(parse_column_span("none").unwrap(), ColumnSpan::None);
971        assert_eq!(parse_column_span(" all \t").unwrap(), ColumnSpan::All);
972        assert_eq!(parse_column_fill("auto").unwrap(), ColumnFill::Auto);
973        assert_eq!(parse_column_fill("\n balance ").unwrap(), ColumnFill::Balance);
974
975        for bad in ["", "   ", "2", "ALL", "None", "all all", "all;", "\u{1F600}", "nonee", "\0"] {
976            assert!(parse_column_span(bad).is_err(), "column-span accepted {bad:?}");
977        }
978        for bad in ["", "   ", "none", "AUTO", "balanced", "auto balance", "\u{1F600}"] {
979            assert!(parse_column_fill(bad).is_err(), "column-fill accepted {bad:?}");
980        }
981        // The two properties must not accept each other's keywords.
982        assert!(parse_column_span("balance").is_err());
983        assert!(parse_column_fill("all").is_err());
984    }
985
986    #[test]
987    fn column_span_and_fill_survive_long_and_nested_input() {
988        let huge = "all".repeat(LONG / 3);
989        assert!(parse_column_span(&huge).is_err());
990        assert!(parse_column_fill(&huge).is_err());
991        let nested = "(".repeat(10_000);
992        assert!(parse_column_span(&nested).is_err());
993        assert!(parse_column_fill(&nested).is_err());
994    }
995
996    #[test]
997    fn column_span_error_reports_the_untrimmed_input() {
998        // The macro-generated parser matches on the trimmed input but reports the
999        // *original* one - that asymmetry is load-bearing for error messages.
1000        let err = parse_column_span("  bogus  ").unwrap_err();
1001        match err {
1002            ColumnSpanParseError::InvalidValue(s) => assert_eq!(s, "  bogus  "),
1003        }
1004        assert_eq!(format!("{err:?}"), format!("{err}"));
1005        assert!(format!("{err}").contains("column-span"));
1006
1007        let owned = err.to_contained();
1008        assert_eq!(owned, ColumnSpanParseErrorOwned::InvalidValue(AzString::from("  bogus  ")));
1009        assert_eq!(owned.to_shared(), err);
1010
1011        let fill_err = parse_column_fill("\u{1F600}").unwrap_err();
1012        let fill_owned = fill_err.to_contained();
1013        assert_eq!(fill_owned.to_shared(), fill_err);
1014        assert!(format!("{fill_err}").contains("column-fill"));
1015    }
1016
1017    // -----------------------------------------------------------------
1018    // parse_column_rule_width
1019    // -----------------------------------------------------------------
1020
1021    #[test]
1022    fn column_rule_width_valid_and_invalid() {
1023        assert_eq!(
1024            parse_column_rule_width("5px").unwrap().inner,
1025            PixelValue::px(5.0)
1026        );
1027        assert_eq!(
1028            parse_column_rule_width("  0  ").unwrap().inner,
1029            PixelValue::px(0.0)
1030        );
1031        for bad in ["", "   ", "auto", "solid", "px", "\u{1F600}", "5px;5px", "5 px extra"] {
1032            assert!(
1033                parse_column_rule_width(bad).is_err(),
1034                "column-rule-width accepted {bad:?}"
1035            );
1036        }
1037    }
1038
1039    #[test]
1040    fn column_rule_width_extremes_stay_finite() {
1041        for input in ["NaN", "inf", "-inf", "1e40px", "-1e40px", "1e-45px"] {
1042            let w = parse_column_rule_width(input).unwrap();
1043            assert!(
1044                w.inner.number.get().is_finite(),
1045                "{input:?} produced a non-finite column-rule-width"
1046            );
1047        }
1048        assert!(parse_column_rule_width(&"9".repeat(LONG)).unwrap().inner.number.get().is_finite());
1049        assert!(parse_column_rule_width(&"(".repeat(10_000)).is_err());
1050    }
1051
1052    #[test]
1053    fn column_rule_width_default_is_medium_and_roundtrips() {
1054        let default = ColumnRuleWidth::default();
1055        assert_eq!(default.inner, PixelValue::const_px(3));
1056        assert_eq!(default.print_as_css_value(), "3px");
1057        assert_eq!(parse_column_rule_width("3px").unwrap(), default);
1058
1059        for value in [
1060            ColumnRuleWidth::default(),
1061            ColumnRuleWidth { inner: PixelValue::px(0.0) },
1062            ColumnRuleWidth { inner: PixelValue::em(2.5) },
1063            ColumnRuleWidth { inner: PixelValue::percent(-25.0) },
1064        ] {
1065            let printed = value.print_as_css_value();
1066            assert_eq!(parse_column_rule_width(&printed).unwrap(), value);
1067        }
1068    }
1069
1070    #[test]
1071    fn column_rule_width_error_roundtrips_through_owned() {
1072        for bad in ["", "auto", "px"] {
1073            let err = parse_column_rule_width(bad).unwrap_err();
1074            let owned = err.to_contained();
1075            assert_eq!(owned.to_shared(), err, "lossy round-trip for {bad:?}");
1076            assert_eq!(format!("{err:?}"), format!("{err}"));
1077            assert!(!format!("{err}").is_empty());
1078        }
1079    }
1080
1081    // -----------------------------------------------------------------
1082    // parse_column_rule_style
1083    // -----------------------------------------------------------------
1084
1085    #[test]
1086    fn column_rule_style_accepts_every_border_style_and_roundtrips() {
1087        for style in [
1088            BorderStyle::None,
1089            BorderStyle::Solid,
1090            BorderStyle::Double,
1091            BorderStyle::Dotted,
1092            BorderStyle::Dashed,
1093            BorderStyle::Hidden,
1094            BorderStyle::Groove,
1095            BorderStyle::Ridge,
1096            BorderStyle::Inset,
1097            BorderStyle::Outset,
1098        ] {
1099            let value = ColumnRuleStyle { inner: style };
1100            let printed = value.print_as_css_value();
1101            assert_eq!(
1102                parse_column_rule_style(&printed).unwrap(),
1103                value,
1104                "round-trip failed for {style:?} (printed as {printed:?})"
1105            );
1106        }
1107    }
1108
1109    #[test]
1110    fn column_rule_style_rejects_garbage_without_panicking() {
1111        for bad in [
1112            "", "   ", "SOLID", "solidd", "solid solid", "3px", "\u{1F600}", "\0", "soli\u{0301}d",
1113        ] {
1114            assert!(
1115                parse_column_rule_style(bad).is_err(),
1116                "column-rule-style accepted {bad:?}"
1117            );
1118        }
1119        assert!(parse_column_rule_style(&"solid".repeat(LONG / 5)).is_err());
1120        assert!(parse_column_rule_style(&"(".repeat(10_000)).is_err());
1121        // Leading/trailing whitespace *is* trimmed.
1122        assert_eq!(
1123            parse_column_rule_style("  dotted \n").unwrap().inner,
1124            BorderStyle::Dotted
1125        );
1126    }
1127
1128    #[test]
1129    fn column_rule_style_error_roundtrips_through_owned() {
1130        let err = parse_column_rule_style("bogus").unwrap_err();
1131        let owned = err.to_contained();
1132        assert_eq!(owned.to_shared(), err);
1133        assert_eq!(format!("{err:?}"), format!("{err}"));
1134        assert!(format!("{err}").contains("bogus"));
1135
1136        let unicode_err = parse_column_rule_style("\u{1F600}").unwrap_err();
1137        let unicode_owned = unicode_err.to_contained();
1138        assert_eq!(unicode_owned.to_shared(), unicode_err);
1139    }
1140
1141    // -----------------------------------------------------------------
1142    // parse_column_rule_color
1143    // -----------------------------------------------------------------
1144
1145    #[test]
1146    fn column_rule_color_accepts_names_hex_and_functions() {
1147        assert_eq!(parse_column_rule_color("blue").unwrap().inner, ColorU::BLUE);
1148        // Named colors *are* case-insensitive here (unlike column-span/fill/count).
1149        assert_eq!(parse_column_rule_color("  BLUE \t").unwrap().inner, ColorU::BLUE);
1150        assert_eq!(parse_column_rule_color("#000f").unwrap().inner, ColorU::BLACK);
1151        assert_eq!(
1152            parse_column_rule_color("#ff000080").unwrap().inner,
1153            ColorU::rgba(255, 0, 0, 128)
1154        );
1155        assert_eq!(parse_column_rule_color("transparent").unwrap().inner.a, 0);
1156        assert_eq!(
1157            parse_column_rule_color("rgba(255, 0, 0, 1.0)").unwrap().inner,
1158            ColorU::RED
1159        );
1160    }
1161
1162    #[test]
1163    fn column_rule_color_rejects_garbage_without_panicking() {
1164        for bad in [
1165            "",
1166            "   ",
1167            "#",
1168            "#f",
1169            "#ff",
1170            "#fffff",
1171            "#zzzzzz",
1172            "notacolor",
1173            "rgb(",
1174            "rgb(1,2",
1175            "rgb()",
1176            "rgb(300)",
1177            "hsl(",
1178            "\u{1F600}",
1179            "#\u{1F600}",
1180            "\u{130}", // dotted capital I: to_lowercase() expands to 2 chars
1181        ] {
1182            assert!(
1183                parse_column_rule_color(bad).is_err(),
1184                "column-rule-color accepted {bad:?}"
1185            );
1186        }
1187    }
1188
1189    #[test]
1190    fn column_rule_color_survives_long_and_nested_input() {
1191        assert!(parse_column_rule_color(&"a".repeat(100_000)).is_err());
1192        assert!(parse_column_rule_color(&"#".repeat(100_000)).is_err());
1193        // Unbalanced and deeply nested parens must not recurse or hang.
1194        assert!(parse_column_rule_color(&"(".repeat(10_000)).is_err());
1195        assert!(parse_column_rule_color(&format!("rgb{}", "(".repeat(10_000))).is_err());
1196        let nested = format!("rgb{}{}", "(".repeat(10_000), ")".repeat(10_000));
1197        assert!(parse_column_rule_color(&nested).is_err());
1198    }
1199
1200    #[test]
1201    fn column_rule_color_roundtrips_through_to_hash() {
1202        for color in [
1203            ColorU::BLACK,
1204            ColorU::WHITE,
1205            ColorU::RED,
1206            ColorU::BLUE,
1207            ColorU::TRANSPARENT,
1208            ColorU::rgba(1, 2, 3, 4),
1209            ColorU::rgba(255, 255, 255, 0),
1210            ColorU::rgba(0, 0, 0, 255),
1211        ] {
1212            let value = ColumnRuleColor { inner: color };
1213            let printed = value.print_as_css_value(); // "#rrggbbaa"
1214            assert_eq!(printed.len(), 9, "unexpected hash form: {printed:?}");
1215            assert_eq!(
1216                parse_column_rule_color(&printed).unwrap(),
1217                value,
1218                "round-trip failed for {color:?} (printed as {printed:?})"
1219            );
1220        }
1221    }
1222
1223    #[test]
1224    fn column_rule_color_error_roundtrips_through_owned() {
1225        for bad in ["", "notacolor", "rgb(", "#zzzzzz"] {
1226            let err = parse_column_rule_color(bad).unwrap_err();
1227            let owned = err.to_contained();
1228            assert_eq!(owned.to_shared(), err, "lossy round-trip for {bad:?}");
1229            assert_eq!(format!("{err:?}"), format!("{err}"));
1230            assert!(!format!("{err}").is_empty());
1231        }
1232    }
1233
1234    // -----------------------------------------------------------------
1235    // Type invariants: defaults, ordering, hashing, codegen
1236    // -----------------------------------------------------------------
1237
1238    #[test]
1239    fn defaults_match_the_documented_css_initial_values() {
1240        assert_eq!(ColumnCount::default(), ColumnCount::Auto);
1241        assert_eq!(ColumnWidth::default(), ColumnWidth::Auto);
1242        assert_eq!(ColumnSpan::default(), ColumnSpan::None);
1243        assert_eq!(ColumnFill::default(), ColumnFill::Balance);
1244        assert_eq!(ColumnRuleStyle::default().inner, BorderStyle::None);
1245        // NOTE: per CSS this should be `currentcolor`; the type doc records the deviation.
1246        assert_eq!(ColumnRuleColor::default().inner, ColorU::BLACK);
1247
1248        // Every default must print to something its own parser accepts.
1249        assert_eq!(
1250            parse_column_count(&ColumnCount::default().print_as_css_value()).unwrap(),
1251            ColumnCount::default()
1252        );
1253        assert_eq!(
1254            parse_column_width(&ColumnWidth::default().print_as_css_value()).unwrap(),
1255            ColumnWidth::default()
1256        );
1257        assert_eq!(
1258            parse_column_span(&ColumnSpan::default().print_as_css_value()).unwrap(),
1259            ColumnSpan::default()
1260        );
1261        assert_eq!(
1262            parse_column_fill(&ColumnFill::default().print_as_css_value()).unwrap(),
1263            ColumnFill::default()
1264        );
1265        assert_eq!(
1266            parse_column_rule_width(&ColumnRuleWidth::default().print_as_css_value()).unwrap(),
1267            ColumnRuleWidth::default()
1268        );
1269        assert_eq!(
1270            parse_column_rule_style(&ColumnRuleStyle::default().print_as_css_value()).unwrap(),
1271            ColumnRuleStyle::default()
1272        );
1273        assert_eq!(
1274            parse_column_rule_color(&ColumnRuleColor::default().print_as_css_value()).unwrap(),
1275            ColumnRuleColor::default()
1276        );
1277    }
1278
1279    #[test]
1280    fn ord_and_hash_agree_with_eq() {
1281        use std::{
1282            collections::hash_map::DefaultHasher,
1283            hash::{Hash, Hasher},
1284        };
1285
1286        fn hash_of<T: Hash>(t: &T) -> u64 {
1287            let mut h = DefaultHasher::new();
1288            t.hash(&mut h);
1289            h.finish()
1290        }
1291
1292        // Keyword variants sort before their value-carrying counterparts.
1293        assert!(ColumnCount::Auto < ColumnCount::Integer(0));
1294        assert!(ColumnCount::Integer(1) < ColumnCount::Integer(u32::MAX));
1295        assert!(ColumnWidth::Auto < ColumnWidth::Length(PixelValue::px(0.0)));
1296        assert!(ColumnSpan::None < ColumnSpan::All);
1297        assert!(ColumnFill::Auto < ColumnFill::Balance);
1298
1299        // Eq implies equal hashes (these types are used as prop-cache keys).
1300        assert_eq!(
1301            hash_of(&ColumnCount::Integer(7)),
1302            hash_of(&parse_column_count("7").unwrap())
1303        );
1304        assert_eq!(
1305            hash_of(&ColumnWidth::Length(PixelValue::px(0.0))),
1306            hash_of(&parse_column_width("-0px").unwrap())
1307        );
1308        assert_ne!(hash_of(&ColumnFill::Auto), hash_of(&ColumnFill::Balance));
1309    }
1310
1311    #[test]
1312    fn format_as_rust_code_emits_constructible_snippets() {
1313        assert_eq!(ColumnCount::Auto.format_as_rust_code(0), "ColumnCount::Auto");
1314        assert_eq!(
1315            ColumnCount::Integer(u32::MAX).format_as_rust_code(0),
1316            "ColumnCount::Integer(4294967295)"
1317        );
1318        assert_eq!(ColumnWidth::Auto.format_as_rust_code(0), "ColumnWidth::Auto");
1319        assert_eq!(ColumnSpan::All.format_as_rust_code(0), "ColumnSpan::All");
1320        assert_eq!(ColumnSpan::None.format_as_rust_code(0), "ColumnSpan::None");
1321        assert_eq!(ColumnFill::Auto.format_as_rust_code(0), "ColumnFill::Auto");
1322        assert_eq!(ColumnFill::Balance.format_as_rust_code(0), "ColumnFill::Balance");
1323
1324        // Extreme instances must format without panicking.
1325        let wide = ColumnWidth::Length(PixelValue::px(f32::MAX));
1326        assert!(wide.format_as_rust_code(0).starts_with("ColumnWidth::Length("));
1327        let rule = ColumnRuleWidth { inner: PixelValue::px(-0.5) };
1328        assert!(rule.format_as_rust_code(0).starts_with("ColumnRuleWidth {"));
1329        let style = ColumnRuleStyle { inner: BorderStyle::Dotted };
1330        assert!(style.format_as_rust_code(0).contains("Dotted"));
1331        let color = ColumnRuleColor { inner: ColorU::TRANSPARENT };
1332        assert!(color.format_as_rust_code(0).starts_with("ColumnRuleColor {"));
1333    }
1334}