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

            
6
use alloc::{
7
    string::{String, ToString},
8
    vec::Vec,
9
};
10

            
11
#[cfg(feature = "parser")]
12
use crate::props::basic::pixel::{parse_pixel_value_with_auto, PixelValueWithAuto};
13
use 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

            
25
macro_rules! impl_spacing_type_impls {
26
    ($name:ident) => {
27
        impl ::core::fmt::Debug for $name {
28
1299
            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
29
1299
                write!(f, "{}", self.inner)
30
1299
            }
31
        }
32

            
33
        impl PixelValueTaker for $name {
34
1
            fn from_pixel_value(inner: PixelValue) -> Self {
35
1
                Self { inner }
36
1
            }
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)]
46
pub struct LayoutPaddingTop {
47
    pub inner: PixelValue,
48
}
49
impl_spacing_type_impls!(LayoutPaddingTop);
50

            
51
/// Layout padding right value
52
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
53
#[repr(C)]
54
pub struct LayoutPaddingRight {
55
    pub inner: PixelValue,
56
}
57
impl_spacing_type_impls!(LayoutPaddingRight);
58

            
59
/// Layout padding bottom value
60
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
61
#[repr(C)]
62
pub struct LayoutPaddingBottom {
63
    pub inner: PixelValue,
64
}
65
impl_spacing_type_impls!(LayoutPaddingBottom);
66

            
67
/// Layout padding left value
68
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
69
#[repr(C)]
70
pub struct LayoutPaddingLeft {
71
    pub inner: PixelValue,
72
}
73
impl_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)]
78
pub struct LayoutPaddingInlineStart {
79
    pub inner: PixelValue,
80
}
81
impl_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)]
86
pub struct LayoutPaddingInlineEnd {
87
    pub inner: PixelValue,
88
}
89
impl_spacing_type_impls!(LayoutPaddingInlineEnd);
90

            
91
/// Layout margin top value
92
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
93
#[repr(C)]
94
pub struct LayoutMarginTop {
95
    pub inner: PixelValue,
96
}
97
impl_spacing_type_impls!(LayoutMarginTop);
98

            
99
/// Layout margin right value
100
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
101
#[repr(C)]
102
pub struct LayoutMarginRight {
103
    pub inner: PixelValue,
104
}
105
impl_spacing_type_impls!(LayoutMarginRight);
106

            
107
/// Layout margin bottom value
108
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
109
#[repr(C)]
110
pub struct LayoutMarginBottom {
111
    pub inner: PixelValue,
112
}
113
impl_spacing_type_impls!(LayoutMarginBottom);
114

            
115
/// Layout margin left value
116
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
117
#[repr(C)]
118
pub struct LayoutMarginLeft {
119
    pub inner: PixelValue,
120
}
121
impl_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)]
126
pub struct LayoutColumnGap {
127
    pub inner: PixelValue,
128
}
129
impl_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)]
134
pub struct LayoutRowGap {
135
    pub inner: PixelValue,
136
}
137
impl_spacing_type_impls!(LayoutRowGap);
138

            
139
// --- PARSERS ---
140

            
141
#[cfg(feature = "parser")]
142
macro_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
4
            #[must_use] pub fn to_contained(&self) -> $owned {
163
4
                match self {
164
2
                    $borrowed::PixelValueParseError(e) => {
165
2
                        $owned::PixelValueParseError(e.to_contained())
166
                    }
167
1
                    $borrowed::TooManyValues => $owned::TooManyValues,
168
1
                    $borrowed::TooFewValues => $owned::TooFewValues,
169
                }
170
4
            }
171
        }
172

            
173
        #[cfg(feature = "parser")]
