1use alloc::{
7 string::{String, ToString},
8 vec::Vec,
9};
10
11#[cfg(feature = "parser")]
12use crate::props::basic::pixel::{parse_pixel_value_with_auto, PixelValueWithAuto};
13use crate::{
14 css::PrintAsCssValue,
15 props::{
16 basic::pixel::{CssPixelValueParseError, CssPixelValueParseErrorOwned, PixelValue},
17 macros::PixelValueTaker,
18 },
19};
20
21macro_rules! impl_spacing_type_impls {
26 ($name:ident) => {
27 impl ::core::fmt::Debug for $name {
28 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
29 write!(f, "{}", self.inner)
30 }
31 }
32
33 impl PixelValueTaker for $name {
34 fn from_pixel_value(inner: PixelValue) -> Self {
35 Self { inner }
36 }
37 }
38
39 impl_pixel_value!($name);
40 };
41}
42
43#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
45#[repr(C)]
46pub struct LayoutPaddingTop {
47 pub inner: PixelValue,
48}
49impl_spacing_type_impls!(LayoutPaddingTop);
50
51#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
53#[repr(C)]
54pub struct LayoutPaddingRight {
55 pub inner: PixelValue,
56}
57impl_spacing_type_impls!(LayoutPaddingRight);
58
59#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
61#[repr(C)]
62pub struct LayoutPaddingBottom {
63 pub inner: PixelValue,
64}
65impl_spacing_type_impls!(LayoutPaddingBottom);
66
67#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
69#[repr(C)]
70pub struct LayoutPaddingLeft {
71 pub inner: PixelValue,
72}
73impl_spacing_type_impls!(LayoutPaddingLeft);
74
75#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
77#[repr(C)]
78pub struct LayoutPaddingInlineStart {
79 pub inner: PixelValue,
80}
81impl_spacing_type_impls!(LayoutPaddingInlineStart);
82
83#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
85#[repr(C)]
86pub struct LayoutPaddingInlineEnd {
87 pub inner: PixelValue,
88}
89impl_spacing_type_impls!(LayoutPaddingInlineEnd);
90
91#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
93#[repr(C)]
94pub struct LayoutMarginTop {
95 pub inner: PixelValue,
96}
97impl_spacing_type_impls!(LayoutMarginTop);
98
99#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
101#[repr(C)]
102pub struct LayoutMarginRight {
103 pub inner: PixelValue,
104}
105impl_spacing_type_impls!(LayoutMarginRight);
106
107#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
109#[repr(C)]
110pub struct LayoutMarginBottom {
111 pub inner: PixelValue,
112}
113impl_spacing_type_impls!(LayoutMarginBottom);
114
115#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
117#[repr(C)]
118pub struct LayoutMarginLeft {
119 pub inner: PixelValue,
120}
121impl_spacing_type_impls!(LayoutMarginLeft);
122
123#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
125#[repr(C)]
126pub struct LayoutColumnGap {
127 pub inner: PixelValue,
128}
129impl_spacing_type_impls!(LayoutColumnGap);
130
131#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
133#[repr(C)]
134pub struct LayoutRowGap {
135 pub inner: PixelValue,
136}
137impl_spacing_type_impls!(LayoutRowGap);
138
139#[cfg(feature = "parser")]
142macro_rules! impl_spacing_parse_error {
143 ($borrowed:ident, $owned:ident, $property_name:expr) => {
144 #[cfg(feature = "parser")]
145 impl_debug_as_display!($borrowed<'a>);
146
147 #[cfg(feature = "parser")]
148 impl_display! { $borrowed<'a>, {
149 PixelValueParseError(e) => format!("Could not parse pixel value: {}", e),
150 TooManyValues => concat!("Too many values: ", $property_name, " property accepts at most 4 values."),
151 TooFewValues => concat!("Too few values: ", $property_name, " property requires at least 1 value."),
152 }}
153
154 #[cfg(feature = "parser")]
155 impl_from!(
156 CssPixelValueParseError<'a>,
157 $borrowed::PixelValueParseError
158 );
159
160 #[cfg(feature = "parser")]
161 impl $borrowed<'_> {
162 #[must_use] pub fn to_contained(&self) -> $owned {
163 match self {
164 $borrowed::PixelValueParseError(e) => {
165 $owned::PixelValueParseError(e.to_contained())
166 }
167 $borrowed::TooManyValues => $owned::TooManyValues,
168 $borrowed::TooFewValues => $owned::TooFewValues,
169 }
170 }
171 }
172
173 #[cfg(feature = "parser")]
174 impl $owned {
175 #[must_use] pub fn to_shared(&self) -> $borrowed<'_> {
176 match self {
177 $owned::PixelValueParseError(e) => {
178 $borrowed::PixelValueParseError(e.to_shared())
179 }
180 $owned::TooManyValues => $borrowed::TooManyValues,
181 $owned::TooFewValues => $borrowed::TooFewValues,
182 }
183 }
184 }
185 };
186}
187
188#[cfg(feature = "parser")]
192#[derive(Clone, PartialEq, Eq)]
193pub enum LayoutPaddingParseError<'a> {
194 PixelValueParseError(CssPixelValueParseError<'a>),
195 TooManyValues,
196 TooFewValues,
197}
198#[allow(variant_size_differences)]
199#[cfg(feature = "parser")]
202#[derive(Debug, Clone, PartialEq, Eq)]
203#[repr(C, u8)]
204pub enum LayoutPaddingParseErrorOwned {
205 PixelValueParseError(CssPixelValueParseErrorOwned),
206 TooManyValues,
207 TooFewValues,
208}
209
210#[cfg(feature = "parser")]
211impl_spacing_parse_error!(
212 LayoutPaddingParseError,
213 LayoutPaddingParseErrorOwned,
214 "padding"
215);
216
217#[cfg(feature = "parser")]
219#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
220pub struct LayoutPadding {
221 pub top: PixelValueWithAuto,
222 pub bottom: PixelValueWithAuto,
223 pub left: PixelValueWithAuto,
224 pub right: PixelValueWithAuto,
225}
226
227#[cfg(feature = "parser")]
228pub fn parse_layout_padding(input: &str) -> Result<LayoutPadding, LayoutPaddingParseError<'_>> {
232 let values: Vec<_> = input.split_whitespace().collect();
233
234 let parsed_values: Vec<PixelValueWithAuto> = values
235 .iter()
236 .map(|s| parse_pixel_value_with_auto(s))
237 .collect::<Result<_, _>>()?;
238
239 match parsed_values.len() {
240 1 => {
241 let all = parsed_values[0];
243 Ok(LayoutPadding {
244 top: all,
245 right: all,
246 bottom: all,
247 left: all,
248 })
249 }
250 2 => {
251 let vertical = parsed_values[0];
253 let horizontal = parsed_values[1];
254 Ok(LayoutPadding {
255 top: vertical,
256 right: horizontal,
257 bottom: vertical,
258 left: horizontal,
259 })
260 }
261 3 => {
262 let top = parsed_values[0];
264 let horizontal = parsed_values[1];
265 let bottom = parsed_values[2];
266 Ok(LayoutPadding {
267 top,
268 right: horizontal,
269 bottom,
270 left: horizontal,
271 })
272 }
273 4 => {
274 Ok(LayoutPadding {
276 top: parsed_values[0],
277 right: parsed_values[1],
278 bottom: parsed_values[2],
279 left: parsed_values[3],
280 })
281 }
282 0 => Err(LayoutPaddingParseError::TooFewValues),
283 _ => Err(LayoutPaddingParseError::TooManyValues),
284 }
285}
286
287#[cfg(feature = "parser")]
291#[derive(Clone, PartialEq, Eq)]
292pub enum LayoutMarginParseError<'a> {
293 PixelValueParseError(CssPixelValueParseError<'a>),
294 TooManyValues,
295 TooFewValues,
296}
297#[allow(variant_size_differences)]
298#[cfg(feature = "parser")]
301#[derive(Debug, Clone, PartialEq, Eq)]
302#[repr(C, u8)]
303pub enum LayoutMarginParseErrorOwned {
304 PixelValueParseError(CssPixelValueParseErrorOwned),
305 TooManyValues,
306 TooFewValues,
307}
308
309#[cfg(feature = "parser")]
310impl_spacing_parse_error!(
311 LayoutMarginParseError,
312 LayoutMarginParseErrorOwned,
313 "margin"
314);
315
316#[cfg(feature = "parser")]
318#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
319pub struct LayoutMargin {
320 pub top: PixelValueWithAuto,
321 pub bottom: PixelValueWithAuto,
322 pub left: PixelValueWithAuto,
323 pub right: PixelValueWithAuto,
324}
325
326#[cfg(feature = "parser")]
327pub fn parse_layout_margin(input: &str) -> Result<LayoutMargin, LayoutMarginParseError<'_>> {
331 match parse_layout_padding(input) {
334 Ok(padding) => Ok(LayoutMargin {
335 top: padding.top,
336 left: padding.left,
337 right: padding.right,
338 bottom: padding.bottom,
339 }),
340 Err(e) => match e {
341 LayoutPaddingParseError::PixelValueParseError(err) => {
342 Err(LayoutMarginParseError::PixelValueParseError(err))
343 }
344 LayoutPaddingParseError::TooManyValues => Err(LayoutMarginParseError::TooManyValues),
345 LayoutPaddingParseError::TooFewValues => Err(LayoutMarginParseError::TooFewValues),
346 },
347 }
348}
349
350macro_rules! typed_pixel_value_parser {
353 (
354 $fn:ident, $fn_str:expr, $return:ident, $return_str:expr, $import_str:expr, $test_str:expr
355 ) => {
356 #[doc = $return_str]
358 #[doc = $import_str]
364 #[doc = $test_str]
365 pub fn $fn(input: &str) -> Result<$return, CssPixelValueParseError<'_>> {
370 crate::props::basic::parse_pixel_value(input).map(|e| $return { inner: e })
371 }
372
373 impl crate::props::formatter::FormatAsCssValue for $return {
374 fn format_as_css_value(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
375 self.inner.format_as_css_value(f)
376 }
377 }
378 };
379 ($fn:ident, $return:ident) => {
380 typed_pixel_value_parser!(
381 $fn,
382 stringify!($fn),
383 $return,
384 stringify!($return),
385 concat!(
386 "# extern crate azul_css;",
387 "\r\n",
388 "# use azul_css::props::layout::spacing::",
389 stringify!($fn),
390 ";",
391 "\r\n",
392 "# use azul_css::props::basic::pixel::PixelValue;\r\n",
393 "# use azul_css::props::layout::spacing::",
394 stringify!($return),
395 ";\r\n"
396 ),
397 concat!(
398 "assert_eq!(",
399 stringify!($fn),
400 "(\"5px\"), Ok(",
401 stringify!($return),
402 " { inner: PixelValue::px(5.0) }));"
403 )
404 );
405 };
406}
407
408#[cfg(feature = "parser")]
409typed_pixel_value_parser!(parse_layout_padding_top, LayoutPaddingTop);
410#[cfg(feature = "parser")]
411typed_pixel_value_parser!(parse_layout_padding_right, LayoutPaddingRight);
412#[cfg(feature = "parser")]
413typed_pixel_value_parser!(parse_layout_padding_bottom, LayoutPaddingBottom);
414#[cfg(feature = "parser")]
415typed_pixel_value_parser!(parse_layout_padding_left, LayoutPaddingLeft);
416#[cfg(feature = "parser")]
417typed_pixel_value_parser!(parse_layout_padding_inline_start, LayoutPaddingInlineStart);
418#[cfg(feature = "parser")]
419typed_pixel_value_parser!(parse_layout_padding_inline_end, LayoutPaddingInlineEnd);
420
421#[cfg(feature = "parser")]
422typed_pixel_value_parser!(parse_layout_margin_top, LayoutMarginTop);
423#[cfg(feature = "parser")]
424typed_pixel_value_parser!(parse_layout_margin_right, LayoutMarginRight);
425#[cfg(feature = "parser")]
426typed_pixel_value_parser!(parse_layout_margin_bottom, LayoutMarginBottom);
427#[cfg(feature = "parser")]
428typed_pixel_value_parser!(parse_layout_margin_left, LayoutMarginLeft);
429
430#[cfg(feature = "parser")]
431typed_pixel_value_parser!(parse_layout_column_gap, LayoutColumnGap);
432#[cfg(feature = "parser")]
433typed_pixel_value_parser!(parse_layout_row_gap, LayoutRowGap);
434
435#[cfg(all(test, feature = "parser"))]
436mod tests {
437 use super::*;
438 use crate::props::basic::pixel::{PixelValue, PixelValueWithAuto};
439
440 #[test]
441 fn test_parse_layout_padding_shorthand() {
442 let result = parse_layout_padding("10px").unwrap();
444 assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(10.0)));
445 assert_eq!(
446 result.right,
447 PixelValueWithAuto::Exact(PixelValue::px(10.0))
448 );
449 assert_eq!(
450 result.bottom,
451 PixelValueWithAuto::Exact(PixelValue::px(10.0))
452 );
453 assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::px(10.0)));
454
455 let result = parse_layout_padding("5% 2em").unwrap();
457 assert_eq!(
458 result.top,
459 PixelValueWithAuto::Exact(PixelValue::percent(5.0))
460 );
461 assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::em(2.0)));
462 assert_eq!(
463 result.bottom,
464 PixelValueWithAuto::Exact(PixelValue::percent(5.0))
465 );
466 assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::em(2.0)));
467
468 let result = parse_layout_padding("1px 2px 3px").unwrap();
470 assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(1.0)));
471 assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
472 assert_eq!(
473 result.bottom,
474 PixelValueWithAuto::Exact(PixelValue::px(3.0))
475 );
476 assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
477
478 let result = parse_layout_padding("1px 2px 3px 4px").unwrap();
480 assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(1.0)));
481 assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
482 assert_eq!(
483 result.bottom,
484 PixelValueWithAuto::Exact(PixelValue::px(3.0))
485 );
486 assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::px(4.0)));
487
488 let result = parse_layout_padding(" 1px 2px ").unwrap();
490 assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(1.0)));
491 assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
492 }
493
494 #[test]
495 fn test_parse_layout_padding_errors() {
496 assert!(matches!(
497 parse_layout_padding("").err().unwrap(),
498 LayoutPaddingParseError::TooFewValues
499 ));
500 assert!(matches!(
501 parse_layout_padding("1px 2px 3px 4px 5px").err().unwrap(),
502 LayoutPaddingParseError::TooManyValues
503 ));
504 assert!(matches!(
505 parse_layout_padding("1px oops 3px").err().unwrap(),
506 LayoutPaddingParseError::PixelValueParseError(_)
507 ));
508 }
509
510 #[test]
511 fn test_parse_layout_margin_shorthand() {
512 let result = parse_layout_margin("auto").unwrap();
514 assert_eq!(result.top, PixelValueWithAuto::Auto);
515 assert_eq!(result.right, PixelValueWithAuto::Auto);
516 assert_eq!(result.bottom, PixelValueWithAuto::Auto);
517 assert_eq!(result.left, PixelValueWithAuto::Auto);
518
519 let result = parse_layout_margin("10px auto").unwrap();
521 assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(10.0)));
522 assert_eq!(result.right, PixelValueWithAuto::Auto);
523 assert_eq!(
524 result.bottom,
525 PixelValueWithAuto::Exact(PixelValue::px(10.0))
526 );
527 assert_eq!(result.left, PixelValueWithAuto::Auto);
528 }
529
530 #[test]
531 fn test_parse_layout_margin_errors() {
532 assert!(matches!(
533 parse_layout_margin("").err().unwrap(),
534 LayoutMarginParseError::TooFewValues
535 ));
536 assert!(matches!(
537 parse_layout_margin("1px 2px 3px 4px 5px").err().unwrap(),
538 LayoutMarginParseError::TooManyValues
539 ));
540 assert!(matches!(
541 parse_layout_margin("1px invalid").err().unwrap(),
542 LayoutMarginParseError::PixelValueParseError(_)
543 ));
544 }
545
546 #[test]
547 fn test_parse_longhand_spacing() {
548 assert_eq!(
549 parse_layout_padding_left("2em").unwrap(),
550 LayoutPaddingLeft {
551 inner: PixelValue::em(2.0)
552 }
553 );
554 assert!(parse_layout_margin_top("auto").is_err()); assert_eq!(
556 parse_layout_column_gap("20px").unwrap(),
557 LayoutColumnGap {
558 inner: PixelValue::px(20.0)
559 }
560 );
561 }
562}
563
564#[cfg(all(test, feature = "parser"))]
565mod autotest_generated {
566 #![allow(clippy::float_cmp)] use std::collections::hash_map::DefaultHasher;
569
570 #[allow(clippy::wildcard_imports)]
571 use super::*;
572 use alloc::format;
573 use core::{
574 fmt,
575 hash::{Hash, Hasher},
576 };
577
578 use crate::props::{
579 basic::{
580 length::SizeMetric,
581 pixel::{CssPixelValueParseError, PixelValue, PixelValueWithAuto},
582 },
583 formatter::FormatAsCssValue,
584 };
585
586 #[allow(missing_debug_implementations)]
589 struct AsCss<'a, T: FormatAsCssValue>(&'a T);
590
591 impl<T: FormatAsCssValue> fmt::Display for AsCss<'_, T> {
592 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
593 self.0.format_as_css_value(f)
594 }
595 }
596
597 fn hash_of<T: Hash>(value: &T) -> u64 {
598 let mut hasher = DefaultHasher::new();
599 value.hash(&mut hasher);
600 hasher.finish()
601 }
602
603 fn exact_px(value: f32) -> PixelValueWithAuto {
604 PixelValueWithAuto::Exact(PixelValue::px(value))
605 }
606
607 macro_rules! assert_css_roundtrip {
610 ($parse:ident, $input:expr) => {{
611 let parsed = $parse($input).expect("positive control must parse");
612 let printed = parsed.print_as_css_value();
613 let reparsed = $parse(printed.as_str()).expect("printed value must re-parse");
614 assert_eq!(
615 parsed,
616 reparsed,
617 "{} did not survive {:?} -> {:?}",
618 stringify!($parse),
619 $input,
620 printed
621 );
622 assert_eq!(
623 printed,
624 format!("{}", AsCss(&parsed)),
625 "FormatAsCssValue and PrintAsCssValue disagree for {:?}",
626 $input
627 );
628 }};
629 }
630
631 macro_rules! assert_all_longhands_err {
632 ($input:expr) => {{
633 assert!(
634 parse_layout_padding_top($input).is_err(),
635 "padding-top accepted {:?}",
636 $input
637 );
638 assert!(
639 parse_layout_padding_right($input).is_err(),
640 "padding-right accepted {:?}",
641 $input
642 );
643 assert!(
644 parse_layout_padding_bottom($input).is_err(),
645 "padding-bottom accepted {:?}",
646 $input
647 );
648 assert!(
649 parse_layout_padding_left($input).is_err(),
650 "padding-left accepted {:?}",
651 $input
652 );
653 assert!(
654 parse_layout_padding_inline_start($input).is_err(),
655 "padding-inline-start accepted {:?}",
656 $input
657 );
658 assert!(
659 parse_layout_padding_inline_end($input).is_err(),
660 "padding-inline-end accepted {:?}",
661 $input
662 );
663 assert!(
664 parse_layout_margin_top($input).is_err(),
665 "margin-top accepted {:?}",
666 $input
667 );
668 assert!(
669 parse_layout_margin_right($input).is_err(),
670 "margin-right accepted {:?}",
671 $input
672 );
673 assert!(
674 parse_layout_margin_bottom($input).is_err(),
675 "margin-bottom accepted {:?}",
676 $input
677 );
678 assert!(
679 parse_layout_margin_left($input).is_err(),
680 "margin-left accepted {:?}",
681 $input
682 );
683 assert!(
684 parse_layout_column_gap($input).is_err(),
685 "column-gap accepted {:?}",
686 $input
687 );
688 assert!(
689 parse_layout_row_gap($input).is_err(),
690 "row-gap accepted {:?}",
691 $input
692 );
693 }};
694 }
695
696 #[test]
699 fn minimal_valid_inputs_parse_to_the_documented_values() {
700 assert_eq!(
701 parse_layout_padding("0").unwrap(),
702 LayoutPadding {
703 top: exact_px(0.0),
704 right: exact_px(0.0),
705 bottom: exact_px(0.0),
706 left: exact_px(0.0),
707 }
708 );
709 assert_eq!(
710 parse_layout_margin("1px").unwrap(),
711 LayoutMargin {
712 top: exact_px(1.0),
713 right: exact_px(1.0),
714 bottom: exact_px(1.0),
715 left: exact_px(1.0),
716 }
717 );
718 }
719
720 #[test]
721 fn shorthand_expansion_follows_the_css_1_to_4_value_rules() {
722 let one = parse_layout_padding("7px").unwrap();
723 assert_eq!(one.top, exact_px(7.0));
724 assert_eq!(one.right, one.top);
725 assert_eq!(one.bottom, one.top);
726 assert_eq!(one.left, one.top);
727
728 let two = parse_layout_padding("1px 2px").unwrap();
729 assert_eq!((two.top, two.bottom), (exact_px(1.0), exact_px(1.0)));
730 assert_eq!((two.right, two.left), (exact_px(2.0), exact_px(2.0)));
731
732 let three = parse_layout_padding("1px 2px 3px").unwrap();
735 assert_eq!(three.top, exact_px(1.0));
736 assert_eq!(three.right, exact_px(2.0));
737 assert_eq!(three.bottom, exact_px(3.0));
738 assert_eq!(three.left, exact_px(2.0));
739
740 let four = parse_layout_padding("1px 2px 3px 4px").unwrap();
742 assert_eq!(four.top, exact_px(1.0));
743 assert_eq!(four.right, exact_px(2.0));
744 assert_eq!(four.bottom, exact_px(3.0));
745 assert_eq!(four.left, exact_px(4.0));
746 }
747
748 #[test]
749 fn margin_is_a_faithful_mirror_of_padding() {
750 for input in [
751 "0",
752 "10px",
753 "5% 2em",
754 "1px 2px 3px",
755 "1px 2px 3px 4px",
756 "auto",
757 "10px auto",
758 "auto 0 inherit 2em",
759 ] {
760 let p = parse_layout_padding(input).unwrap();
761 let m = parse_layout_margin(input).unwrap();
762 assert_eq!(m.top, p.top, "top differs for {input:?}");
763 assert_eq!(m.right, p.right, "right differs for {input:?}");
764 assert_eq!(m.bottom, p.bottom, "bottom differs for {input:?}");
765 assert_eq!(m.left, p.left, "left differs for {input:?}");
766 }
767
768 assert!(matches!(
770 parse_layout_margin(""),
771 Err(LayoutMarginParseError::TooFewValues)
772 ));
773 assert!(matches!(
774 parse_layout_margin("1 2 3 4 5"),
775 Err(LayoutMarginParseError::TooManyValues)
776 ));
777 assert!(matches!(
778 parse_layout_margin("nope"),
779 Err(LayoutMarginParseError::PixelValueParseError(_))
780 ));
781 }
782
783 #[test]
786 fn empty_and_whitespace_only_input_is_too_few_values() {
787 for input in [
788 "", " ", " ", "\t", "\n", "\r\n", "\x0b", "\x0c", " \t\r\n ",
789 ] {
790 assert!(
791 matches!(
792 parse_layout_padding(input),
793 Err(LayoutPaddingParseError::TooFewValues)
794 ),
795 "padding {input:?}"
796 );
797 assert!(
798 matches!(
799 parse_layout_margin(input),
800 Err(LayoutMarginParseError::TooFewValues)
801 ),
802 "margin {input:?}"
803 );
804 }
805 }
806
807 #[test]
808 fn garbage_is_rejected_without_panicking() {
809 for input in [
810 "oops",
811 "px",
812 "%",
813 "-",
814 "+",
815 ".",
816 "e",
817 "--",
818 "10px;",
819 "10px,20px",
820 "10px, 20px",
821 "10px!important",
822 "calc(1px + 2px)",
823 "1px/2px",
824 "#10px",
825 "0x10px",
826 "1_000px",
827 "auto auto auto auto auto",
828 ] {
829 assert!(
830 parse_layout_padding(input).is_err(),
831 "padding accepted {input:?}"
832 );
833 assert!(
834 parse_layout_margin(input).is_err(),
835 "margin accepted {input:?}"
836 );
837 }
838 }
839
840 #[test]
841 fn shorthand_rejects_a_unit_split_from_its_number() {
842 let err = parse_layout_padding("10 px").unwrap_err();
846 assert!(
847 matches!(
848 err,
849 LayoutPaddingParseError::PixelValueParseError(
850 CssPixelValueParseError::NoValueGiven("px", SizeMetric::Px)
851 )
852 ),
853 "expected NoValueGiven(\"px\", Px), got {err:?}"
854 );
855 assert_eq!(
856 parse_layout_padding_top("10 px").unwrap(),
857 LayoutPaddingTop::px(10.0)
858 );
859 }
860
861 #[test]
862 fn value_errors_are_reported_before_arity_errors() {
863 assert!(matches!(
866 parse_layout_padding("1px 2px 3px 4px 5px"),
867 Err(LayoutPaddingParseError::TooManyValues)
868 ));
869 assert!(matches!(
870 parse_layout_padding("1px 2px 3px 4px bogus"),
871 Err(LayoutPaddingParseError::PixelValueParseError(_))
872 ));
873 assert!(matches!(
874 parse_layout_margin("1px 2px 3px 4px bogus"),
875 Err(LayoutMarginParseError::PixelValueParseError(_))
876 ));
877 }
878
879 #[test]
880 fn boundary_numbers_saturate_instead_of_overflowing() {
881 let zero = parse_layout_padding("0").unwrap();
883 assert_eq!(parse_layout_padding("-0").unwrap(), zero);
884 assert_eq!(zero.top, PixelValueWithAuto::Exact(PixelValue::zero()));
885
886 for input in [
889 "1e39px",
890 "-1e39px",
891 "inf",
892 "-inf",
893 "infinity",
894 "9223372036854775807",
895 "-9223372036854775808",
896 "340282350000000000000000000000000000000px",
897 ] {
898 let parsed =
899 parse_layout_padding(input).unwrap_or_else(|e| panic!("{input:?} failed: {e}"));
900 let PixelValueWithAuto::Exact(value) = parsed.top else {
901 panic!("{input:?} did not parse to an exact length");
902 };
903 let raw = value.number.get();
904 assert!(
905 raw.is_finite(),
906 "{input:?} produced a non-finite length: {raw}"
907 );
908 }
909
910 let nan = parse_layout_padding("NaN").unwrap();
915 assert_eq!(nan.top, PixelValueWithAuto::Exact(PixelValue::zero()));
916 assert_eq!(nan.right, nan.top);
917 assert_eq!(nan.bottom, nan.top);
918 assert_eq!(nan.left, nan.top);
919 }
920
921 #[test]
922 fn sub_milli_unit_values_truncate_toward_zero() {
923 assert_eq!(
925 parse_layout_padding_top("0.0004px").unwrap(),
926 LayoutPaddingTop::zero()
927 );
928 assert_eq!(
929 parse_layout_padding_top("-0.0009px").unwrap(),
930 LayoutPaddingTop::zero()
931 );
932 assert_eq!(
933 parse_layout_padding_top("1e-40px").unwrap(),
934 LayoutPaddingTop::zero()
935 );
936 assert_eq!(
938 parse_layout_padding_top("1.9999px")
939 .unwrap()
940 .inner
941 .number
942 .get(),
943 1.999
944 );
945 }
946
947 #[test]
948 fn unicode_junk_is_rejected_without_panicking() {
949 for input in [
950 "\u{1F600}", "10px\u{1F600}", "\u{FF11}\u{FF10}\u{FF50}\u{FF58}", "10px\u{0301}", "10\u{200b}px", "\u{661}\u{660}px", "\u{202E}10px", ] {
958 assert!(
959 parse_layout_padding(input).is_err(),
960 "padding accepted {input:?}"
961 );
962 assert!(
963 parse_layout_margin(input).is_err(),
964 "margin accepted {input:?}"
965 );
966 }
967 }
968
969 #[test]
970 fn unicode_whitespace_separates_values_even_though_css_only_splits_on_ascii() {
971 for sep in [" ", "\u{a0}", "\u{2003}"] {
977 let input = format!("10px{sep}20px");
978 let tokens = input.split_whitespace().count();
979 let parsed = parse_layout_padding(&input);
980 assert_eq!(
981 parsed.is_ok(),
982 tokens == 2,
983 "{input:?} split into {tokens} token(s)"
984 );
985 if let Ok(p) = parsed {
986 assert_eq!(p.top, exact_px(10.0));
987 assert_eq!(p.right, exact_px(20.0));
988 assert_eq!(p.bottom, p.top);
989 assert_eq!(p.left, p.right);
990 }
991 }
992 }
993
994 #[test]
995 fn extremely_long_inputs_terminate_without_panicking() {
996 let many = "1px ".repeat(200_000);
998 assert!(matches!(
999 parse_layout_padding(&many),
1000 Err(LayoutPaddingParseError::TooManyValues)
1001 ));
1002 assert!(matches!(
1003 parse_layout_margin(&many),
1004 Err(LayoutMarginParseError::TooManyValues)
1005 ));
1006
1007 let huge = format!("{}px", "9".repeat(100_000));
1009 let parsed = parse_layout_padding(&huge).unwrap();
1010 let PixelValueWithAuto::Exact(value) = parsed.top else {
1011 panic!("a huge number did not parse to an exact length");
1012 };
1013 assert!(value.number.get().is_finite());
1014 assert!(value.number.get() > 0.0);
1015
1016 let junk = "z".repeat(1_000_000);
1018 assert!(parse_layout_padding(&junk).is_err());
1019 assert!(parse_layout_margin(&junk).is_err());
1020 }
1021
1022 #[test]
1023 fn deeply_nested_brackets_do_not_stack_overflow() {
1024 let nested = format!("{}1px{}", "(".repeat(10_000), ")".repeat(10_000));
1027 assert!(
1028 matches!(
1029 parse_layout_padding(&nested),
1030 Err(LayoutPaddingParseError::PixelValueParseError(
1031 CssPixelValueParseError::InvalidPixelValue(_)
1032 ))
1033 ),
1034 "deeply nested input was not rejected as an invalid pixel value"
1035 );
1036
1037 let spread = format!("{n} {n} {n} {n}", n = "(".repeat(1_000));
1038 assert!(parse_layout_padding(&spread).is_err());
1039 assert!(parse_layout_margin(&spread).is_err());
1040 }
1041
1042 #[test]
1043 fn shorthands_accept_css_wide_keywords_per_side() {
1044 assert_eq!(
1049 parse_layout_padding("auto").unwrap().top,
1050 PixelValueWithAuto::Auto
1051 );
1052 assert_eq!(
1053 parse_layout_padding("none").unwrap().top,
1054 PixelValueWithAuto::None
1055 );
1056
1057 let mixed = parse_layout_padding("initial 10px inherit auto").unwrap();
1058 assert_eq!(mixed.top, PixelValueWithAuto::Initial);
1059 assert_eq!(mixed.right, exact_px(10.0));
1060 assert_eq!(mixed.bottom, PixelValueWithAuto::Inherit);
1061 assert_eq!(mixed.left, PixelValueWithAuto::Auto);
1062 }
1063
1064 #[test]
1067 fn arity_error_messages_name_the_right_property() {
1068 let pad_many = format!("{}", LayoutPaddingParseError::TooManyValues);
1071 let pad_few = format!("{}", LayoutPaddingParseError::TooFewValues);
1072 assert!(
1073 pad_many.contains("padding") && pad_many.contains("at most 4"),
1074 "{pad_many}"
1075 );
1076 assert!(pad_few.contains("padding"), "{pad_few}");
1077
1078 let margin_many = format!("{}", LayoutMarginParseError::TooManyValues);
1079 let margin_few = format!("{}", LayoutMarginParseError::TooFewValues);
1080 assert!(
1081 margin_many.contains("margin") && !margin_many.contains("padding"),
1082 "{margin_many}"
1083 );
1084 assert!(
1085 margin_few.contains("margin") && !margin_few.contains("padding"),
1086 "{margin_few}"
1087 );
1088
1089 assert_eq!(
1091 format!("{}", parse_layout_margin("1 2 3 4 5").unwrap_err()),
1092 margin_many
1093 );
1094 assert_eq!(
1095 format!("{}", parse_layout_padding("").unwrap_err()),
1096 pad_few
1097 );
1098 }
1099
1100 #[test]
1101 fn owned_and_shared_error_forms_round_trip() {
1102 assert_eq!(
1103 LayoutPaddingParseError::TooManyValues.to_contained(),
1104 LayoutPaddingParseErrorOwned::TooManyValues
1105 );
1106 assert_eq!(
1107 LayoutPaddingParseErrorOwned::TooFewValues.to_shared(),
1108 LayoutPaddingParseError::TooFewValues
1109 );
1110 assert_eq!(
1111 LayoutMarginParseError::TooFewValues.to_contained(),
1112 LayoutMarginParseErrorOwned::TooFewValues
1113 );
1114 assert_eq!(
1115 LayoutMarginParseErrorOwned::TooManyValues.to_shared(),
1116 LayoutMarginParseError::TooManyValues
1117 );
1118
1119 let owned = parse_layout_padding("1px oops").unwrap_err().to_contained();
1122 assert!(matches!(
1123 &owned,
1124 LayoutPaddingParseErrorOwned::PixelValueParseError(_)
1125 ));
1126 assert!(matches!(
1127 owned.to_shared(),
1128 LayoutPaddingParseError::PixelValueParseError(_)
1129 ));
1130
1131 let owned_margin = parse_layout_margin("1px oops").unwrap_err().to_contained();
1132 assert!(matches!(
1133 owned_margin.to_shared(),
1134 LayoutMarginParseError::PixelValueParseError(_)
1135 ));
1136 }
1137
1138 #[test]
1141 fn longhand_parsers_reject_keywords_and_empty_input() {
1142 for input in [
1145 "",
1146 " ",
1147 "auto",
1148 "none",
1149 "initial",
1150 "inherit",
1151 "oops",
1152 "10px 20px",
1153 ] {
1154 assert_all_longhands_err!(input);
1155 }
1156 }
1157
1158 #[test]
1159 fn every_longhand_spacing_parser_accepts_a_minimal_value() {
1160 assert_eq!(
1161 parse_layout_padding_top("0").unwrap(),
1162 LayoutPaddingTop::px(0.0)
1163 );
1164 assert_eq!(
1165 parse_layout_padding_right("1px").unwrap(),
1166 LayoutPaddingRight::px(1.0)
1167 );
1168 assert_eq!(
1169 parse_layout_padding_bottom("2pt").unwrap(),
1170 LayoutPaddingBottom::pt(2.0)
1171 );
1172 assert_eq!(
1173 parse_layout_padding_left("2em").unwrap(),
1174 LayoutPaddingLeft::em(2.0)
1175 );
1176 assert_eq!(
1177 parse_layout_padding_inline_start("3px").unwrap(),
1178 LayoutPaddingInlineStart::px(3.0)
1179 );
1180 assert_eq!(
1181 parse_layout_padding_inline_end("4px").unwrap(),
1182 LayoutPaddingInlineEnd::px(4.0)
1183 );
1184 assert_eq!(
1185 parse_layout_margin_top("-5px").unwrap(),
1186 LayoutMarginTop::px(-5.0)
1187 );
1188 assert_eq!(
1189 parse_layout_margin_right("6%").unwrap(),
1190 LayoutMarginRight::percent(6.0)
1191 );
1192 assert_eq!(
1193 parse_layout_margin_bottom("7px").unwrap(),
1194 LayoutMarginBottom::px(7.0)
1195 );
1196 assert_eq!(
1197 parse_layout_margin_left("8px").unwrap(),
1198 LayoutMarginLeft::px(8.0)
1199 );
1200 assert_eq!(
1201 parse_layout_column_gap("20px").unwrap(),
1202 LayoutColumnGap::px(20.0)
1203 );
1204 assert_eq!(
1205 parse_layout_row_gap("1.5em").unwrap(),
1206 LayoutRowGap::em(1.5)
1207 );
1208 }
1209
1210 #[test]
1211 fn every_longhand_spacing_parser_round_trips_through_its_printed_form() {
1212 for input in [
1215 "0", "1px", "10.5px", "1.5em", "2rem", "-20pt", "50%", "0.125px", "3.25in", "12.75mm",
1216 "2.54cm", "0.5vmin", "4vmax", "8vw", "100vh",
1217 ] {
1218 assert_css_roundtrip!(parse_layout_padding_top, input);
1219 assert_css_roundtrip!(parse_layout_padding_right, input);
1220 assert_css_roundtrip!(parse_layout_padding_bottom, input);
1221 assert_css_roundtrip!(parse_layout_padding_left, input);
1222 assert_css_roundtrip!(parse_layout_padding_inline_start, input);
1223 assert_css_roundtrip!(parse_layout_padding_inline_end, input);
1224 assert_css_roundtrip!(parse_layout_margin_top, input);
1225 assert_css_roundtrip!(parse_layout_margin_right, input);
1226 assert_css_roundtrip!(parse_layout_margin_bottom, input);
1227 assert_css_roundtrip!(parse_layout_margin_left, input);
1228 assert_css_roundtrip!(parse_layout_column_gap, input);
1229 assert_css_roundtrip!(parse_layout_row_gap, input);
1230 }
1231 }
1232
1233 #[test]
1234 fn printed_css_matches_the_source_text_for_representable_values() {
1235 assert_eq!(
1236 parse_layout_padding_top("10px")
1237 .unwrap()
1238 .print_as_css_value(),
1239 "10px"
1240 );
1241 assert_eq!(
1242 parse_layout_column_gap("50%").unwrap().print_as_css_value(),
1243 "50%"
1244 );
1245 assert_eq!(
1246 parse_layout_margin_left("-2.5em")
1247 .unwrap()
1248 .print_as_css_value(),
1249 "-2.5em"
1250 );
1251 assert_eq!(LayoutRowGap::zero().print_as_css_value(), "0px");
1252 assert_eq!(
1254 parse_layout_padding_bottom("3")
1255 .unwrap()
1256 .print_as_css_value(),
1257 "3px"
1258 );
1259 }
1260
1261 #[test]
1264 fn const_and_runtime_constructors_agree() {
1265 assert_eq!(LayoutPaddingLeft::const_px(5), LayoutPaddingLeft::px(5.0));
1266 assert_eq!(LayoutPaddingLeft::const_em(2), LayoutPaddingLeft::em(2.0));
1267 assert_eq!(LayoutPaddingLeft::const_pt(-3), LayoutPaddingLeft::pt(-3.0));
1268 assert_eq!(
1269 LayoutPaddingLeft::const_percent(50),
1270 LayoutPaddingLeft::percent(50.0)
1271 );
1272 assert_eq!(
1273 LayoutColumnGap::const_from_metric(SizeMetric::Vh, 7),
1274 LayoutColumnGap::from_metric(SizeMetric::Vh, 7.0)
1275 );
1276 assert_eq!(
1277 LayoutColumnGap::const_in(1),
1278 LayoutColumnGap::from_metric(SizeMetric::In, 1.0)
1279 );
1280 assert_eq!(
1281 LayoutColumnGap::const_cm(2),
1282 LayoutColumnGap::from_metric(SizeMetric::Cm, 2.0)
1283 );
1284 assert_eq!(
1285 LayoutColumnGap::const_mm(3),
1286 LayoutColumnGap::from_metric(SizeMetric::Mm, 3.0)
1287 );
1288
1289 assert_eq!(
1291 LayoutRowGap::from_pixel_value(PixelValue::em(2.0)).inner,
1292 PixelValue::em(2.0)
1293 );
1294
1295 assert_eq!(LayoutPaddingBottom::zero(), LayoutPaddingBottom::default());
1296 assert_eq!(
1297 LayoutPaddingBottom::default().inner.metric,
1298 SizeMetric::Px,
1299 "the default spacing metric is px"
1300 );
1301 }
1302
1303 #[test]
1304 fn ordering_is_metric_major_not_physical_length() {
1305 assert!(LayoutPaddingTop::px(1000.0) < LayoutPaddingTop::pt(0.0));
1309 assert!(LayoutPaddingTop::pt(0.0) < LayoutPaddingTop::em(0.0));
1310
1311 assert!(LayoutPaddingTop::px(1.0) < LayoutPaddingTop::px(2.0));
1313 assert!(LayoutMarginLeft::const_px(-5) < LayoutMarginLeft::zero());
1314 }
1315
1316 #[test]
1317 fn equal_values_hash_equal_across_signed_zero_and_quantisation() {
1318 let pos = LayoutMarginTop::px(0.0);
1319 let neg = LayoutMarginTop::px(-0.0);
1320 assert_eq!(pos, neg, "signed zero must have one canonical encoding");
1321 assert_eq!(hash_of(&pos), hash_of(&neg));
1322 assert_eq!(pos, LayoutMarginTop::zero());
1323
1324 let a = LayoutMarginTop::px(1.0001);
1327 let b = LayoutMarginTop::px(1.0009);
1328 assert_eq!(a, b);
1329 assert_eq!(hash_of(&a), hash_of(&b));
1330
1331 assert_ne!(LayoutMarginTop::px(1.0), LayoutMarginTop::em(1.0));
1333 }
1334
1335 #[test]
1336 fn debug_renders_the_value_as_css() {
1337 assert_eq!(format!("{:?}", LayoutPaddingTop::px(10.0)), "10px");
1338 assert_eq!(format!("{:?}", LayoutColumnGap::percent(50.0)), "50%");
1339 assert_eq!(format!("{:?}", LayoutRowGap::zero()), "0px");
1340 }
1341
1342 #[test]
1343 fn interpolate_hits_its_endpoints_and_survives_nan_and_huge_t() {
1344 let a = LayoutRowGap::px(0.0);
1345 let b = LayoutRowGap::px(10.0);
1346 assert_eq!(a.interpolate(&b, 0.0), a);
1347 assert_eq!(a.interpolate(&b, 1.0), b);
1348 assert_eq!(a.interpolate(&b, 0.5), LayoutRowGap::px(5.0));
1349
1350 let nan = a.interpolate(&b, f32::NAN);
1353 assert_eq!(nan.inner.metric, SizeMetric::Px);
1354 assert_eq!(nan.inner.number.get(), 0.0);
1355
1356 for t in [1e30_f32, -1e30_f32, f32::INFINITY, f32::NEG_INFINITY] {
1358 let out = a.interpolate(&b, t);
1359 assert!(
1360 out.inner.number.get().is_finite(),
1361 "t = {t} produced a non-finite length"
1362 );
1363 }
1364
1365 let mixed = LayoutRowGap::px(0.0).interpolate(&LayoutRowGap::em(1.0), 1.0);
1367 assert_eq!(mixed.inner.metric, SizeMetric::Px);
1368 assert!(mixed.inner.number.get().is_finite());
1369 }
1370}