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