174
        impl $owned {
175
4
            #[must_use] pub fn to_shared(&self) -> $borrowed<'_> {
176
4
                match self {
177
2
                    $owned::PixelValueParseError(e) => {
178
2
                        $borrowed::PixelValueParseError(e.to_shared())
179
                    }
180
1
                    $owned::TooManyValues => $borrowed::TooManyValues,
181
1
                    $owned::TooFewValues => $borrowed::TooFewValues,
182
                }
183
4
            }
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)]
193
pub 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)]
203
pub enum LayoutPaddingParseErrorOwned {
204
    PixelValueParseError(CssPixelValueParseErrorOwned),
205
    TooManyValues,
206
    TooFewValues,
207
}
208

            
209
#[cfg(feature = "parser")]
210
impl_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)]
215
pub 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.
226
15448
pub fn parse_layout_padding(
227
15448
    input: &str,
228
15448
) -> Result<LayoutPadding, LayoutPaddingParseError<'_>> {
229
15448
    let values: Vec<_> = input.split_whitespace().collect();
230

            
231
15448
    let parsed_values: Vec<PixelValueWithAuto> = values
232
15448
        .iter()
233
436156
        .map(|s| parse_pixel_value_with_auto(s))
234
15448
        .collect::<Result<_, _>>()?;
235

            
236
15376
    match parsed_values.len() {
237
        1 => {
238
            // top, right, bottom, left
239
14707
            let all = parsed_values[0];
240
14707
            Ok(LayoutPadding {
241
14707
                top: all,
242
14707
                right: all,
243
14707
                bottom: all,
244
14707
                left: all,
245
14707
            })
246
        }
247
        2 => {
248
            // top/bottom, left/right
249
593
            let vertical = parsed_values[0];
250
593
            let horizontal = parsed_values[1];
251
593
            Ok(LayoutPadding {
252
593
                top: vertical,
253
593
                right: horizontal,
254
593
                bottom: vertical,
255
593
                left: horizontal,
256
593
            })
257
        }
258
        3 => {
259
            // top, left/right, bottom
260
18
            let top = parsed_values[0];
261
18
            let horizontal = parsed_values[1];
262
18
            let bottom = parsed_values[2];
263
18
            Ok(LayoutPadding {
264
18
                top,
265
18
                right: horizontal,
266
18
                bottom,
267
18
                left: horizontal,
268
18
            })
269
        }
270
        4 => {
271
            // top, right, bottom, left
272
21
            Ok(LayoutPadding {
273
21
                top: parsed_values[0],
274
21
                right: parsed_values[1],
275
21
                bottom: parsed_values[2],
276
21
                left: parsed_values[3],
277
21
            })
278
        }
279
26
        0 => Err(LayoutPaddingParseError::TooFewValues),
280
11
        _ => Err(LayoutPaddingParseError::TooManyValues),
281
    }
282
15448
}
283

            
284
// -- Margin Shorthand Parser --
285

            
286
/// Error from parsing a CSS `margin` shorthand value.
287
#[cfg(feature = "parser")]
288
#[derive(Clone, PartialEq, Eq)]
289
pub 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)]
299
pub enum LayoutMarginParseErrorOwned {
300
    PixelValueParseError(CssPixelValueParseErrorOwned),
301
    TooManyValues,
302
    TooFewValues,
303
}
304

            
305
#[cfg(feature = "parser")]
306
impl_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)]
311
pub 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.
322
7584
pub 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
7584
    match parse_layout_padding(input) {
326
7521
        Ok(padding) => Ok(LayoutMargin {
327
7521
            top: padding.top,
328
7521
            left: padding.left,
329
7521
            right: padding.right,
330
7521
            bottom: padding.bottom,
331
7521
        }),
332
63
        Err(e) => match e {
333
41
            LayoutPaddingParseError::PixelValueParseError(err) => {
334
41
                Err(LayoutMarginParseError::PixelValueParseError(err))
335
            }
336
7
            LayoutPaddingParseError::TooManyValues => Err(LayoutMarginParseError::TooManyValues),
337
15
            LayoutPaddingParseError::TooFewValues => Err(LayoutMarginParseError::TooFewValues),
338
        },
339
    }
