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