Skip to main content

azul_css/props/layout/
position.rs

1//! CSS properties for positioning elements: `position`, `top`, `right`,
2//! `bottom`, `left`, and `z-index`. Types defined here are consumed by the
3//! layout solver to resolve positioned elements.
4
5use alloc::string::{String, ToString};
6use crate::corety::AzString;
7
8#[cfg(feature = "parser")]
9use crate::props::basic::pixel::parse_pixel_value;
10use crate::props::{
11    basic::pixel::{CssPixelValueParseError, CssPixelValueParseErrorOwned, PixelValue},
12    formatter::PrintAsCssValue,
13    macros::PixelValueTaker,
14};
15
16// --- LayoutPosition ---
17
18/// Represents a `position` attribute - default: `Static`
19#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
20#[repr(C)]
21#[derive(Default)]
22pub enum LayoutPosition {
23    #[default]
24    Static,
25    Relative,
26    Absolute,
27    Fixed,
28    Sticky,
29}
30
31impl LayoutPosition {
32    #[must_use] pub fn is_positioned(&self) -> bool {
33        *self != Self::Static
34    }
35}
36
37
38impl PrintAsCssValue for LayoutPosition {
39    fn print_as_css_value(&self) -> String {
40        String::from(match self {
41            Self::Static => "static",
42            Self::Relative => "relative",
43            Self::Absolute => "absolute",
44            Self::Fixed => "fixed",
45            Self::Sticky => "sticky",
46        })
47    }
48}
49
50impl_enum_fmt!(LayoutPosition, Static, Fixed, Absolute, Relative, Sticky);
51
52// -- Parser for LayoutPosition
53
54#[derive(Clone, PartialEq, Eq)]
55pub enum LayoutPositionParseError<'a> {
56    InvalidValue(&'a str),
57}
58
59impl_debug_as_display!(LayoutPositionParseError<'a>);
60impl_display! { LayoutPositionParseError<'a>, {
61    InvalidValue(val) => format!("Invalid position value: \"{}\"", val),
62}}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65#[repr(C, u8)]
66pub enum LayoutPositionParseErrorOwned {
67    InvalidValue(AzString),
68}
69
70impl LayoutPositionParseError<'_> {
71    #[must_use] pub fn to_contained(&self) -> LayoutPositionParseErrorOwned {
72        match self {
73            LayoutPositionParseError::InvalidValue(s) => {
74                LayoutPositionParseErrorOwned::InvalidValue((*s).to_string().into())
75            }
76        }
77    }
78}
79
80impl LayoutPositionParseErrorOwned {
81    #[must_use] pub fn to_shared(&self) -> LayoutPositionParseError<'_> {
82        match self {
83            Self::InvalidValue(s) => {
84                LayoutPositionParseError::InvalidValue(s.as_str())
85            }
86        }
87    }
88}
89
90#[cfg(feature = "parser")]
91/// # Errors
92///
93/// Returns an error if `input` is not a valid CSS `position` value.
94pub fn parse_layout_position(
95    input: &str,
96) -> Result<LayoutPosition, LayoutPositionParseError<'_>> {
97    let input = input.trim();
98    match input {
99        "static" => Ok(LayoutPosition::Static),
100        "relative" => Ok(LayoutPosition::Relative),
101        "absolute" => Ok(LayoutPosition::Absolute),
102        "fixed" => Ok(LayoutPosition::Fixed),
103        "sticky" => Ok(LayoutPosition::Sticky),
104        _ => Err(LayoutPositionParseError::InvalidValue(input)),
105    }
106}
107
108// --- Offset Properties (top, right, bottom, left) ---
109
110macro_rules! define_position_property {
111    ($struct_name:ident) => {
112        #[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
113        #[repr(C)]
114        pub struct $struct_name {
115            pub inner: PixelValue,
116        }
117
118        impl ::core::fmt::Debug for $struct_name {
119            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
120                write!(f, "{}", self.inner)
121            }
122        }
123
124        impl PixelValueTaker for $struct_name {
125            fn from_pixel_value(inner: PixelValue) -> Self {
126                Self { inner }
127            }
128        }
129
130        impl_pixel_value!($struct_name);
131
132        impl PrintAsCssValue for $struct_name {
133            fn print_as_css_value(&self) -> String {
134                format!("{}", self.inner)
135            }
136        }
137    };
138}
139
140/// Represents the CSS `top` offset property for positioned elements.
141define_position_property!(LayoutTop);
142/// Represents the CSS `right` offset property for positioned elements.
143define_position_property!(LayoutRight);
144/// Represents the CSS `bottom` offset property for positioned elements.
145define_position_property!(LayoutInsetBottom);
146/// Represents the CSS `left` offset property for positioned elements.
147define_position_property!(LayoutLeft);
148
149// -- Parse error types and parsers for offset properties (top, right, bottom, left)
150
151macro_rules! define_offset_parse_error {
152    ($struct_name:ident, $error_name:ident, $error_owned_name:ident, $parse_fn:ident) => {
153        #[derive(Clone, PartialEq, Eq)]
154        pub enum $error_name<'a> {
155            PixelValue(CssPixelValueParseError<'a>),
156        }
157        impl_debug_as_display!($error_name<'a>);
158        impl_display! { $error_name<'a>, { PixelValue(e) => format!("{}", e), }}
159        impl_from!(CssPixelValueParseError<'a>, $error_name::PixelValue);
160
161        #[derive(Debug, Clone, PartialEq, Eq)]
162        #[repr(C, u8)]
163        pub enum $error_owned_name {
164            PixelValue(CssPixelValueParseErrorOwned),
165        }
166        impl $error_name<'_> {
167            #[must_use] pub fn to_contained(&self) -> $error_owned_name {
168                match self {
169                    $error_name::PixelValue(e) => {
170                        $error_owned_name::PixelValue(e.to_contained())
171                    }
172                }
173            }
174        }
175        impl $error_owned_name {
176            #[must_use] pub fn to_shared(&self) -> $error_name<'_> {
177                match self {
178                    $error_owned_name::PixelValue(e) => {
179                        $error_name::PixelValue(e.to_shared())
180                    }
181                }
182            }
183        }
184
185        #[cfg(feature = "parser")]
186        /// # Errors
187        ///
188        /// Returns an error if `input` is not a valid CSS value for this property.
189        pub fn $parse_fn(input: &str) -> Result<$struct_name, $error_name<'_>> {
190            parse_pixel_value(input)
191                .map(|v| $struct_name { inner: v })
192                .map_err(Into::into)
193        }
194    };
195}
196
197define_offset_parse_error!(LayoutTop, LayoutTopParseError, LayoutTopParseErrorOwned, parse_layout_top);
198define_offset_parse_error!(LayoutRight, LayoutRightParseError, LayoutRightParseErrorOwned, parse_layout_right);
199define_offset_parse_error!(LayoutInsetBottom, LayoutInsetBottomParseError, LayoutInsetBottomParseErrorOwned, parse_layout_bottom);
200define_offset_parse_error!(LayoutLeft, LayoutLeftParseError, LayoutLeftParseErrorOwned, parse_layout_left);
201
202// --- LayoutZIndex ---
203
204/// Represents a `z-index` attribute - controls stacking order of positioned elements
205#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
206#[repr(C, u8)]
207#[derive(Default)]
208pub enum LayoutZIndex {
209    #[default]
210    Auto,
211    Integer(i32),
212}
213
214// Formatting to Rust code
215impl crate::codegen::format::FormatAsRustCode for LayoutZIndex {
216    fn format_as_rust_code(&self, _tabs: usize) -> String {
217        match self {
218            Self::Auto => String::from("LayoutZIndex::Auto"),
219            Self::Integer(val) => {
220                format!("LayoutZIndex::Integer({val})")
221            }
222        }
223    }
224}
225
226
227impl PrintAsCssValue for LayoutZIndex {
228    fn print_as_css_value(&self) -> String {
229        match self {
230            Self::Auto => String::from("auto"),
231            Self::Integer(val) => val.to_string(),
232        }
233    }
234}
235
236// -- Parser for LayoutZIndex
237
238#[derive(Clone, PartialEq, Eq)]
239pub enum LayoutZIndexParseError<'a> {
240    InvalidValue(&'a str),
241    ParseInt(::core::num::ParseIntError, &'a str),
242}
243impl_debug_as_display!(LayoutZIndexParseError<'a>);
244impl_display! { LayoutZIndexParseError<'a>, {
245    InvalidValue(val) => format!("Invalid z-index value: \"{}\"", val),
246    ParseInt(e, s) => format!("Invalid z-index integer \"{}\": {}", s, e),
247}}
248
249/// Wrapper for `ParseIntError` that stores the error message and original
250/// input as owned strings for FFI compatibility.
251#[derive(Debug, Clone, PartialEq, Eq)]
252#[repr(C)]
253pub struct ParseIntErrorWithInput {
254    /// The stringified parse error (e.g. "invalid digit found in string").
255    pub error: AzString,
256    /// The original input string that failed to parse.
257    pub input: AzString,
258}
259
260#[derive(Debug, Clone, PartialEq, Eq)]
261#[repr(C, u8)]
262pub enum LayoutZIndexParseErrorOwned {
263    InvalidValue(AzString),
264    ParseInt(ParseIntErrorWithInput),
265}
266
267impl LayoutZIndexParseError<'_> {
268    #[must_use] pub fn to_contained(&self) -> LayoutZIndexParseErrorOwned {
269        match self {
270            LayoutZIndexParseError::InvalidValue(s) => {
271                LayoutZIndexParseErrorOwned::InvalidValue((*s).to_string().into())
272            }
273            LayoutZIndexParseError::ParseInt(e, s) => {
274                LayoutZIndexParseErrorOwned::ParseInt(ParseIntErrorWithInput { error: e.to_string().into(), input: (*s).to_string().into() })
275            }
276        }
277    }
278}
279
280impl LayoutZIndexParseErrorOwned {
281    /// Converts back to the borrowed error type.
282    ///
283    /// **Note:** This conversion is lossy for `ParseInt` — the original
284    /// `core::num::ParseIntError` cannot be reconstructed from its string
285    /// representation, so `ParseInt` is mapped to `InvalidValue` instead.
286    #[must_use] pub fn to_shared(&self) -> LayoutZIndexParseError<'_> {
287        match self {
288            Self::InvalidValue(s) => {
289                LayoutZIndexParseError::InvalidValue(s.as_str())
290            }
291            Self::ParseInt(e) => {
292                // We can't reconstruct ParseIntError, so use InvalidValue
293                LayoutZIndexParseError::InvalidValue(e.input.as_str())
294            }
295        }
296    }
297}
298
299#[cfg(feature = "parser")]
300/// # Errors
301///
302/// Returns an error if `input` is not a valid CSS `z-index` value.
303pub fn parse_layout_z_index(
304    input: &str,
305) -> Result<LayoutZIndex, LayoutZIndexParseError<'_>> {
306    let input = input.trim();
307    if input == "auto" {
308        return Ok(LayoutZIndex::Auto);
309    }
310
311    match input.parse::<i32>() {
312        Ok(val) => Ok(LayoutZIndex::Integer(val)),
313        Err(e) => Err(LayoutZIndexParseError::ParseInt(e, input)),
314    }
315}
316
317#[cfg(all(test, feature = "parser"))]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn test_parse_layout_position() {
323        assert_eq!(
324            parse_layout_position("static").unwrap(),
325            LayoutPosition::Static
326        );
327        assert_eq!(
328            parse_layout_position("relative").unwrap(),
329            LayoutPosition::Relative
330        );
331        assert_eq!(
332            parse_layout_position("absolute").unwrap(),
333            LayoutPosition::Absolute
334        );
335        assert_eq!(
336            parse_layout_position("fixed").unwrap(),
337            LayoutPosition::Fixed
338        );
339        assert_eq!(
340            parse_layout_position("sticky").unwrap(),
341            LayoutPosition::Sticky
342        );
343    }
344
345    #[test]
346    fn test_parse_layout_position_whitespace() {
347        assert_eq!(
348            parse_layout_position("  absolute  ").unwrap(),
349            LayoutPosition::Absolute
350        );
351    }
352
353    #[test]
354    fn test_parse_layout_position_invalid() {
355        assert!(parse_layout_position("").is_err());
356        assert!(parse_layout_position("absolutely").is_err());
357    }
358
359    #[test]
360    fn test_parse_layout_z_index() {
361        assert_eq!(parse_layout_z_index("auto").unwrap(), LayoutZIndex::Auto);
362        assert_eq!(
363            parse_layout_z_index("10").unwrap(),
364            LayoutZIndex::Integer(10)
365        );
366        assert_eq!(parse_layout_z_index("0").unwrap(), LayoutZIndex::Integer(0));
367        assert_eq!(
368            parse_layout_z_index("-5").unwrap(),
369            LayoutZIndex::Integer(-5)
370        );
371        assert_eq!(
372            parse_layout_z_index("  999  ").unwrap(),
373            LayoutZIndex::Integer(999)
374        );
375    }
376
377    #[test]
378    fn test_parse_layout_z_index_invalid() {
379        assert!(parse_layout_z_index("10px").is_err());
380        assert!(parse_layout_z_index("1.5").is_err());
381        assert!(parse_layout_z_index("none").is_err());
382        assert!(parse_layout_z_index("").is_err());
383    }
384
385    #[test]
386    fn test_parse_offsets() {
387        assert_eq!(
388            parse_layout_top("10px").unwrap(),
389            LayoutTop {
390                inner: PixelValue::px(10.0)
391            }
392        );
393        assert_eq!(
394            parse_layout_right("5%").unwrap(),
395            LayoutRight {
396                inner: PixelValue::percent(5.0)
397            }
398        );
399        assert_eq!(
400            parse_layout_bottom("2.5em").unwrap(),
401            LayoutInsetBottom {
402                inner: PixelValue::em(2.5)
403            }
404        );
405        assert_eq!(
406            parse_layout_left("0").unwrap(),
407            LayoutLeft {
408                inner: PixelValue::px(0.0)
409            }
410        );
411    }
412
413    #[test]
414    fn test_parse_offsets_invalid() {
415        // The simple `parse_pixel_value` does not handle `auto`.
416        assert!(parse_layout_top("auto").is_err());
417        assert!(parse_layout_right("").is_err());
418        // Liberal parsing accepts whitespace between number and unit
419        assert!(parse_layout_bottom("10 px").is_ok());
420        assert!(parse_layout_left("ten pixels").is_err());
421    }
422}
423
424#[cfg(test)]
425mod autotest_generated {
426    use alloc::{string::String, vec::Vec};
427
428    use super::*;
429    use crate::{codegen::format::FormatAsRustCode, props::basic::length::SizeMetric};
430
431    /// Every `LayoutPosition` variant, so tests stay exhaustive if one is added.
432    const ALL_POSITIONS: [LayoutPosition; 5] = [
433        LayoutPosition::Static,
434        LayoutPosition::Relative,
435        LayoutPosition::Absolute,
436        LayoutPosition::Fixed,
437        LayoutPosition::Sticky,
438    ];
439
440    /// Every `SizeMetric` variant *except* `Vmin`, which cannot survive a
441    /// `parse_pixel_value` round-trip — see `vmin_suffix_is_shadowed_by_in_bug`.
442    const ROUNDTRIPPABLE_METRICS: [SizeMetric; 11] = [
443        SizeMetric::Px,
444        SizeMetric::Pt,
445        SizeMetric::Em,
446        SizeMetric::Rem,
447        SizeMetric::In,
448        SizeMetric::Cm,
449        SizeMetric::Mm,
450        SizeMetric::Percent,
451        SizeMetric::Vw,
452        SizeMetric::Vh,
453        SizeMetric::Vmax,
454    ];
455
456    // ---------------------------------------------------------------------
457    // LayoutPosition::is_positioned (predicate)
458    // ---------------------------------------------------------------------
459
460    #[test]
461    fn is_positioned_basic_true_false() {
462        assert!(!LayoutPosition::Static.is_positioned());
463        assert!(LayoutPosition::Absolute.is_positioned());
464    }
465
466    #[test]
467    fn is_positioned_holds_for_every_variant() {
468        // The invariant the layout solver relies on: positioned <=> not static.
469        for p in ALL_POSITIONS {
470            assert_eq!(p.is_positioned(), p != LayoutPosition::Static, "{p:?}");
471        }
472    }
473
474    #[test]
475    fn is_positioned_default_is_static_and_unpositioned() {
476        assert_eq!(LayoutPosition::default(), LayoutPosition::Static);
477        assert!(!LayoutPosition::default().is_positioned());
478    }
479
480    #[test]
481    fn is_positioned_is_pure() {
482        // Takes &self; repeated calls must not mutate or drift.
483        let p = LayoutPosition::Sticky;
484        assert_eq!(p.is_positioned(), p.is_positioned());
485        assert!(p.is_positioned());
486    }
487
488    // ---------------------------------------------------------------------
489    // parse_layout_position (parser)
490    // ---------------------------------------------------------------------
491
492    #[cfg(feature = "parser")]
493    #[test]
494    fn parse_position_valid_minimal() {
495        assert_eq!(parse_layout_position("static"), Ok(LayoutPosition::Static));
496        assert_eq!(
497            parse_layout_position("relative"),
498            Ok(LayoutPosition::Relative)
499        );
500        assert_eq!(
501            parse_layout_position("absolute"),
502            Ok(LayoutPosition::Absolute)
503        );
504        assert_eq!(parse_layout_position("fixed"), Ok(LayoutPosition::Fixed));
505        assert_eq!(parse_layout_position("sticky"), Ok(LayoutPosition::Sticky));
506    }
507
508    #[cfg(feature = "parser")]
509    #[test]
510    fn parse_position_empty_and_whitespace_only() {
511        // Empty and whitespace-only both trim down to "" and must report "".
512        for input in ["", "   ", "\t\n", "\r\n\t  \x0c", " \u{0b} "] {
513            let err = parse_layout_position(input)
514                .expect_err("whitespace-only input must not parse");
515            assert_eq!(err, LayoutPositionParseError::InvalidValue(""));
516        }
517    }
518
519    #[cfg(feature = "parser")]
520    #[test]
521    fn parse_position_garbage_never_panics() {
522        for input in [
523            "absolutely",
524            "STATIC",
525            "position: absolute",
526            "!@#$%^&*()",
527            "\0\0\0",
528            "-1",
529            "0",
530            "null",
531            "static;",
532            "static static",
533            "\u{7f}\u{1}",
534        ] {
535            assert!(
536                parse_layout_position(input).is_err(),
537                "expected Err for {input:?}"
538            );
539        }
540    }
541
542    #[cfg(feature = "parser")]
543    #[test]
544    fn parse_position_is_ascii_case_sensitive() {
545        // NOTE: pins *current* behaviour. Per CSS Syntax, keywords are ASCII
546        // case-insensitive, so these arguably ought to be Ok(..) — see report.
547        for input in ["Static", "STATIC", "sTaTiC", "Absolute", "FIXED"] {
548            assert!(
549                parse_layout_position(input).is_err(),
550                "expected Err for {input:?}"
551            );
552        }
553    }
554
555    #[cfg(feature = "parser")]
556    #[test]
557    fn parse_position_trims_but_rejects_inner_junk() {
558        // Surrounding whitespace is trimmed...
559        assert_eq!(
560            parse_layout_position("  \t absolute \n "),
561            Ok(LayoutPosition::Absolute)
562        );
563        // ...but anything else attached to the keyword is rejected.
564        for input in ["absolute;", "absolute garbage", ";absolute", "absolute,"] {
565            assert!(
566                parse_layout_position(input).is_err(),
567                "expected Err for {input:?}"
568            );
569        }
570    }
571
572    #[cfg(feature = "parser")]
573    #[test]
574    fn parse_position_error_borrows_the_trimmed_input() {
575        let err = parse_layout_position("   bogus   ").unwrap_err();
576        assert_eq!(err, LayoutPositionParseError::InvalidValue("bogus"));
577        // Display must show the trimmed slice, not the padded original.
578        assert_eq!(err.to_string(), "Invalid position value: \"bogus\"");
579    }
580
581    #[cfg(feature = "parser")]
582    #[test]
583    fn parse_position_unicode_never_panics() {
584        for input in [
585            "\u{1F600}",
586            "static\u{0301}",   // combining acute accent
587            "\u{202E}static",   // RTL override
588            "абсолютный",
589            "\u{FEFF}static",   // BOM is not ASCII whitespace -> must not parse
590            "𝔰𝔱𝔞𝔱𝔦𝔠",
591        ] {
592            assert!(
593                parse_layout_position(input).is_err(),
594                "expected Err for {input:?}"
595            );
596        }
597    }
598
599    #[cfg(feature = "parser")]
600    #[test]
601    fn parse_position_extremely_long_input() {
602        let huge = "static".repeat(200_000); // ~1.2M bytes
603        assert!(parse_layout_position(&huge).is_err());
604
605        let huge_ws = String::from(" ").repeat(1_000_000);
606        assert_eq!(
607            parse_layout_position(&huge_ws),
608            Err(LayoutPositionParseError::InvalidValue(""))
609        );
610    }
611
612    #[cfg(feature = "parser")]
613    #[test]
614    fn parse_position_deeply_nested_does_not_stack_overflow() {
615        let nested = "[".repeat(10_000);
616        assert!(parse_layout_position(&nested).is_err());
617    }
618
619    #[cfg(feature = "parser")]
620    #[test]
621    fn parse_position_round_trip_encode_decode() {
622        for p in ALL_POSITIONS {
623            let css = p.print_as_css_value();
624            assert_eq!(parse_layout_position(&css), Ok(p), "round-trip of {p:?}");
625        }
626    }
627
628    #[cfg(feature = "parser")]
629    #[test]
630    fn parse_position_round_trip_decode_encode() {
631        for css in ["static", "relative", "absolute", "fixed", "sticky"] {
632            let parsed = parse_layout_position(css).unwrap();
633            assert_eq!(parsed.print_as_css_value(), css);
634        }
635    }
636
637    #[test]
638    fn position_format_as_rust_code_names_the_variant() {
639        assert_eq!(
640            LayoutPosition::Static.format_as_rust_code(0),
641            "LayoutPosition::Static"
642        );
643        assert_eq!(
644            LayoutPosition::Sticky.format_as_rust_code(usize::MAX),
645            "LayoutPosition::Sticky"
646        );
647    }
648
649    // ---------------------------------------------------------------------
650    // LayoutPositionParseError <-> Owned (getters)
651    // ---------------------------------------------------------------------
652
653    #[test]
654    fn position_error_to_contained_basic_access() {
655        let shared = LayoutPositionParseError::InvalidValue("bogus");
656        assert_eq!(
657            shared.to_contained(),
658            LayoutPositionParseErrorOwned::InvalidValue("bogus".to_string().into())
659        );
660    }
661
662    #[test]
663    fn position_error_owned_to_shared_basic_access() {
664        let owned = LayoutPositionParseErrorOwned::InvalidValue("bogus".to_string().into());
665        assert_eq!(
666            owned.to_shared(),
667            LayoutPositionParseError::InvalidValue("bogus")
668        );
669    }
670
671    #[test]
672    fn position_error_round_trip_is_lossless_on_edge_payloads() {
673        // Empty, unicode, NUL and a 100k-byte payload must all survive intact.
674        let long = "x".repeat(100_000);
675        for payload in [
676            "",
677            " ",
678            "\0",
679            "\u{1F600}\u{0301}",
680            "quote\"and\\backslash",
681            long.as_str(),
682        ] {
683            let shared = LayoutPositionParseError::InvalidValue(payload);
684            let owned = shared.to_contained();
685            assert_eq!(owned.to_shared(), shared, "payload {payload:?} was mangled");
686            // ...and the owned form is a fixed point of the two conversions.
687            assert_eq!(owned.to_shared().to_contained(), owned);
688        }
689    }
690
691    // ---------------------------------------------------------------------
692    // parse_layout_z_index (parser)
693    // ---------------------------------------------------------------------
694
695    #[cfg(feature = "parser")]
696    #[test]
697    fn parse_z_index_valid_minimal() {
698        assert_eq!(parse_layout_z_index("auto"), Ok(LayoutZIndex::Auto));
699        assert_eq!(parse_layout_z_index("1"), Ok(LayoutZIndex::Integer(1)));
700    }
701
702    #[cfg(feature = "parser")]
703    #[test]
704    fn parse_z_index_i32_boundaries_saturate_into_err_not_wraparound() {
705        // Exactly at the i32 limits: must parse.
706        assert_eq!(
707            parse_layout_z_index("2147483647"),
708            Ok(LayoutZIndex::Integer(i32::MAX))
709        );
710        assert_eq!(
711            parse_layout_z_index("-2147483648"),
712            Ok(LayoutZIndex::Integer(i32::MIN))
713        );
714
715        // One past the limits: must be a clean Err, never a wrapped value.
716        for overflowing in [
717            "2147483648",
718            "-2147483649",
719            "9223372036854775807",  // i64::MAX
720            "-9223372036854775808", // i64::MIN
721            "340282366920938463463374607431768211456",
722        ] {
723            let err = parse_layout_z_index(overflowing)
724                .expect_err("out-of-range integer must not parse");
725            assert!(
726                matches!(err, LayoutZIndexParseError::ParseInt(_, s) if s == overflowing),
727                "expected ParseInt({overflowing:?}), got {err:?}"
728            );
729        }
730    }
731
732    #[cfg(feature = "parser")]
733    #[test]
734    fn parse_z_index_zero_sign_and_padding_forms() {
735        assert_eq!(parse_layout_z_index("0"), Ok(LayoutZIndex::Integer(0)));
736        assert_eq!(parse_layout_z_index("-0"), Ok(LayoutZIndex::Integer(0)));
737        assert_eq!(parse_layout_z_index("+0"), Ok(LayoutZIndex::Integer(0)));
738        assert_eq!(parse_layout_z_index("+7"), Ok(LayoutZIndex::Integer(7)));
739        assert_eq!(parse_layout_z_index("007"), Ok(LayoutZIndex::Integer(7)));
740        assert_eq!(
741            parse_layout_z_index("  \t -42 \n "),
742            Ok(LayoutZIndex::Integer(-42))
743        );
744    }
745
746    #[cfg(feature = "parser")]
747    #[test]
748    fn parse_z_index_rejects_floats_and_float_literals() {
749        // z-index is an <integer>; nothing float-shaped may sneak through.
750        for input in [
751            "1.5", "1.0", "0.0", "-0.0", "1e3", "1E3", "1e-3", "NaN", "nan", "inf", "-inf",
752            "infinity",
753        ] {
754            assert!(
755                parse_layout_z_index(input).is_err(),
756                "expected Err for {input:?}"
757            );
758        }
759    }
760
761    #[cfg(feature = "parser")]
762    #[test]
763    fn parse_z_index_garbage_never_panics() {
764        for input in [
765            "", "   ", "\t\n", "auto auto", "AUTO", "Auto", "none", "10px", "1_000", "1,000",
766            "- 5", "5-", "--5", "++5", "0x1F", "0b1", "١٢٣", // Arabic-Indic digits
767            "123",                                          // fullwidth digits
768            "\u{1F600}", "\0", "5\0",
769        ] {
770            assert!(
771                parse_layout_z_index(input).is_err(),
772                "expected Err for {input:?}"
773            );
774        }
775    }
776
777    #[cfg(feature = "parser")]
778    #[test]
779    fn parse_z_index_extremely_long_input_terminates() {
780        let huge = "9".repeat(1_000_000);
781        assert!(parse_layout_z_index(&huge).is_err());
782
783        let nested = "[".repeat(10_000);
784        assert!(parse_layout_z_index(&nested).is_err());
785    }
786
787    #[cfg(feature = "parser")]
788    #[test]
789    fn parse_z_index_error_borrows_the_trimmed_input() {
790        let err = parse_layout_z_index("  abc  ").unwrap_err();
791        assert!(
792            matches!(&err, LayoutZIndexParseError::ParseInt(_, s) if *s == "abc"),
793            "got {err:?}"
794        );
795        assert!(err.to_string().starts_with("Invalid z-index integer \"abc\""));
796    }
797
798    #[cfg(feature = "parser")]
799    #[test]
800    fn parse_z_index_round_trip_encode_decode() {
801        let values = [
802            LayoutZIndex::Auto,
803            LayoutZIndex::Integer(0),
804            LayoutZIndex::Integer(1),
805            LayoutZIndex::Integer(-1),
806            LayoutZIndex::Integer(12_345),
807            LayoutZIndex::Integer(-99_999),
808            LayoutZIndex::Integer(i32::MAX),
809            LayoutZIndex::Integer(i32::MIN),
810        ];
811        for z in values {
812            let css = z.print_as_css_value();
813            assert_eq!(parse_layout_z_index(&css), Ok(z), "round-trip of {z:?}");
814        }
815    }
816
817    #[test]
818    fn z_index_default_is_auto() {
819        assert_eq!(LayoutZIndex::default(), LayoutZIndex::Auto);
820        assert_eq!(LayoutZIndex::default().print_as_css_value(), "auto");
821    }
822
823    #[test]
824    fn z_index_format_as_rust_code_survives_i32_min() {
825        assert_eq!(LayoutZIndex::Auto.format_as_rust_code(0), "LayoutZIndex::Auto");
826        assert_eq!(
827            LayoutZIndex::Integer(i32::MIN).format_as_rust_code(0),
828            "LayoutZIndex::Integer(-2147483648)"
829        );
830    }
831
832    // ---------------------------------------------------------------------
833    // LayoutZIndexParseError <-> Owned (getters)
834    // ---------------------------------------------------------------------
835
836    #[test]
837    fn z_index_error_invalid_value_round_trips_losslessly() {
838        for payload in ["", "auto ", "\u{1F600}", "\0"] {
839            let shared = LayoutZIndexParseError::InvalidValue(payload);
840            let owned = shared.to_contained();
841            assert_eq!(
842                owned,
843                LayoutZIndexParseErrorOwned::InvalidValue(payload.to_string().into())
844            );
845            assert_eq!(owned.to_shared(), shared);
846        }
847    }
848
849    #[test]
850    fn z_index_error_to_contained_captures_parse_int_message_and_input() {
851        let int_err = "abc".parse::<i32>().unwrap_err();
852        let shared = LayoutZIndexParseError::ParseInt(int_err.clone(), "abc");
853
854        match shared.to_contained() {
855            LayoutZIndexParseErrorOwned::ParseInt(e) => {
856                assert_eq!(e.input.as_str(), "abc");
857                assert!(!e.error.as_str().is_empty());
858                assert_eq!(e.error.as_str(), int_err.to_string());
859            }
860            other => panic!("expected ParseInt, got {other:?}"),
861        }
862    }
863
864    #[test]
865    fn z_index_error_overflow_and_invalid_digit_are_distinct_before_to_shared() {
866        let invalid_digit = "abc".parse::<i32>().unwrap_err();
867        let overflow = "2147483648".parse::<i32>().unwrap_err();
868
869        let a = LayoutZIndexParseError::ParseInt(invalid_digit, "abc").to_contained();
870        let b = LayoutZIndexParseError::ParseInt(overflow, "2147483648").to_contained();
871        assert_ne!(a, b, "the two ParseIntError kinds must not collapse");
872    }
873
874    #[test]
875    fn z_index_error_to_shared_is_lossy_for_parse_int_as_documented() {
876        let int_err = "abc".parse::<i32>().unwrap_err();
877        let owned = LayoutZIndexParseError::ParseInt(int_err, "abc").to_contained();
878
879        // Documented: ParseIntError cannot be rebuilt, so ParseInt -> InvalidValue.
880        let shared = owned.to_shared();
881        assert_eq!(shared, LayoutZIndexParseError::InvalidValue("abc"));
882
883        // ...which means the owned form is NOT a fixed point of the two
884        // conversions. This asserts the lossiness is exactly as documented,
885        // and will fail loudly if someone silently changes the mapping.
886        assert_ne!(shared.to_contained(), owned);
887        assert_eq!(
888            shared.to_contained(),
889            LayoutZIndexParseErrorOwned::InvalidValue("abc".to_string().into())
890        );
891    }
892
893    #[test]
894    fn z_index_error_to_shared_on_empty_parse_int_payload_does_not_panic() {
895        let owned = LayoutZIndexParseErrorOwned::ParseInt(ParseIntErrorWithInput {
896            error: String::new().into(),
897            input: String::new().into(),
898        });
899        assert_eq!(owned.to_shared(), LayoutZIndexParseError::InvalidValue(""));
900    }
901
902    // ---------------------------------------------------------------------
903    // Offset properties: top / right / bottom / left
904    // ---------------------------------------------------------------------
905
906    #[cfg(feature = "parser")]
907    #[test]
908    fn offsets_nan_input_saturates_to_zero_instead_of_propagating_nan() {
909        // f32::from_str accepts "NaN"; PixelValue stores fixed-point isize, and
910        // `NaN as isize` == 0. Assert the NaN is absorbed, not carried into layout.
911        for input in ["NaN", "nan", "-NaN", "NaNpx", "nan%"] {
912            let v = parse_layout_top(input)
913                .unwrap_or_else(|e| panic!("{input:?} should parse, got {e:?}"));
914            let n = v.inner.number.get();
915            assert!(!n.is_nan(), "{input:?} leaked a NaN into PixelValue");
916            assert_eq!(n, 0.0, "{input:?} should saturate to 0");
917        }
918    }
919
920    #[cfg(feature = "parser")]
921    #[test]
922    fn offsets_infinite_input_saturates_to_finite_bounds() {
923        // f32::from_str maps overflow to +/-inf; the fixed-point cast then
924        // saturates at isize::MIN/MAX. Nothing infinite may reach the solver.
925        for (input, positive) in [
926            ("inf", true),
927            ("infinity", true),
928            ("-inf", false),
929            ("1e40px", true),
930            ("-1e40px", false),
931            ("99999999999999999999999999999999999999999999px", true),
932        ] {
933            let v = parse_layout_right(input)
934                .unwrap_or_else(|e| panic!("{input:?} should parse, got {e:?}"));
935            let n = v.inner.number.get();
936            assert!(n.is_finite(), "{input:?} leaked a non-finite PixelValue");
937            assert_eq!(n > 0.0, positive, "{input:?} lost its sign");
938        }
939    }
940
941    #[cfg(feature = "parser")]
942    #[test]
943    fn offsets_subnormal_input_flushes_to_zero() {
944        // Below the 1/1000 fixed-point resolution everything truncates to 0.
945        for input in ["1e-40px", "-1e-40px", "0.0001px", "0.0009px"] {
946            let v = parse_layout_bottom(input)
947                .unwrap_or_else(|e| panic!("{input:?} should parse, got {e:?}"));
948            assert_eq!(v.inner.number.get(), 0.0, "{input:?} should truncate to 0");
949        }
950    }
951
952    #[cfg(feature = "parser")]
953    #[test]
954    fn offsets_reject_empty_garbage_and_unicode() {
955        for input in [
956            "",
957            "   ",
958            "auto",
959            "px",
960            "%",
961            "10 20px",
962            "ten pixels",
963            "10pxx",
964            "10 px extra",
965            "\u{1F600}",
966            "١٠px", // Arabic-Indic digits
967            "10\u{00B5}m",
968        ] {
969            assert!(
970                parse_layout_left(input).is_err(),
971                "expected Err for {input:?}"
972            );
973        }
974    }
975
976    #[cfg(feature = "parser")]
977    #[test]
978    fn offsets_extremely_long_input_terminates() {
979        let huge = "9".repeat(1_000_000);
980        // Parses to a saturated-but-finite value; must not hang or panic.
981        let v = parse_layout_top(&huge).expect("a million 9s is a valid f32 literal");
982        assert!(v.inner.number.get().is_finite());
983
984        let junk = "z".repeat(1_000_000);
985        assert!(parse_layout_top(&junk).is_err());
986
987        let nested = "[".repeat(10_000);
988        assert!(parse_layout_top(&nested).is_err());
989    }
990
991    #[cfg(feature = "parser")]
992    #[test]
993    fn offsets_round_trip_encode_decode_across_metrics() {
994        // Values chosen to be exact at the 1/1000 fixed-point resolution.
995        for metric in ROUNDTRIPPABLE_METRICS {
996            for raw in [0.0_f32, 1.0, -1.0, 10.5, -7.25, 1024.0] {
997                let expected = LayoutTop {
998                    inner: PixelValue::from_metric(metric, raw),
999                };
1000                let css = expected.print_as_css_value();
1001                let parsed = parse_layout_top(&css)
1002                    .unwrap_or_else(|e| panic!("{css:?} failed to re-parse: {e:?}"));
1003                assert_eq!(parsed, expected, "round-trip of {css:?}");
1004            }
1005        }
1006    }
1007
1008    #[cfg(feature = "parser")]
1009    #[test]
1010    fn offsets_all_four_sides_agree_on_the_same_input() {
1011        // The four parsers are macro-generated; they must not diverge.
1012        let t = parse_layout_top("12.5%").unwrap();
1013        let r = parse_layout_right("12.5%").unwrap();
1014        let b = parse_layout_bottom("12.5%").unwrap();
1015        let l = parse_layout_left("12.5%").unwrap();
1016        assert_eq!(t.inner, r.inner);
1017        assert_eq!(r.inner, b.inner);
1018        assert_eq!(b.inner, l.inner);
1019        assert_eq!(t.inner, PixelValue::percent(12.5));
1020    }
1021
1022    #[cfg(feature = "parser")]
1023    #[test]
1024    fn offsets_zero_is_bare_number_defaulting_to_px() {
1025        assert_eq!(parse_layout_left("0").unwrap(), LayoutLeft::zero());
1026        assert_eq!(LayoutTop::zero().inner, PixelValue::px(0.0));
1027        assert_eq!(LayoutTop::default().inner, PixelValue::zero());
1028    }
1029
1030    #[cfg(feature = "parser")]
1031    #[test]
1032    fn offset_error_round_trip_is_lossless_for_every_variant() {
1033        // Drive each CssPixelValueParseError variant through the real parser,
1034        // then assert to_contained/to_shared preserves it.
1035        let inputs = ["", "px", "abcpx", "not-a-length"];
1036        let mut seen: Vec<String> = Vec::new();
1037        for input in inputs {
1038            let err = parse_layout_top(input).unwrap_err();
1039            let owned = err.to_contained();
1040            assert_eq!(owned.to_shared(), err, "error for {input:?} was mangled");
1041            assert_eq!(owned.to_shared().to_contained(), owned);
1042            seen.push(err.to_string());
1043        }
1044        // The four inputs must exercise four *distinct* error messages,
1045        // otherwise this test is silently only covering one variant.
1046        seen.sort();
1047        seen.dedup();
1048        assert_eq!(seen.len(), 4, "expected 4 distinct pixel-value errors");
1049    }
1050
1051    /// KNOWN BUG (pinned, not endorsed): `vmin` lengths are unparseable.
1052    ///
1053    /// `parse_pixel_value`'s suffix table tries `("in", In)` *before*
1054    /// `("vmin", Vmin)`, so `"10vmin"` hits `strip_suffix("in")` first, leaving
1055    /// `"10vm"` to be parsed as an f32 — which fails. Every `vmin` length in a
1056    /// stylesheet is therefore silently dropped, on `top`/`right`/`bottom`/
1057    /// `left` and on every other property that goes through `parse_pixel_value`.
1058    ///
1059    /// This test asserts the *current* (broken) behaviour so the suite stays
1060    /// honest; flip it to `is_ok()` / an equality assert once the suffix table
1061    /// in `css/src/props/basic/pixel.rs` is reordered.
1062    #[cfg(feature = "parser")]
1063    #[test]
1064    fn vmin_suffix_is_shadowed_by_in_bug() {
1065        // FIXED (was a characterization of the bug): "vmin" used to be shadowed by an
1066        // earlier "in" suffix and rejected. It now parses on every longhand.
1067        assert!(parse_layout_top("10vmin").is_ok());
1068        assert!(parse_layout_right("0vmin").is_ok());
1069        assert!(parse_layout_bottom("2.5vmin").is_ok());
1070        assert!(parse_layout_left("100vmin").is_ok());
1071
1072        // The neighbouring viewport units keep working too.
1073        assert!(parse_layout_top("10vmax").is_ok());
1074        assert!(parse_layout_top("10vw").is_ok());
1075        assert!(parse_layout_top("10vh").is_ok());
1076    }
1077}