340
7584
}
341

            
342
// -- Longhand Property Parsers --
343

            
344
macro_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
37932
        pub fn $fn(input: &str) -> Result<$return, CssPixelValueParseError<'_>> {
362
37932
            crate::props::basic::parse_pixel_value(input).map(|e| $return { inner: e })
363
37932
        }
364

            
365
        impl crate::props::formatter::FormatAsCssValue for $return {
366
180
            fn format_as_css_value(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
367
180
                self.inner.format_as_css_value(f)
368
180
            }
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")]
401
typed_pixel_value_parser!(parse_layout_padding_top, LayoutPaddingTop);
402
#[cfg(feature = "parser")]
403
typed_pixel_value_parser!(parse_layout_padding_right, LayoutPaddingRight);
404
#[cfg(feature = "parser")]
405
typed_pixel_value_parser!(parse_layout_padding_bottom, LayoutPaddingBottom);
406
#[cfg(feature = "parser")]
407
typed_pixel_value_parser!(parse_layout_padding_left, LayoutPaddingLeft);
408
#[cfg(feature = "parser")]
409
typed_pixel_value_parser!(parse_layout_padding_inline_start, LayoutPaddingInlineStart);
410
#[cfg(feature = "parser")]
411
typed_pixel_value_parser!(parse_layout_padding_inline_end, LayoutPaddingInlineEnd);
412

            
413
#[cfg(feature = "parser")]
414
typed_pixel_value_parser!(parse_layout_margin_top, LayoutMarginTop);
415
#[cfg(feature = "parser")]
416
typed_pixel_value_parser!(parse_layout_margin_right, LayoutMarginRight);
417
#[cfg(feature = "parser")]
418
typed_pixel_value_parser!(parse_layout_margin_bottom, LayoutMarginBottom);
419
#[cfg(feature = "parser")]
420
typed_pixel_value_parser!(parse_layout_margin_left, LayoutMarginLeft);
421

            
422
#[cfg(feature = "parser")]
423
typed_pixel_value_parser!(parse_layout_column_gap, LayoutColumnGap);
424
#[cfg(feature = "parser")]
425
typed_pixel_value_parser!(parse_layout_row_gap, LayoutRowGap);
426

            
427
#[cfg(all(test, feature = "parser"))]
428
mod tests {
429
    use super::*;
430
    use crate::props::basic::pixel::{PixelValue, PixelValueWithAuto};
431

            
432
    #[test]
433
1
    fn test_parse_layout_padding_shorthand() {
434
        // 1 value
435
1
        let result = parse_layout_padding("10px").unwrap();
436
1
        assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(10.0)));
437
1
        assert_eq!(
438
            result.right,
439
1
            PixelValueWithAuto::Exact(PixelValue::px(10.0))
440
        );
441
1
        assert_eq!(
442
            result.bottom,
443
1
            PixelValueWithAuto::Exact(PixelValue::px(10.0))
444
        );
445
1
        assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::px(10.0)));
446

            
447
        // 2 values
448
1
        let result = parse_layout_padding("5% 2em").unwrap();
449
1
        assert_eq!(
450
            result.top,
451
1
            PixelValueWithAuto::Exact(PixelValue::percent(5.0))
452
        );
453
1
        assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::em(2.0)));
454
1
        assert_eq!(
455
            result.bottom,
456
1
            PixelValueWithAuto::Exact(PixelValue::percent(5.0))
457
        );
458
1
        assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::em(2.0)));
459

            
460
        // 3 values
461
1
        let result = parse_layout_padding("1px 2px 3px").unwrap();
462
1
        assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(1.0)));
463
1
        assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
464
1
        assert_eq!(
465
            result.bottom,
466
1
            PixelValueWithAuto::Exact(PixelValue::px(3.0))
467
        );
468
1
        assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
