Skip to main content

azul_css/props/layout/
spacing.rs

1//! CSS properties for `margin`, `padding`, and `gap` (column-gap / row-gap).
2//!
3//! Shorthand parsers (`parse_layout_padding`, `parse_layout_margin`) and
4//! longhand per-side parsers are gated behind `#[cfg(feature = "parser")]`.
5
6use alloc::{
7    string::{String, ToString},
8    vec::Vec,
9};
10
11#[cfg(feature = "parser")]
12use crate::props::basic::pixel::{parse_pixel_value_with_auto, PixelValueWithAuto};
13use crate::{
14    css::PrintAsCssValue,
15    props::{
16        basic::pixel::{CssPixelValueParseError, CssPixelValueParseErrorOwned, PixelValue},
17        macros::PixelValueTaker,
18    },
19};
20
21// --- TYPE DEFINITIONS ---
22
23// Spacing properties - wrapper structs around PixelValue for type safety
24
25macro_rules! impl_spacing_type_impls {
26    ($name:ident) => {
27        impl ::core::fmt::Debug for $name {
28            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
29                write!(f, "{}", self.inner)
30            }
31        }
32
33        impl PixelValueTaker for $name {
34            fn from_pixel_value(inner: PixelValue) -> Self {
35                Self { inner }
36            }
37        }
38
39        impl_pixel_value!($name);
40    };
41}
42
43/// Layout padding top value
44#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
45#[repr(C)]
46pub struct LayoutPaddingTop {
47    pub inner: PixelValue,
48}
49impl_spacing_type_impls!(LayoutPaddingTop);
50
51/// Layout padding right value
52#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
53#[repr(C)]
54pub struct LayoutPaddingRight {
55    pub inner: PixelValue,
56}
57impl_spacing_type_impls!(LayoutPaddingRight);
58
59/// Layout padding bottom value
60#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
61#[repr(C)]
62pub struct LayoutPaddingBottom {
63    pub inner: PixelValue,
64}
65impl_spacing_type_impls!(LayoutPaddingBottom);
66
67/// Layout padding left value
68#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
69#[repr(C)]
70pub struct LayoutPaddingLeft {
71    pub inner: PixelValue,
72}
73impl_spacing_type_impls!(LayoutPaddingLeft);
74
75/// Layout padding inline start value (for RTL/LTR support)
76#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
77#[repr(C)]
78pub struct LayoutPaddingInlineStart {
79    pub inner: PixelValue,
80}
81impl_spacing_type_impls!(LayoutPaddingInlineStart);
82
83/// Layout padding inline end value (for RTL/LTR support)
84#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
85#[repr(C)]
86pub struct LayoutPaddingInlineEnd {
87    pub inner: PixelValue,
88}
89impl_spacing_type_impls!(LayoutPaddingInlineEnd);
90
91/// Layout margin top value
92#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
93#[repr(C)]
94pub struct LayoutMarginTop {
95    pub inner: PixelValue,
96}
97impl_spacing_type_impls!(LayoutMarginTop);
98
99/// Layout margin right value
100#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
101#[repr(C)]
102pub struct LayoutMarginRight {
103    pub inner: PixelValue,
104}
105impl_spacing_type_impls!(LayoutMarginRight);
106
107/// Layout margin bottom value
108#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
109#[repr(C)]
110pub struct LayoutMarginBottom {
111    pub inner: PixelValue,
112}
113impl_spacing_type_impls!(LayoutMarginBottom);
114
115/// Layout margin left value
116#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
117#[repr(C)]
118pub struct LayoutMarginLeft {
119    pub inner: PixelValue,
120}
121impl_spacing_type_impls!(LayoutMarginLeft);
122
123/// Layout column gap value (for flexbox/grid)
124#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
125#[repr(C)]
126pub struct LayoutColumnGap {
127    pub inner: PixelValue,
128}
129impl_spacing_type_impls!(LayoutColumnGap);
130
131/// Layout row gap value (for flexbox/grid)
132#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
133#[repr(C)]
134pub struct LayoutRowGap {
135    pub inner: PixelValue,
136}
137impl_spacing_type_impls!(LayoutRowGap);
138
139// --- PARSERS ---
140
141#[cfg(feature = "parser")]
142macro_rules! impl_spacing_parse_error {
143    ($borrowed:ident, $owned:ident, $property_name:expr) => {
144        #[cfg(feature = "parser")]
145        impl_debug_as_display!($borrowed<'a>);
146
147        #[cfg(feature = "parser")]
148        impl_display! { $borrowed<'a>, {
149            PixelValueParseError(e) => format!("Could not parse pixel value: {}", e),
150            TooManyValues => concat!("Too many values: ", $property_name, " property accepts at most 4 values."),
151            TooFewValues => concat!("Too few values: ", $property_name, " property requires at least 1 value."),
152        }}
153
154        #[cfg(feature = "parser")]
155        impl_from!(
156            CssPixelValueParseError<'a>,
157            $borrowed::PixelValueParseError
158        );
159
160        #[cfg(feature = "parser")]
161        impl $borrowed<'_> {
162            #[must_use] pub fn to_contained(&self) -> $owned {
163                match self {
164                    $borrowed::PixelValueParseError(e) => {
165                        $owned::PixelValueParseError(e.to_contained())
166                    }
167                    $borrowed::TooManyValues => $owned::TooManyValues,
168                    $borrowed::TooFewValues => $owned::TooFewValues,
169                }
170            }
171        }
172
173        #[cfg(feature = "parser")]
174        impl $owned {
175            #[must_use] pub fn to_shared(&self) -> $borrowed<'_> {
176                match self {
177                    $owned::PixelValueParseError(e) => {
178                        $borrowed::PixelValueParseError(e.to_shared())
179                    }
180                    $owned::TooManyValues => $borrowed::TooManyValues,
181                    $owned::TooFewValues => $borrowed::TooFewValues,
182                }
183            }
184        }
185    };
186}
187
188// -- Padding Shorthand Parser --
189
190/// Error from parsing a CSS `padding` shorthand value.
191#[cfg(feature = "parser")]
192#[derive(Clone, PartialEq, Eq)]
193pub enum LayoutPaddingParseError<'a> {
194    PixelValueParseError(CssPixelValueParseError<'a>),
195    TooManyValues,
196    TooFewValues,
197}
198#[allow(variant_size_differences)]
199// repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
200/// Owned variant of [`LayoutPaddingParseError`].
201#[cfg(feature = "parser")]
202#[derive(Debug, Clone, PartialEq, Eq)]
203#[repr(C, u8)]
204pub enum LayoutPaddingParseErrorOwned {
205    PixelValueParseError(CssPixelValueParseErrorOwned),
206    TooManyValues,
207    TooFewValues,
208}
209
210#[cfg(feature = "parser")]
211impl_spacing_parse_error!(
212    LayoutPaddingParseError,
213    LayoutPaddingParseErrorOwned,
214    "padding"
215);
216
217/// Result of parsing the CSS `padding` shorthand property (1–4 values).
218#[cfg(feature = "parser")]
219#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
220pub struct LayoutPadding {
221    pub top: PixelValueWithAuto,
222    pub bottom: PixelValueWithAuto,
223    pub left: PixelValueWithAuto,
224    pub right: PixelValueWithAuto,
225}
226
227#[cfg(feature = "parser")]
228/// # Errors
229///
230/// Returns an error if `input` is not a valid CSS `padding` value.
231pub fn parse_layout_padding(input: &str) -> Result<LayoutPadding, LayoutPaddingParseError<'_>> {
232    let values: Vec<_> = input.split_whitespace().collect();
233
234    let parsed_values: Vec<PixelValueWithAuto> = values
235        .iter()
236        .map(|s| parse_pixel_value_with_auto(s))
237        .collect::<Result<_, _>>()?;
238
239    match parsed_values.len() {
240        1 => {
241            // top, right, bottom, left
242            let all = parsed_values[0];
243            Ok(LayoutPadding {
244                top: all,
245                right: all,
246                bottom: all,
247                left: all,
248            })
249        }
250        2 => {
251            // top/bottom, left/right
252            let vertical = parsed_values[0];
253            let horizontal = parsed_values[1];
254            Ok(LayoutPadding {
255                top: vertical,
256                right: horizontal,
257                bottom: vertical,
258                left: horizontal,
259            })
260        }
261        3 => {
262            // top, left/right, bottom
263            let top = parsed_values[0];
264            let horizontal = parsed_values[1];
265            let bottom = parsed_values[2];
266            Ok(LayoutPadding {
267                top,
268                right: horizontal,
269                bottom,
270                left: horizontal,
271            })
272        }
273        4 => {
274            // top, right, bottom, left
275            Ok(LayoutPadding {
276                top: parsed_values[0],
277                right: parsed_values[1],
278                bottom: parsed_values[2],
279                left: parsed_values[3],
280            })
281        }
282        0 => Err(LayoutPaddingParseError::TooFewValues),
283        _ => Err(LayoutPaddingParseError::TooManyValues),
284    }
285}
286
287// -- Margin Shorthand Parser --
288
289/// Error from parsing a CSS `margin` shorthand value.
290#[cfg(feature = "parser")]
291#[derive(Clone, PartialEq, Eq)]
292pub enum LayoutMarginParseError<'a> {
293    PixelValueParseError(CssPixelValueParseError<'a>),
294    TooManyValues,
295    TooFewValues,
296}
297#[allow(variant_size_differences)]
298// repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
299/// Owned variant of [`LayoutMarginParseError`].
300#[cfg(feature = "parser")]
301#[derive(Debug, Clone, PartialEq, Eq)]
302#[repr(C, u8)]
303pub enum LayoutMarginParseErrorOwned {
304    PixelValueParseError(CssPixelValueParseErrorOwned),
305    TooManyValues,
306    TooFewValues,
307}
308
309#[cfg(feature = "parser")]
310impl_spacing_parse_error!(
311    LayoutMarginParseError,
312    LayoutMarginParseErrorOwned,
313    "margin"
314);
315
316/// Result of parsing the CSS `margin` shorthand property (1–4 values).
317#[cfg(feature = "parser")]
318#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
319pub struct LayoutMargin {
320    pub top: PixelValueWithAuto,
321    pub bottom: PixelValueWithAuto,
322    pub left: PixelValueWithAuto,
323    pub right: PixelValueWithAuto,
324}
325
326#[cfg(feature = "parser")]
327/// # Errors
328///
329/// Returns an error if `input` is not a valid CSS `margin` value.
330pub fn parse_layout_margin(input: &str) -> Result<LayoutMargin, LayoutMarginParseError<'_>> {
331    // Margin parsing logic is identical to padding, so we can reuse the padding parser
332    // and just map the Ok and Err variants to the margin-specific types.
333    match parse_layout_padding(input) {
334        Ok(padding) => Ok(LayoutMargin {
335            top: padding.top,
336            left: padding.left,
337            right: padding.right,
338            bottom: padding.bottom,
339        }),
340        Err(e) => match e {
341            LayoutPaddingParseError::PixelValueParseError(err) => {
342                Err(LayoutMarginParseError::PixelValueParseError(err))
343            }
344            LayoutPaddingParseError::TooManyValues => Err(LayoutMarginParseError::TooManyValues),
345            LayoutPaddingParseError::TooFewValues => Err(LayoutMarginParseError::TooFewValues),
346        },
347    }
348}
349
350// -- Longhand Property Parsers --
351
352macro_rules! typed_pixel_value_parser {
353    (
354        $fn:ident, $fn_str:expr, $return:ident, $return_str:expr, $import_str:expr, $test_str:expr
355    ) => {
356        ///Parses a `
357        #[doc = $return_str]
358        ///` attribute from a `&str`
359        ///
360        ///# Example
361        ///
362        ///```rust
363        #[doc = $import_str]
364        #[doc = $test_str]
365        ///```
366        /// # Errors
367        ///
368        /// Returns an error if `input` is not a valid CSS value for this property.
369        pub fn $fn(input: &str) -> Result<$return, CssPixelValueParseError<'_>> {
370            crate::props::basic::parse_pixel_value(input).map(|e| $return { inner: e })
371        }
372
373        impl crate::props::formatter::FormatAsCssValue for $return {
374            fn format_as_css_value(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
375                self.inner.format_as_css_value(f)
376            }
377        }
378    };
379    ($fn:ident, $return:ident) => {
380        typed_pixel_value_parser!(
381            $fn,
382            stringify!($fn),
383            $return,
384            stringify!($return),
385            concat!(
386                "# extern crate azul_css;",
387                "\r\n",
388                "# use azul_css::props::layout::spacing::",
389                stringify!($fn),
390                ";",
391                "\r\n",
392                "# use azul_css::props::basic::pixel::PixelValue;\r\n",
393                "# use azul_css::props::layout::spacing::",
394                stringify!($return),
395                ";\r\n"
396            ),
397            concat!(
398                "assert_eq!(",
399                stringify!($fn),
400                "(\"5px\"), Ok(",
401                stringify!($return),
402                " { inner: PixelValue::px(5.0) }));"
403            )
404        );
405    };
406}
407
408#[cfg(feature = "parser")]
409typed_pixel_value_parser!(parse_layout_padding_top, LayoutPaddingTop);
410#[cfg(feature = "parser")]
411typed_pixel_value_parser!(parse_layout_padding_right, LayoutPaddingRight);
412#[cfg(feature = "parser")]
413typed_pixel_value_parser!(parse_layout_padding_bottom, LayoutPaddingBottom);
414#[cfg(feature = "parser")]
415typed_pixel_value_parser!(parse_layout_padding_left, LayoutPaddingLeft);
416#[cfg(feature = "parser")]
417typed_pixel_value_parser!(parse_layout_padding_inline_start, LayoutPaddingInlineStart);
418#[cfg(feature = "parser")]
419typed_pixel_value_parser!(parse_layout_padding_inline_end, LayoutPaddingInlineEnd);
420
421#[cfg(feature = "parser")]
422typed_pixel_value_parser!(parse_layout_margin_top, LayoutMarginTop);
423#[cfg(feature = "parser")]
424typed_pixel_value_parser!(parse_layout_margin_right, LayoutMarginRight);
425#[cfg(feature = "parser")]
426typed_pixel_value_parser!(parse_layout_margin_bottom, LayoutMarginBottom);
427#[cfg(feature = "parser")]
428typed_pixel_value_parser!(parse_layout_margin_left, LayoutMarginLeft);
429
430#[cfg(feature = "parser")]
431typed_pixel_value_parser!(parse_layout_column_gap, LayoutColumnGap);
432#[cfg(feature = "parser")]
433typed_pixel_value_parser!(parse_layout_row_gap, LayoutRowGap);
434
435#[cfg(all(test, feature = "parser"))]
436mod tests {
437    use super::*;
438    use crate::props::basic::pixel::{PixelValue, PixelValueWithAuto};
439
440    #[test]
441    fn test_parse_layout_padding_shorthand() {
442        // 1 value
443        let result = parse_layout_padding("10px").unwrap();
444        assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(10.0)));
445        assert_eq!(
446            result.right,
447            PixelValueWithAuto::Exact(PixelValue::px(10.0))
448        );
449        assert_eq!(
450            result.bottom,
451            PixelValueWithAuto::Exact(PixelValue::px(10.0))
452        );
453        assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::px(10.0)));
454
455        // 2 values
456        let result = parse_layout_padding("5% 2em").unwrap();
457        assert_eq!(
458            result.top,
459            PixelValueWithAuto::Exact(PixelValue::percent(5.0))
460        );
461        assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::em(2.0)));
462        assert_eq!(
463            result.bottom,
464            PixelValueWithAuto::Exact(PixelValue::percent(5.0))
465        );
466        assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::em(2.0)));
467
468        // 3 values
469        let result = parse_layout_padding("1px 2px 3px").unwrap();
470        assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(1.0)));
471        assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
472        assert_eq!(
473            result.bottom,
474            PixelValueWithAuto::Exact(PixelValue::px(3.0))
475        );
476        assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
477
478        // 4 values
479        let result = parse_layout_padding("1px 2px 3px 4px").unwrap();
480        assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(1.0)));
481        assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
482        assert_eq!(
483            result.bottom,
484            PixelValueWithAuto::Exact(PixelValue::px(3.0))
485        );
486        assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::px(4.0)));
487
488        // Whitespace
489        let result = parse_layout_padding("  1px   2px  ").unwrap();
490        assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(1.0)));
491        assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
492    }
493
494    #[test]
495    fn test_parse_layout_padding_errors() {
496        assert!(matches!(
497            parse_layout_padding("").err().unwrap(),
498            LayoutPaddingParseError::TooFewValues
499        ));
500        assert!(matches!(
501            parse_layout_padding("1px 2px 3px 4px 5px").err().unwrap(),
502            LayoutPaddingParseError::TooManyValues
503        ));
504        assert!(matches!(
505            parse_layout_padding("1px oops 3px").err().unwrap(),
506            LayoutPaddingParseError::PixelValueParseError(_)
507        ));
508    }
509
510    #[test]
511    fn test_parse_layout_margin_shorthand() {
512        // 1 value with auto
513        let result = parse_layout_margin("auto").unwrap();
514        assert_eq!(result.top, PixelValueWithAuto::Auto);
515        assert_eq!(result.right, PixelValueWithAuto::Auto);
516        assert_eq!(result.bottom, PixelValueWithAuto::Auto);
517        assert_eq!(result.left, PixelValueWithAuto::Auto);
518
519        // 2 values
520        let result = parse_layout_margin("10px auto").unwrap();
521        assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(10.0)));
522        assert_eq!(result.right, PixelValueWithAuto::Auto);
523        assert_eq!(
524            result.bottom,
525            PixelValueWithAuto::Exact(PixelValue::px(10.0))
526        );
527        assert_eq!(result.left, PixelValueWithAuto::Auto);
528    }
529
530    #[test]
531    fn test_parse_layout_margin_errors() {
532        assert!(matches!(
533            parse_layout_margin("").err().unwrap(),
534            LayoutMarginParseError::TooFewValues
535        ));
536        assert!(matches!(
537            parse_layout_margin("1px 2px 3px 4px 5px").err().unwrap(),
538            LayoutMarginParseError::TooManyValues
539        ));
540        assert!(matches!(
541            parse_layout_margin("1px invalid").err().unwrap(),
542            LayoutMarginParseError::PixelValueParseError(_)
543        ));
544    }
545
546    #[test]
547    fn test_parse_longhand_spacing() {
548        assert_eq!(
549            parse_layout_padding_left("2em").unwrap(),
550            LayoutPaddingLeft {
551                inner: PixelValue::em(2.0)
552            }
553        );
554        assert!(parse_layout_margin_top("auto").is_err()); // Longhands don't parse "auto"
555        assert_eq!(
556            parse_layout_column_gap("20px").unwrap(),
557            LayoutColumnGap {
558                inner: PixelValue::px(20.0)
559            }
560        );
561    }
562}
563
564#[cfg(all(test, feature = "parser"))]
565mod autotest_generated {
566    #![allow(clippy::float_cmp)] // fixed-point quantisation makes exact f32 compares meaningful here
567
568    use std::collections::hash_map::DefaultHasher;
569
570    #[allow(clippy::wildcard_imports)]
571    use super::*;
572    use alloc::format;
573    use core::{
574        fmt,
575        hash::{Hash, Hasher},
576    };
577
578    use crate::props::{
579        basic::{
580            length::SizeMetric,
581            pixel::{CssPixelValueParseError, PixelValue, PixelValueWithAuto},
582        },
583        formatter::FormatAsCssValue,
584    };
585
586    /// Renders a value through `FormatAsCssValue` so it can be compared against the
587    /// `String`-returning `PrintAsCssValue` path.
588    #[allow(missing_debug_implementations)]
589    struct AsCss<'a, T: FormatAsCssValue>(&'a T);
590
591    impl<T: FormatAsCssValue> fmt::Display for AsCss<'_, T> {
592        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
593            self.0.format_as_css_value(f)
594        }
595    }
596
597    fn hash_of<T: Hash>(value: &T) -> u64 {
598        let mut hasher = DefaultHasher::new();
599        value.hash(&mut hasher);
600        hasher.finish()
601    }
602
603    fn exact_px(value: f32) -> PixelValueWithAuto {
604        PixelValueWithAuto::Exact(PixelValue::px(value))
605    }
606
607    /// `parse(print(parse(s))) == parse(s)` — the encode/decode fixed point. Also
608    /// checks the two formatting traits agree, since they are separate impls.
609    macro_rules! assert_css_roundtrip {
610        ($parse:ident, $input:expr) => {{
611            let parsed = $parse($input).expect("positive control must parse");
612            let printed = parsed.print_as_css_value();
613            let reparsed = $parse(printed.as_str()).expect("printed value must re-parse");
614            assert_eq!(
615                parsed,
616                reparsed,
617                "{} did not survive {:?} -> {:?}",
618                stringify!($parse),
619                $input,
620                printed
621            );
622            assert_eq!(
623                printed,
624                format!("{}", AsCss(&parsed)),
625                "FormatAsCssValue and PrintAsCssValue disagree for {:?}",
626                $input
627            );
628        }};
629    }
630
631    macro_rules! assert_all_longhands_err {
632        ($input:expr) => {{
633            assert!(
634                parse_layout_padding_top($input).is_err(),
635                "padding-top accepted {:?}",
636                $input
637            );
638            assert!(
639                parse_layout_padding_right($input).is_err(),
640                "padding-right accepted {:?}",
641                $input
642            );
643            assert!(
644                parse_layout_padding_bottom($input).is_err(),
645                "padding-bottom accepted {:?}",
646                $input
647            );
648            assert!(
649                parse_layout_padding_left($input).is_err(),
650                "padding-left accepted {:?}",
651                $input
652            );
653            assert!(
654                parse_layout_padding_inline_start($input).is_err(),
655                "padding-inline-start accepted {:?}",
656                $input
657            );
658            assert!(
659                parse_layout_padding_inline_end($input).is_err(),
660                "padding-inline-end accepted {:?}",
661                $input
662            );
663            assert!(
664                parse_layout_margin_top($input).is_err(),
665                "margin-top accepted {:?}",
666                $input
667            );
668            assert!(
669                parse_layout_margin_right($input).is_err(),
670                "margin-right accepted {:?}",
671                $input
672            );
673            assert!(
674                parse_layout_margin_bottom($input).is_err(),
675                "margin-bottom accepted {:?}",
676                $input
677            );
678            assert!(
679                parse_layout_margin_left($input).is_err(),
680                "margin-left accepted {:?}",
681                $input
682            );
683            assert!(
684                parse_layout_column_gap($input).is_err(),
685                "column-gap accepted {:?}",
686                $input
687            );
688            assert!(
689                parse_layout_row_gap($input).is_err(),
690                "row-gap accepted {:?}",
691                $input
692            );
693        }};
694    }
695
696    // --- parsers: positive controls -----------------------------------------
697
698    #[test]
699    fn minimal_valid_inputs_parse_to_the_documented_values() {
700        assert_eq!(
701            parse_layout_padding("0").unwrap(),
702            LayoutPadding {
703                top: exact_px(0.0),
704                right: exact_px(0.0),
705                bottom: exact_px(0.0),
706                left: exact_px(0.0),
707            }
708        );
709        assert_eq!(
710            parse_layout_margin("1px").unwrap(),
711            LayoutMargin {
712                top: exact_px(1.0),
713                right: exact_px(1.0),
714                bottom: exact_px(1.0),
715                left: exact_px(1.0),
716            }
717        );
718    }
719
720    #[test]
721    fn shorthand_expansion_follows_the_css_1_to_4_value_rules() {
722        let one = parse_layout_padding("7px").unwrap();
723        assert_eq!(one.top, exact_px(7.0));
724        assert_eq!(one.right, one.top);
725        assert_eq!(one.bottom, one.top);
726        assert_eq!(one.left, one.top);
727
728        let two = parse_layout_padding("1px 2px").unwrap();
729        assert_eq!((two.top, two.bottom), (exact_px(1.0), exact_px(1.0)));
730        assert_eq!((two.right, two.left), (exact_px(2.0), exact_px(2.0)));
731
732        // The third value is the *bottom*, and left mirrors right. Getting this
733        // expansion backwards is the classic shorthand bug, so pin all four sides.
734        let three = parse_layout_padding("1px 2px 3px").unwrap();
735        assert_eq!(three.top, exact_px(1.0));
736        assert_eq!(three.right, exact_px(2.0));
737        assert_eq!(three.bottom, exact_px(3.0));
738        assert_eq!(three.left, exact_px(2.0));
739
740        // Four values run clockwise: top, right, bottom, left.
741        let four = parse_layout_padding("1px 2px 3px 4px").unwrap();
742        assert_eq!(four.top, exact_px(1.0));
743        assert_eq!(four.right, exact_px(2.0));
744        assert_eq!(four.bottom, exact_px(3.0));
745        assert_eq!(four.left, exact_px(4.0));
746    }
747
748    #[test]
749    fn margin_is_a_faithful_mirror_of_padding() {
750        for input in [
751            "0",
752            "10px",
753            "5% 2em",
754            "1px 2px 3px",
755            "1px 2px 3px 4px",
756            "auto",
757            "10px auto",
758            "auto 0 inherit 2em",
759        ] {
760            let p = parse_layout_padding(input).unwrap();
761            let m = parse_layout_margin(input).unwrap();
762            assert_eq!(m.top, p.top, "top differs for {input:?}");
763            assert_eq!(m.right, p.right, "right differs for {input:?}");
764            assert_eq!(m.bottom, p.bottom, "bottom differs for {input:?}");
765            assert_eq!(m.left, p.left, "left differs for {input:?}");
766        }
767
768        // ...and every error variant maps 1:1 through the delegation.
769        assert!(matches!(
770            parse_layout_margin(""),
771            Err(LayoutMarginParseError::TooFewValues)
772        ));
773        assert!(matches!(
774            parse_layout_margin("1 2 3 4 5"),
775            Err(LayoutMarginParseError::TooManyValues)
776        ));
777        assert!(matches!(
778            parse_layout_margin("nope"),
779            Err(LayoutMarginParseError::PixelValueParseError(_))
780        ));
781    }
782
783    // --- parsers: malformed / boundary / unicode ----------------------------
784
785    #[test]
786    fn empty_and_whitespace_only_input_is_too_few_values() {
787        for input in [
788            "", " ", "   ", "\t", "\n", "\r\n", "\x0b", "\x0c", " \t\r\n ",
789        ] {
790            assert!(
791                matches!(
792                    parse_layout_padding(input),
793                    Err(LayoutPaddingParseError::TooFewValues)
794                ),
795                "padding {input:?}"
796            );
797            assert!(
798                matches!(
799                    parse_layout_margin(input),
800                    Err(LayoutMarginParseError::TooFewValues)
801                ),
802                "margin {input:?}"
803            );
804        }
805    }
806
807    #[test]
808    fn garbage_is_rejected_without_panicking() {
809        for input in [
810            "oops",
811            "px",
812            "%",
813            "-",
814            "+",
815            ".",
816            "e",
817            "--",
818            "10px;",
819            "10px,20px",
820            "10px, 20px",
821            "10px!important",
822            "calc(1px + 2px)",
823            "1px/2px",
824            "#10px",
825            "0x10px",
826            "1_000px",
827            "auto auto auto auto auto",
828        ] {
829            assert!(
830                parse_layout_padding(input).is_err(),
831                "padding accepted {input:?}"
832            );
833            assert!(
834                parse_layout_margin(input).is_err(),
835                "margin accepted {input:?}"
836            );
837        }
838    }
839
840    #[test]
841    fn shorthand_rejects_a_unit_split_from_its_number() {
842        // `parse_pixel_value` trims *inside* a token, so "10 px" is a valid longhand.
843        // The shorthand splits on whitespace first, so the same text is two values
844        // and the bare unit is what fails -- a divergence worth pinning.
845        let err = parse_layout_padding("10 px").unwrap_err();
846        assert!(
847            matches!(
848                err,
849                LayoutPaddingParseError::PixelValueParseError(
850                    CssPixelValueParseError::NoValueGiven("px", SizeMetric::Px)
851                )
852            ),
853            "expected NoValueGiven(\"px\", Px), got {err:?}"
854        );
855        assert_eq!(
856            parse_layout_padding_top("10 px").unwrap(),
857            LayoutPaddingTop::px(10.0)
858        );
859    }
860
861    #[test]
862    fn value_errors_are_reported_before_arity_errors() {
863        // Every token is parsed before the count is checked, so a malformed value in
864        // an over-long list surfaces as a value error, not TooManyValues.
865        assert!(matches!(
866            parse_layout_padding("1px 2px 3px 4px 5px"),
867            Err(LayoutPaddingParseError::TooManyValues)
868        ));
869        assert!(matches!(
870            parse_layout_padding("1px 2px 3px 4px bogus"),
871            Err(LayoutPaddingParseError::PixelValueParseError(_))
872        ));
873        assert!(matches!(
874            parse_layout_margin("1px 2px 3px 4px bogus"),
875            Err(LayoutMarginParseError::PixelValueParseError(_))
876        ));
877    }
878
879    #[test]
880    fn boundary_numbers_saturate_instead_of_overflowing() {
881        // Signed zero collapses onto one encoding.
882        let zero = parse_layout_padding("0").unwrap();
883        assert_eq!(parse_layout_padding("-0").unwrap(), zero);
884        assert_eq!(zero.top, PixelValueWithAuto::Exact(PixelValue::zero()));
885
886        // Overflowing literals become +/-inf in `f32::from_str` and must then saturate
887        // to the isize bounds: nothing infinite may escape into layout arithmetic.
888        for input in [
889            "1e39px",
890            "-1e39px",
891            "inf",
892            "-inf",
893            "infinity",
894            "9223372036854775807",
895            "-9223372036854775808",
896            "340282350000000000000000000000000000000px",
897        ] {
898            let parsed =
899                parse_layout_padding(input).unwrap_or_else(|e| panic!("{input:?} failed: {e}"));
900            let PixelValueWithAuto::Exact(value) = parsed.top else {
901                panic!("{input:?} did not parse to an exact length");
902            };
903            let raw = value.number.get();
904            assert!(
905                raw.is_finite(),
906                "{input:?} produced a non-finite length: {raw}"
907            );
908        }
909
910        // SPEC DIVERGENCE (pinned, not endorsed): CSS has no `NaN` value, but Rust's
911        // `f32::from_str` accepts it, so `padding: NaN` parses. The saturating cast at
912        // least keeps it safe -- it quantises to exactly 0px rather than poisoning
913        // layout with a NaN.
914        let nan = parse_layout_padding("NaN").unwrap();
915        assert_eq!(nan.top, PixelValueWithAuto::Exact(PixelValue::zero()));
916        assert_eq!(nan.right, nan.top);
917        assert_eq!(nan.bottom, nan.top);
918        assert_eq!(nan.left, nan.top);
919    }
920
921    #[test]
922    fn sub_milli_unit_values_truncate_toward_zero() {
923        // Lengths are stored as thousandths in an isize, and the cast truncates.
924        assert_eq!(
925            parse_layout_padding_top("0.0004px").unwrap(),
926            LayoutPaddingTop::zero()
927        );
928        assert_eq!(
929            parse_layout_padding_top("-0.0009px").unwrap(),
930            LayoutPaddingTop::zero()
931        );
932        assert_eq!(
933            parse_layout_padding_top("1e-40px").unwrap(),
934            LayoutPaddingTop::zero()
935        );
936        // Truncation, not rounding: 1.9999px stays below 2px.
937        assert_eq!(
938            parse_layout_padding_top("1.9999px")
939                .unwrap()
940                .inner
941                .number
942                .get(),
943            1.999
944        );
945    }
946
947    #[test]
948    fn unicode_junk_is_rejected_without_panicking() {
949        for input in [
950            "\u{1F600}",                        // emoji alone
951            "10px\u{1F600}",                    // emoji glued to a valid value
952            "\u{FF11}\u{FF10}\u{FF50}\u{FF58}", // fullwidth "10px"
953            "10px\u{0301}",                     // combining acute on the unit
954            "10\u{200b}px",                     // zero-width space inside the number
955            "\u{661}\u{660}px",                 // arabic-indic digits
956            "\u{202E}10px",                     // RTL-override prefix
957        ] {
958            assert!(
959                parse_layout_padding(input).is_err(),
960                "padding accepted {input:?}"
961            );
962            assert!(
963                parse_layout_margin(input).is_err(),
964                "margin accepted {input:?}"
965            );
966        }
967    }
968
969    #[test]
970    fn unicode_whitespace_separates_values_even_though_css_only_splits_on_ascii() {
971        // `split_whitespace()` follows the Unicode White_Space property, so U+00A0
972        // (NO-BREAK SPACE) and U+2003 (EM SPACE) split a declaration into two values,
973        // where CSS would treat the whole thing as one malformed token. Stated as an
974        // invariant against `split_whitespace()` itself, so the test pins the parser's
975        // contract instead of restating a Unicode table.
976        for sep in [" ", "\u{a0}", "\u{2003}"] {
977            let input = format!("10px{sep}20px");
978            let tokens = input.split_whitespace().count();
979            let parsed = parse_layout_padding(&input);
980            assert_eq!(
981                parsed.is_ok(),
982                tokens == 2,
983                "{input:?} split into {tokens} token(s)"
984            );
985            if let Ok(p) = parsed {
986                assert_eq!(p.top, exact_px(10.0));
987                assert_eq!(p.right, exact_px(20.0));
988                assert_eq!(p.bottom, p.top);
989                assert_eq!(p.left, p.right);
990            }
991        }
992    }
993
994    #[test]
995    fn extremely_long_inputs_terminate_without_panicking() {
996        // 200_000 well-formed values: an ordinary arity error, not a hang.
997        let many = "1px ".repeat(200_000);
998        assert!(matches!(
999            parse_layout_padding(&many),
1000            Err(LayoutPaddingParseError::TooManyValues)
1001        ));
1002        assert!(matches!(
1003            parse_layout_margin(&many),
1004            Err(LayoutMarginParseError::TooManyValues)
1005        ));
1006
1007        // A single 100_000-digit number overflows f32 to +inf, then saturates.
1008        let huge = format!("{}px", "9".repeat(100_000));
1009        let parsed = parse_layout_padding(&huge).unwrap();
1010        let PixelValueWithAuto::Exact(value) = parsed.top else {
1011            panic!("a huge number did not parse to an exact length");
1012        };
1013        assert!(value.number.get().is_finite());
1014        assert!(value.number.get() > 0.0);
1015
1016        // A 1_000_000-char garbage token is rejected, not scanned forever.
1017        let junk = "z".repeat(1_000_000);
1018        assert!(parse_layout_padding(&junk).is_err());
1019        assert!(parse_layout_margin(&junk).is_err());
1020    }
1021
1022    #[test]
1023    fn deeply_nested_brackets_do_not_stack_overflow() {
1024        // The grammar is flat, so nesting must be rejected by the float parser rather
1025        // than recursed into.
1026        let nested = format!("{}1px{}", "(".repeat(10_000), ")".repeat(10_000));
1027        assert!(
1028            matches!(
1029                parse_layout_padding(&nested),
1030                Err(LayoutPaddingParseError::PixelValueParseError(
1031                    CssPixelValueParseError::InvalidPixelValue(_)
1032                ))
1033            ),
1034            "deeply nested input was not rejected as an invalid pixel value"
1035        );
1036
1037        let spread = format!("{n} {n} {n} {n}", n = "(".repeat(1_000));
1038        assert!(parse_layout_padding(&spread).is_err());
1039        assert!(parse_layout_margin(&spread).is_err());
1040    }
1041
1042    #[test]
1043    fn shorthands_accept_css_wide_keywords_per_side() {
1044        // SPEC DIVERGENCE (pinned, not endorsed): the shorthands run every side through
1045        // `parse_pixel_value_with_auto`, so `padding: auto` parses (CSS has no such
1046        // value) and `initial`/`inherit` are accepted per side rather than only as a
1047        // whole declaration. Pinned so that tightening this is a visible change.
1048        assert_eq!(
1049            parse_layout_padding("auto").unwrap().top,
1050            PixelValueWithAuto::Auto
1051        );
1052        assert_eq!(
1053            parse_layout_padding("none").unwrap().top,
1054            PixelValueWithAuto::None
1055        );
1056
1057        let mixed = parse_layout_padding("initial 10px inherit auto").unwrap();
1058        assert_eq!(mixed.top, PixelValueWithAuto::Initial);
1059        assert_eq!(mixed.right, exact_px(10.0));
1060        assert_eq!(mixed.bottom, PixelValueWithAuto::Inherit);
1061        assert_eq!(mixed.left, PixelValueWithAuto::Auto);
1062    }
1063
1064    // --- errors -------------------------------------------------------------
1065
1066    #[test]
1067    fn arity_error_messages_name_the_right_property() {
1068        // `parse_layout_margin` delegates to the padding parser and re-wraps the error;
1069        // a mis-mapped variant would be invisible to callers but wrong for users.
1070        let pad_many = format!("{}", LayoutPaddingParseError::TooManyValues);
1071        let pad_few = format!("{}", LayoutPaddingParseError::TooFewValues);
1072        assert!(
1073            pad_many.contains("padding") && pad_many.contains("at most 4"),
1074            "{pad_many}"
1075        );
1076        assert!(pad_few.contains("padding"), "{pad_few}");
1077
1078        let margin_many = format!("{}", LayoutMarginParseError::TooManyValues);
1079        let margin_few = format!("{}", LayoutMarginParseError::TooFewValues);
1080        assert!(
1081            margin_many.contains("margin") && !margin_many.contains("padding"),
1082            "{margin_many}"
1083        );
1084        assert!(
1085            margin_few.contains("margin") && !margin_few.contains("padding"),
1086            "{margin_few}"
1087        );
1088
1089        // The live parser surfaces those same messages.
1090        assert_eq!(
1091            format!("{}", parse_layout_margin("1 2 3 4 5").unwrap_err()),
1092            margin_many
1093        );
1094        assert_eq!(
1095            format!("{}", parse_layout_padding("").unwrap_err()),
1096            pad_few
1097        );
1098    }
1099
1100    #[test]
1101    fn owned_and_shared_error_forms_round_trip() {
1102        assert_eq!(
1103            LayoutPaddingParseError::TooManyValues.to_contained(),
1104            LayoutPaddingParseErrorOwned::TooManyValues
1105        );
1106        assert_eq!(
1107            LayoutPaddingParseErrorOwned::TooFewValues.to_shared(),
1108            LayoutPaddingParseError::TooFewValues
1109        );
1110        assert_eq!(
1111            LayoutMarginParseError::TooFewValues.to_contained(),
1112            LayoutMarginParseErrorOwned::TooFewValues
1113        );
1114        assert_eq!(
1115            LayoutMarginParseErrorOwned::TooManyValues.to_shared(),
1116            LayoutMarginParseError::TooManyValues
1117        );
1118
1119        // The borrowed payload must survive the owned round-trip as the same variant,
1120        // even though it holds a `&str` into the (now dropped) input.
1121        let owned = parse_layout_padding("1px oops").unwrap_err().to_contained();
1122        assert!(matches!(
1123            &owned,
1124            LayoutPaddingParseErrorOwned::PixelValueParseError(_)
1125        ));
1126        assert!(matches!(
1127            owned.to_shared(),
1128            LayoutPaddingParseError::PixelValueParseError(_)
1129        ));
1130
1131        let owned_margin = parse_layout_margin("1px oops").unwrap_err().to_contained();
1132        assert!(matches!(
1133            owned_margin.to_shared(),
1134            LayoutMarginParseError::PixelValueParseError(_)
1135        ));
1136    }
1137
1138    // --- longhands: parse + round-trip --------------------------------------
1139
1140    #[test]
1141    fn longhand_parsers_reject_keywords_and_empty_input() {
1142        // The longhands go through `parse_pixel_value`, which -- unlike the shorthands
1143        // -- has no keyword table.
1144        for input in [
1145            "",
1146            "   ",
1147            "auto",
1148            "none",
1149            "initial",
1150            "inherit",
1151            "oops",
1152            "10px 20px",
1153        ] {
1154            assert_all_longhands_err!(input);
1155        }
1156    }
1157
1158    #[test]
1159    fn every_longhand_spacing_parser_accepts_a_minimal_value() {
1160        assert_eq!(
1161            parse_layout_padding_top("0").unwrap(),
1162            LayoutPaddingTop::px(0.0)
1163        );
1164        assert_eq!(
1165            parse_layout_padding_right("1px").unwrap(),
1166            LayoutPaddingRight::px(1.0)
1167        );
1168        assert_eq!(
1169            parse_layout_padding_bottom("2pt").unwrap(),
1170            LayoutPaddingBottom::pt(2.0)
1171        );
1172        assert_eq!(
1173            parse_layout_padding_left("2em").unwrap(),
1174            LayoutPaddingLeft::em(2.0)
1175        );
1176        assert_eq!(
1177            parse_layout_padding_inline_start("3px").unwrap(),
1178            LayoutPaddingInlineStart::px(3.0)
1179        );
1180        assert_eq!(
1181            parse_layout_padding_inline_end("4px").unwrap(),
1182            LayoutPaddingInlineEnd::px(4.0)
1183        );
1184        assert_eq!(
1185            parse_layout_margin_top("-5px").unwrap(),
1186            LayoutMarginTop::px(-5.0)
1187        );
1188        assert_eq!(
1189            parse_layout_margin_right("6%").unwrap(),
1190            LayoutMarginRight::percent(6.0)
1191        );
1192        assert_eq!(
1193            parse_layout_margin_bottom("7px").unwrap(),
1194            LayoutMarginBottom::px(7.0)
1195        );
1196        assert_eq!(
1197            parse_layout_margin_left("8px").unwrap(),
1198            LayoutMarginLeft::px(8.0)
1199        );
1200        assert_eq!(
1201            parse_layout_column_gap("20px").unwrap(),
1202            LayoutColumnGap::px(20.0)
1203        );
1204        assert_eq!(
1205            parse_layout_row_gap("1.5em").unwrap(),
1206            LayoutRowGap::em(1.5)
1207        );
1208    }
1209
1210    #[test]
1211    fn every_longhand_spacing_parser_round_trips_through_its_printed_form() {
1212        // Only exactly-representable values here: the point is the encode/decode fixed
1213        // point, not the quantisation (covered by `sub_milli_unit_values_*`).
1214        for input in [
1215            "0", "1px", "10.5px", "1.5em", "2rem", "-20pt", "50%", "0.125px", "3.25in", "12.75mm",
1216            "2.54cm", "0.5vmin", "4vmax", "8vw", "100vh",
1217        ] {
1218            assert_css_roundtrip!(parse_layout_padding_top, input);
1219            assert_css_roundtrip!(parse_layout_padding_right, input);
1220            assert_css_roundtrip!(parse_layout_padding_bottom, input);
1221            assert_css_roundtrip!(parse_layout_padding_left, input);
1222            assert_css_roundtrip!(parse_layout_padding_inline_start, input);
1223            assert_css_roundtrip!(parse_layout_padding_inline_end, input);
1224            assert_css_roundtrip!(parse_layout_margin_top, input);
1225            assert_css_roundtrip!(parse_layout_margin_right, input);
1226            assert_css_roundtrip!(parse_layout_margin_bottom, input);
1227            assert_css_roundtrip!(parse_layout_margin_left, input);
1228            assert_css_roundtrip!(parse_layout_column_gap, input);
1229            assert_css_roundtrip!(parse_layout_row_gap, input);
1230        }
1231    }
1232
1233    #[test]
1234    fn printed_css_matches_the_source_text_for_representable_values() {
1235        assert_eq!(
1236            parse_layout_padding_top("10px")
1237                .unwrap()
1238                .print_as_css_value(),
1239            "10px"
1240        );
1241        assert_eq!(
1242            parse_layout_column_gap("50%").unwrap().print_as_css_value(),
1243            "50%"
1244        );
1245        assert_eq!(
1246            parse_layout_margin_left("-2.5em")
1247                .unwrap()
1248                .print_as_css_value(),
1249            "-2.5em"
1250        );
1251        assert_eq!(LayoutRowGap::zero().print_as_css_value(), "0px");
1252        // A unitless number is a px length, and prints back *with* the unit.
1253        assert_eq!(
1254            parse_layout_padding_bottom("3")
1255                .unwrap()
1256                .print_as_css_value(),
1257            "3px"
1258        );
1259    }
1260
1261    // --- constructors, ordering, hashing, interpolation ----------------------
1262
1263    #[test]
1264    fn const_and_runtime_constructors_agree() {
1265        assert_eq!(LayoutPaddingLeft::const_px(5), LayoutPaddingLeft::px(5.0));
1266        assert_eq!(LayoutPaddingLeft::const_em(2), LayoutPaddingLeft::em(2.0));
1267        assert_eq!(LayoutPaddingLeft::const_pt(-3), LayoutPaddingLeft::pt(-3.0));
1268        assert_eq!(
1269            LayoutPaddingLeft::const_percent(50),
1270            LayoutPaddingLeft::percent(50.0)
1271        );
1272        assert_eq!(
1273            LayoutColumnGap::const_from_metric(SizeMetric::Vh, 7),
1274            LayoutColumnGap::from_metric(SizeMetric::Vh, 7.0)
1275        );
1276        assert_eq!(
1277            LayoutColumnGap::const_in(1),
1278            LayoutColumnGap::from_metric(SizeMetric::In, 1.0)
1279        );
1280        assert_eq!(
1281            LayoutColumnGap::const_cm(2),
1282            LayoutColumnGap::from_metric(SizeMetric::Cm, 2.0)
1283        );
1284        assert_eq!(
1285            LayoutColumnGap::const_mm(3),
1286            LayoutColumnGap::from_metric(SizeMetric::Mm, 3.0)
1287        );
1288
1289        // `PixelValueTaker` is what the shorthand macros build these types through.
1290        assert_eq!(
1291            LayoutRowGap::from_pixel_value(PixelValue::em(2.0)).inner,
1292            PixelValue::em(2.0)
1293        );
1294
1295        assert_eq!(LayoutPaddingBottom::zero(), LayoutPaddingBottom::default());
1296        assert_eq!(
1297            LayoutPaddingBottom::default().inner.metric,
1298            SizeMetric::Px,
1299            "the default spacing metric is px"
1300        );
1301    }
1302
1303    #[test]
1304    fn ordering_is_metric_major_not_physical_length() {
1305        // `Ord` is derived over (metric, number), so 1000px sorts *below* 0pt. Anything
1306        // ranking spacing by real size has to resolve to px first -- this is a trap, and
1307        // the test exists to state it.
1308        assert!(LayoutPaddingTop::px(1000.0) < LayoutPaddingTop::pt(0.0));
1309        assert!(LayoutPaddingTop::pt(0.0) < LayoutPaddingTop::em(0.0));
1310
1311        // Within one metric the ordering is numeric, as expected.
1312        assert!(LayoutPaddingTop::px(1.0) < LayoutPaddingTop::px(2.0));
1313        assert!(LayoutMarginLeft::const_px(-5) < LayoutMarginLeft::zero());
1314    }
1315
1316    #[test]
1317    fn equal_values_hash_equal_across_signed_zero_and_quantisation() {
1318        let pos = LayoutMarginTop::px(0.0);
1319        let neg = LayoutMarginTop::px(-0.0);
1320        assert_eq!(pos, neg, "signed zero must have one canonical encoding");
1321        assert_eq!(hash_of(&pos), hash_of(&neg));
1322        assert_eq!(pos, LayoutMarginTop::zero());
1323
1324        // Two values that quantise to the same thousandth are Eq, so they must hash
1325        // alike -- otherwise they would behave inconsistently as HashMap keys.
1326        let a = LayoutMarginTop::px(1.0001);
1327        let b = LayoutMarginTop::px(1.0009);
1328        assert_eq!(a, b);
1329        assert_eq!(hash_of(&a), hash_of(&b));
1330
1331        // Same number, different metric: not equal.
1332        assert_ne!(LayoutMarginTop::px(1.0), LayoutMarginTop::em(1.0));
1333    }
1334
1335    #[test]
1336    fn debug_renders_the_value_as_css() {
1337        assert_eq!(format!("{:?}", LayoutPaddingTop::px(10.0)), "10px");
1338        assert_eq!(format!("{:?}", LayoutColumnGap::percent(50.0)), "50%");
1339        assert_eq!(format!("{:?}", LayoutRowGap::zero()), "0px");
1340    }
1341
1342    #[test]
1343    fn interpolate_hits_its_endpoints_and_survives_nan_and_huge_t() {
1344        let a = LayoutRowGap::px(0.0);
1345        let b = LayoutRowGap::px(10.0);
1346        assert_eq!(a.interpolate(&b, 0.0), a);
1347        assert_eq!(a.interpolate(&b, 1.0), b);
1348        assert_eq!(a.interpolate(&b, 0.5), LayoutRowGap::px(5.0));
1349
1350        // A NaN `t` (a degenerate zero-length animation, say) must not leak a NaN length
1351        // into layout: the saturating cast maps it to 0.
1352        let nan = a.interpolate(&b, f32::NAN);
1353        assert_eq!(nan.inner.metric, SizeMetric::Px);
1354        assert_eq!(nan.inner.number.get(), 0.0);
1355
1356        // Out-of-range `t` saturates rather than wrapping the fixed-point encoding.
1357        for t in [1e30_f32, -1e30_f32, f32::INFINITY, f32::NEG_INFINITY] {
1358            let out = a.interpolate(&b, t);
1359            assert!(
1360                out.inner.number.get().is_finite(),
1361                "t = {t} produced a non-finite length"
1362            );
1363        }
1364
1365        // Mismatched metrics fall back to px instead of silently keeping the left metric.
1366        let mixed = LayoutRowGap::px(0.0).interpolate(&LayoutRowGap::em(1.0), 1.0);
1367        assert_eq!(mixed.inner.metric, SizeMetric::Px);
1368        assert!(mixed.inner.number.get().is_finite());
1369    }
1370}