1use 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#[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#[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")]
91pub 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
106macro_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
138define_position_property!(LayoutTop);
140define_position_property!(LayoutRight);
142define_position_property!(LayoutInsetBottom);
144define_position_property!(LayoutLeft);
146
147macro_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 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#[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
230impl 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#[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#[derive(Debug, Clone, PartialEq, Eq)]
267#[repr(C)]
268pub struct ParseIntErrorWithInput {
269 pub error: AzString,
271 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 #[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 LayoutZIndexParseError::InvalidValue(e.input.as_str())
312 }
313 }
314 }
315}
316
317#[cfg(feature = "parser")]
318pub 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 assert!(parse_layout_top("auto").is_err());
433 assert!(parse_layout_right("").is_err());
434 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 const ALL_POSITIONS: [LayoutPosition; 5] = [
449 LayoutPosition::Static,
450 LayoutPosition::Relative,
451 LayoutPosition::Absolute,
452 LayoutPosition::Fixed,
453 LayoutPosition::Sticky,
454 ];
455
456 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 #[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 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 let p = LayoutPosition::Sticky;
500 assert_eq!(p.is_positioned(), p.is_positioned());
501 assert!(p.is_positioned());
502 }
503
504 #[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 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 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 assert_eq!(
576 parse_layout_position(" \t absolute \n "),
577 Ok(LayoutPosition::Absolute)
578 );
579 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 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}", "\u{202E}static", "абсолютный",
605 "\u{FEFF}static", "𝔰𝔱𝔞𝔱𝔦𝔠",
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); 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 #[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 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 assert_eq!(owned.to_shared().to_contained(), owned);
704 }
705 }
706
707 #[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 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 for overflowing in [
733 "2147483648",
734 "-2147483649",
735 "9223372036854775807", "-9223372036854775808", "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 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 "١٢٣", "123", "\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 #[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 let shared = owned.to_shared();
919 assert_eq!(shared, LayoutZIndexParseError::InvalidValue("abc"));
920
921 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 #[cfg(feature = "parser")]
945 #[test]
946 fn offsets_nan_input_saturates_to_zero_instead_of_propagating_nan() {
947 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 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 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", "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 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 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 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 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 seen.sort();
1085 seen.dedup();
1086 assert_eq!(seen.len(), 4, "expected 4 distinct pixel-value errors");
1087 }
1088
1089 #[cfg(feature = "parser")]
1101 #[test]
1102 fn vmin_suffix_is_shadowed_by_in_bug() {
1103 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 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}