469

            
470
        // 4 values
471
1
        let result = parse_layout_padding("1px 2px 3px 4px").unwrap();
472
1
        assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(1.0)));
473
1
        assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
474
1
        assert_eq!(
475
            result.bottom,
476
1
            PixelValueWithAuto::Exact(PixelValue::px(3.0))
477
        );
478
1
        assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::px(4.0)));
479

            
480
        // Whitespace
481
1
        let result = parse_layout_padding("  1px   2px  ").unwrap();
482
1
        assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(1.0)));
483
1
        assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
484
1
    }
485

            
486
    #[test]
487
1
    fn test_parse_layout_padding_errors() {
488
1
        assert!(matches!(
489
1
            parse_layout_padding("").err().unwrap(),
490
            LayoutPaddingParseError::TooFewValues
491
        ));
492
1
        assert!(matches!(
493
1
            parse_layout_padding("1px 2px 3px 4px 5px").err().unwrap(),
494
            LayoutPaddingParseError::TooManyValues
495
        ));
496
1
        assert!(matches!(
497
1
            parse_layout_padding("1px oops 3px").err().unwrap(),
498
            LayoutPaddingParseError::PixelValueParseError(_)
499
        ));
500
1
    }
501

            
502
    #[test]
503
1
    fn test_parse_layout_margin_shorthand() {
504
        // 1 value with auto
505
1
        let result = parse_layout_margin("auto").unwrap();
506
1
        assert_eq!(result.top, PixelValueWithAuto::Auto);
507
1
        assert_eq!(result.right, PixelValueWithAuto::Auto);
508
1
        assert_eq!(result.bottom, PixelValueWithAuto::Auto);
509
1
        assert_eq!(result.left, PixelValueWithAuto::Auto);
510

            
511
        // 2 values
512
1
        let result = parse_layout_margin("10px auto").unwrap();
513
1
        assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(10.0)));
514
1
        assert_eq!(result.right, PixelValueWithAuto::Auto);
515
1
        assert_eq!(
516
            result.bottom,
517
1
            PixelValueWithAuto::Exact(PixelValue::px(10.0))
518
        );
519
1
        assert_eq!(result.left, PixelValueWithAuto::Auto);
520
1
    }
521

            
522
    #[test]
523
1
    fn test_parse_layout_margin_errors() {
524
1
        assert!(matches!(
525
1
            parse_layout_margin("").err().unwrap(),
526
            LayoutMarginParseError::TooFewValues
527
        ));
528
1
        assert!(matches!(
529
1
            parse_layout_margin("1px 2px 3px 4px 5px").err().unwrap(),
530
            LayoutMarginParseError::TooManyValues
531
        ));
532
1
        assert!(matches!(
533
1
            parse_layout_margin("1px invalid").err().unwrap(),
534
            LayoutMarginParseError::PixelValueParseError(_)
535
        ));
536
1
    }
537

            
538
    #[test]
539
1
    fn test_parse_longhand_spacing() {
540
1
        assert_eq!(
541
1
            parse_layout_padding_left("2em").unwrap(),
542
1
            LayoutPaddingLeft {
543
1
                inner: PixelValue::em(2.0)
544
1
            }
545
        );
546
1
        assert!(parse_layout_margin_top("auto").is_err()); // Longhands don't parse "auto"
547
1
        assert_eq!(
548
1
            parse_layout_column_gap("20px").unwrap(),
549
1
            LayoutColumnGap {
550
1
                inner: PixelValue::px(20.0)
551
1
            }
552
        );
553
1
    }
