1use 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#[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#[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")]
91pub 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
108macro_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
140define_position_property!(LayoutTop);
142define_position_property!(LayoutRight);
144define_position_property!(LayoutInsetBottom);
146define_position_property!(LayoutLeft);
148
149macro_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 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#[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
214impl 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#[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#[derive(Debug, Clone, PartialEq, Eq)]
252#[repr(C)]
253pub struct ParseIntErrorWithInput {
254 pub error: AzString,
256 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 #[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 LayoutZIndexParseError::InvalidValue(e.input.as_str())
294 }
295 }
296 }
297}
298
299#[cfg(feature = "parser")]
300pub 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 assert!(parse_layout_top("auto").is_err());
417 assert!(parse_layout_right("").is_err());
418 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 const ALL_POSITIONS: [LayoutPosition; 5] = [
433 LayoutPosition::Static,
434 LayoutPosition::Relative,
435 LayoutPosition::Absolute,
436 LayoutPosition::Fixed,
437 LayoutPosition::Sticky,
438 ];
439
440 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 #[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 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 let p = LayoutPosition::Sticky;
484 assert_eq!(p.is_positioned(), p.is_positioned());
485 assert!(p.is_positioned());
486 }
487
488 #[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 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 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 assert_eq!(
560 parse_layout_position(" \t absolute \n "),
561 Ok(LayoutPosition::Absolute)
562 );
563 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 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}", "\u{202E}static", "абсолютный",
589 "\u{FEFF}static", "𝔰𝔱𝔞𝔱𝔦𝔠",
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); 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 #[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 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 assert_eq!(owned.to_shared().to_contained(), owned);
688 }
689 }
690
691 #[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 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 for overflowing in [
717 "2147483648",
718 "-2147483649",
719 "9223372036854775807", "-9223372036854775808", "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 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", "١٢٣", "123", "\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 #[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 let shared = owned.to_shared();
881 assert_eq!(shared, LayoutZIndexParseError::InvalidValue("abc"));
882
883 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 #[cfg(feature = "parser")]
907 #[test]
908 fn offsets_nan_input_saturates_to_zero_instead_of_propagating_nan() {
909 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 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 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", "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 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 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 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 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 seen.sort();
1047 seen.dedup();
1048 assert_eq!(seen.len(), 4, "expected 4 distinct pixel-value errors");
1049 }
1050
1051 #[cfg(feature = "parser")]
1063 #[test]
1064 fn vmin_suffix_is_shadowed_by_in_bug() {
1065 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 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}