554
}
555

            
556
#[cfg(all(test, feature = "parser"))]
557
mod 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

            
    #[test]
    fn arity_error_messages_name_the_right_property() {
        // `parse_layout_margin` delegates to the padding parser and re-wraps the error;
        // a mis-mapped variant would be invisible to callers but wrong for users.
        let pad_many = format!("{}", LayoutPaddingParseError::TooManyValues);
        let pad_few = format!("{}", LayoutPaddingParseError::TooFewValues);
        assert!(
            pad_many.contains("padding") && pad_many.contains("at most 4"),
            "{pad_many}"
        );
        assert!(pad_few.contains("padding"), "{pad_few}");
        let margin_many = format!("{}", LayoutMarginParseError::TooManyValues);
        let margin_few = format!("{}", LayoutMarginParseError::TooFewValues);
        assert!(
            margin_many.contains("margin") && !margin_many.contains("padding"),
            "{margin_many}"
        );
        assert!(
            margin_few.contains("margin") && !margin_few.contains("padding"),
            "{margin_few}"
        );
        // The live parser surfaces those same messages.
        assert_eq!(
            format!("{}", parse_layout_margin("1 2 3 4 5").unwrap_err()),
            margin_many
        );
        assert_eq!(
            format!("{}", parse_layout_padding("").unwrap_err()),
            pad_few
        );
    }
    #[test]
    fn owned_and_shared_error_forms_round_trip() {
        assert_eq!(
            LayoutPaddingParseError::TooManyValues.to_contained(),
            LayoutPaddingParseErrorOwned::TooManyValues
        );
        assert_eq!(
            LayoutPaddingParseErrorOwned::TooFewValues.to_shared(),
            LayoutPaddingParseError::TooFewValues
        );
        assert_eq!(
            LayoutMarginParseError::TooFewValues.to_contained(),
            LayoutMarginParseErrorOwned::TooFewValues
        );
        assert_eq!(
            LayoutMarginParseErrorOwned::TooManyValues.to_shared(),
            LayoutMarginParseError::TooManyValues
        );
        // The borrowed payload must survive the owned round-trip as the same variant,
        // even though it holds a `&str` into the (now dropped) input.
        let owned = parse_layout_padding("1px oops").unwrap_err().to_contained();
        assert!(matches!(
            &owned,
            LayoutPaddingParseErrorOwned::PixelValueParseError(_)
        ));
        assert!(matches!(
            owned.to_shared(),
            LayoutPaddingParseError::PixelValueParseError(_)
        ));
        let owned_margin = parse_layout_margin("1px oops").unwrap_err().to_contained();
        assert!(matches!(
            owned_margin.to_shared(),
            LayoutMarginParseError::PixelValueParseError(_)
        ));
    }
    // --- longhands: parse + round-trip --------------------------------------
    #[test]
    fn longhand_parsers_reject_keywords_and_empty_input() {
        // The longhands go through `parse_pixel_value`, which -- unlike the shorthands
        // -- has no keyword table.
        for input in ["", "   ", "auto", "none", "initial", "inherit", "oops", "10px 20px"] {
            assert_all_longhands_err!(input);
        }
    }
    #[test]
    fn every_longhand_spacing_parser_accepts_a_minimal_value() {
        assert_eq!(parse_layout_padding_top("0").unwrap(), LayoutPaddingTop::px(0.0));
        assert_eq!(parse_layout_padding_right("1px").unwrap(), LayoutPaddingRight::px(1.0));
        assert_eq!(parse_layout_padding_bottom("2pt").unwrap(), LayoutPaddingBottom::pt(2.0));
        assert_eq!(parse_layout_padding_left("2em").unwrap(), LayoutPaddingLeft::em(2.0));
        assert_eq!(
            parse_layout_padding_inline_start("3px").unwrap(),
            LayoutPaddingInlineStart::px(3.0)
        );
        assert_eq!(
            parse_layout_padding_inline_end("4px").unwrap(),
            LayoutPaddingInlineEnd::px(4.0)
        );
        assert_eq!(parse_layout_margin_top("-5px").unwrap(), LayoutMarginTop::px(-5.0));
        assert_eq!(parse_layout_margin_right("6%").unwrap(), LayoutMarginRight::percent(6.0));
        assert_eq!(parse_layout_margin_bottom("7px").unwrap(), LayoutMarginBottom::px(7.0));
        assert_eq!(parse_layout_margin_left("8px").unwrap(), LayoutMarginLeft::px(8.0));
        assert_eq!(parse_layout_column_gap("20px").unwrap(), LayoutColumnGap::px(20.0));
        assert_eq!(parse_layout_row_gap("1.5em").unwrap(), LayoutRowGap::em(1.5));
    }
    #[test]
    fn every_longhand_spacing_parser_round_trips_through_its_printed_form() {
        // Only exactly-representable values here: the point is the encode/decode fixed
        // point, not the quantisation (covered by `sub_milli_unit_values_*`).
        for input in [
            "0", "1px", "10.5px", "1.5em", "2rem", "-20pt", "50%", "0.125px", "3.25in", "12.75mm",
            "2.54cm", "0.5vmin", "4vmax", "8vw", "100vh",
        ] {
            assert_css_roundtrip!(parse_layout_padding_top, input);
            assert_css_roundtrip!(parse_layout_padding_right, input);
            assert_css_roundtrip!(parse_layout_padding_bottom, input);
            assert_css_roundtrip!(parse_layout_padding_left, input);
            assert_css_roundtrip!(parse_layout_padding_inline_start, input);
            assert_css_roundtrip!(parse_layout_padding_inline_end, input);
            assert_css_roundtrip!(parse_layout_margin_top, input);
            assert_css_roundtrip!(parse_layout_margin_right, input);
            assert_css_roundtrip!(parse_layout_margin_bottom, input);
            assert_css_roundtrip!(parse_layout_margin_left, input);
            assert_css_roundtrip!(parse_layout_column_gap, input);
            assert_css_roundtrip!(parse_layout_row_gap, input);
        }
    }
    #[test]
    fn printed_css_matches_the_source_text_for_representable_values() {
        assert_eq!(
            parse_layout_padding_top("10px").unwrap().print_as_css_value(),
            "10px"
        );
        assert_eq!(
            parse_layout_column_gap("50%").unwrap().print_as_css_value(),
            "50%"
        );
        assert_eq!(
            parse_layout_margin_left("-2.5em").unwrap().print_as_css_value(),
            "-2.5em"
        );
        assert_eq!(LayoutRowGap::zero().print_as_css_value(), "0px");
        // A unitless number is a px length, and prints back *with* the unit.
        assert_eq!(
            parse_layout_padding_bottom("3").unwrap().print_as_css_value(),
            "3px"
        );
    }
    // --- constructors, ordering, hashing, interpolation ----------------------
    #[test]
    fn const_and_runtime_constructors_agree() {
        assert_eq!(LayoutPaddingLeft::const_px(5), LayoutPaddingLeft::px(5.0));
        assert_eq!(LayoutPaddingLeft::const_em(2), LayoutPaddingLeft::em(2.0));
        assert_eq!(LayoutPaddingLeft::const_pt(-3), LayoutPaddingLeft::pt(-3.0));
        assert_eq!(
            LayoutPaddingLeft::const_percent(50),
            LayoutPaddingLeft::percent(50.0)
        );
        assert_eq!(
            LayoutColumnGap::const_from_metric(SizeMetric::Vh, 7),
            LayoutColumnGap::from_metric(SizeMetric::Vh, 7.0)
        );
        assert_eq!(
            LayoutColumnGap::const_in(1),
            LayoutColumnGap::from_metric(SizeMetric::In, 1.0)
        );
        assert_eq!(
            LayoutColumnGap::const_cm(2),
            LayoutColumnGap::from_metric(SizeMetric::Cm, 2.0)
        );
        assert_eq!(
            LayoutColumnGap::const_mm(3),
            LayoutColumnGap::from_metric(SizeMetric::Mm, 3.0)
        );
        // `PixelValueTaker` is what the shorthand macros build these types through.
        assert_eq!(
            LayoutRowGap::from_pixel_value(PixelValue::em(2.0)).inner,
            PixelValue::em(2.0)
        );
        assert_eq!(LayoutPaddingBottom::zero(), LayoutPaddingBottom::default());
        assert_eq!(
            LayoutPaddingBottom::default().inner.metric,
            SizeMetric::Px,
            "the default spacing metric is px"
        );
    }
    #[test]
    fn ordering_is_metric_major_not_physical_length() {
        // `Ord` is derived over (metric, number), so 1000px sorts *below* 0pt. Anything
        // ranking spacing by real size has to resolve to px first -- this is a trap, and
        // the test exists to state it.
        assert!(LayoutPaddingTop::px(1000.0) < LayoutPaddingTop::pt(0.0));
        assert!(LayoutPaddingTop::pt(0.0) < LayoutPaddingTop::em(0.0));
        // Within one metric the ordering is numeric, as expected.
        assert!(LayoutPaddingTop::px(1.0) < LayoutPaddingTop::px(2.0));
        assert!(LayoutMarginLeft::const_px(-5) < LayoutMarginLeft::zero());
    }
    #[test]
    fn equal_values_hash_equal_across_signed_zero_and_quantisation() {
        let pos = LayoutMarginTop::px(0.0);
        let neg = LayoutMarginTop::px(-0.0);
        assert_eq!(pos, neg, "signed zero must have one canonical encoding");
        assert_eq!(hash_of(&pos), hash_of(&neg));
        assert_eq!(pos, LayoutMarginTop::zero());
        // Two values that quantise to the same thousandth are Eq, so they must hash
        // alike -- otherwise they would behave inconsistently as HashMap keys.
        let a = LayoutMarginTop::px(1.0001);
        let b = LayoutMarginTop::px(1.0009);
        assert_eq!(a, b);
        assert_eq!(hash_of(&a), hash_of(&b));
        // Same number, different metric: not equal.
        assert_ne!(LayoutMarginTop::px(1.0), LayoutMarginTop::em(1.0));
    }
    #[test]
    fn debug_renders_the_value_as_css() {
        assert_eq!(format!("{:?}", LayoutPaddingTop::px(10.0)), "10px");
        assert_eq!(format!("{:?}", LayoutColumnGap::percent(50.0)), "50%");
        assert_eq!(format!("{:?}", LayoutRowGap::zero()), "0px");
    }
    #[test]
    fn interpolate_hits_its_endpoints_and_survives_nan_and_huge_t() {
        let a = LayoutRowGap::px(0.0);
        let b = LayoutRowGap::px(10.0);
        assert_eq!(a.interpolate(&b, 0.0), a);
        assert_eq!(a.interpolate(&b, 1.0), b);
        assert_eq!(a.interpolate(&b, 0.5), LayoutRowGap::px(5.0));
        // A NaN `t` (a degenerate zero-length animation, say) must not leak a NaN length
        // into layout: the saturating cast maps it to 0.
        let nan = a.interpolate(&b, f32::NAN);
        assert_eq!(nan.inner.metric, SizeMetric::Px);
        assert_eq!(nan.inner.number.get(), 0.0);
        // Out-of-range `t` saturates rather than wrapping the fixed-point encoding.
        for t in [1e30_f32, -1e30_f32, f32::INFINITY, f32::NEG_INFINITY] {
            let out = a.interpolate(&b, t);
            assert!(
                out.inner.number.get().is_finite(),
                "t = {t} produced a non-finite length"
            );
        }
        // Mismatched metrics fall back to px instead of silently keeping the left metric.
        let mixed = LayoutRowGap::px(0.0).interpolate(&LayoutRowGap::em(1.0), 1.0);
        assert_eq!(mixed.inner.metric, SizeMetric::Px);
        assert!(mixed.inner.number.get().is_finite());
    }
}