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)] #[cfg(feature = "parser")]
201#[derive(Debug, Clone, PartialEq, Eq)]
202#[repr(C, u8)]
203pub enum LayoutPaddingParseErrorOwned {
204 PixelValueParseError(CssPixelValueParseErrorOwned),
205 TooManyValues,
206 TooFewValues,
207}
208
209#[cfg(feature = "parser")]
210impl_spacing_parse_error!(LayoutPaddingParseError, LayoutPaddingParseErrorOwned, "padding");
211
212#[cfg(feature = "parser")]
214#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
215pub struct LayoutPadding {
216 pub top: PixelValueWithAuto,
217 pub bottom: PixelValueWithAuto,
218 pub left: PixelValueWithAuto,
219 pub right: PixelValueWithAuto,
220}
221
222#[cfg(feature = "parser")]
223pub fn parse_layout_padding(
227 input: &str,
228) -> Result<LayoutPadding, LayoutPaddingParseError<'_>> {
229 let values: Vec<_> = input.split_whitespace().collect();
230
231 let parsed_values: Vec<PixelValueWithAuto> = values
232 .iter()
233 .map(|s| parse_pixel_value_with_auto(s))
234 .collect::<Result<_, _>>()?;
235
236 match parsed_values.len() {
237 1 => {
238 let all = parsed_values[0];
240 Ok(LayoutPadding {
241 top: all,
242 right: all,
243 bottom: all,
244 left: all,
245 })
246 }
247 2 => {
248 let vertical = parsed_values[0];
250 let horizontal = parsed_values[1];
251 Ok(LayoutPadding {
252 top: vertical,
253 right: horizontal,
254 bottom: vertical,
255 left: horizontal,
256 })
257 }
258 3 => {
259 let top = parsed_values[0];
261 let horizontal = parsed_values[1];
262 let bottom = parsed_values[2];
263 Ok(LayoutPadding {
264 top,
265 right: horizontal,
266 bottom,
267 left: horizontal,
268 })
269 }
270 4 => {
271 Ok(LayoutPadding {
273 top: parsed_values[0],
274 right: parsed_values[1],
275 bottom: parsed_values[2],
276 left: parsed_values[3],
277 })
278 }
279 0 => Err(LayoutPaddingParseError::TooFewValues),
280 _ => Err(LayoutPaddingParseError::TooManyValues),
281 }
282}
283
284#[cfg(feature = "parser")]
288#[derive(Clone, PartialEq, Eq)]
289pub enum LayoutMarginParseError<'a> {
290 PixelValueParseError(CssPixelValueParseError<'a>),
291 TooManyValues,
292 TooFewValues,
293}
294#[allow(variant_size_differences)] #[cfg(feature = "parser")]
297#[derive(Debug, Clone, PartialEq, Eq)]
298#[repr(C, u8)]
299pub enum LayoutMarginParseErrorOwned {
300 PixelValueParseError(CssPixelValueParseErrorOwned),
301 TooManyValues,
302 TooFewValues,
303}
304
305#[cfg(feature = "parser")]
306impl_spacing_parse_error!(LayoutMarginParseError, LayoutMarginParseErrorOwned, "margin");
307
308#[cfg(feature = "parser")]
310#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
311pub struct LayoutMargin {
312 pub top: PixelValueWithAuto,
313 pub bottom: PixelValueWithAuto,
314 pub left: PixelValueWithAuto,
315 pub right: PixelValueWithAuto,
316}
317
318#[cfg(feature = "parser")]
319pub fn parse_layout_margin(input: &str) -> Result<LayoutMargin, LayoutMarginParseError<'_>> {
323 match parse_layout_padding(input) {
326 Ok(padding) => Ok(LayoutMargin {
327 top: padding.top,
328 left: padding.left,
329 right: padding.right,
330 bottom: padding.bottom,
331 }),
332 Err(e) => match e {
333 LayoutPaddingParseError::PixelValueParseError(err) => {
334 Err(LayoutMarginParseError::PixelValueParseError(err))
335 }
336 LayoutPaddingParseError::TooManyValues => Err(LayoutMarginParseError::TooManyValues),
337 LayoutPaddingParseError::TooFewValues => Err(LayoutMarginParseError::TooFewValues),
338 },
339 }
340}
341
342macro_rules! typed_pixel_value_parser {
345 (
346 $fn:ident, $fn_str:expr, $return:ident, $return_str:expr, $import_str:expr, $test_str:expr
347 ) => {
348 #[doc = $return_str]
350 #[doc = $import_str]
356 #[doc = $test_str]
357 pub fn $fn(input: &str) -> Result<$return, CssPixelValueParseError<'_>> {
362 crate::props::basic::parse_pixel_value(input).map(|e| $return { inner: e })
363 }
364
365 impl crate::props::formatter::FormatAsCssValue for $return {
366 fn format_as_css_value(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
367 self.inner.format_as_css_value(f)
368 }
369 }
370 };
371 ($fn:ident, $return:ident) => {
372 typed_pixel_value_parser!(
373 $fn,
374 stringify!($fn),
375 $return,
376 stringify!($return),
377 concat!(
378 "# extern crate azul_css;",
379 "\r\n",
380 "# use azul_css::props::layout::spacing::",
381 stringify!($fn),
382 ";",
383 "\r\n",
384 "# use azul_css::props::basic::pixel::PixelValue;\r\n",
385 "# use azul_css::props::layout::spacing::",
386 stringify!($return),
387 ";\r\n"
388 ),
389 concat!(
390 "assert_eq!(",
391 stringify!($fn),
392 "(\"5px\"), Ok(",
393 stringify!($return),
394 " { inner: PixelValue::px(5.0) }));"
395 )
396 );
397 };
398}
399
400#[cfg(feature = "parser")]
401typed_pixel_value_parser!(parse_layout_padding_top, LayoutPaddingTop);
402#[cfg(feature = "parser")]
403typed_pixel_value_parser!(parse_layout_padding_right, LayoutPaddingRight);
404#[cfg(feature = "parser")]
405typed_pixel_value_parser!(parse_layout_padding_bottom, LayoutPaddingBottom);
406#[cfg(feature = "parser")]
407typed_pixel_value_parser!(parse_layout_padding_left, LayoutPaddingLeft);
408#[cfg(feature = "parser")]
409typed_pixel_value_parser!(parse_layout_padding_inline_start, LayoutPaddingInlineStart);
410#[cfg(feature = "parser")]
411typed_pixel_value_parser!(parse_layout_padding_inline_end, LayoutPaddingInlineEnd);
412
413#[cfg(feature = "parser")]
414typed_pixel_value_parser!(parse_layout_margin_top, LayoutMarginTop);
415#[cfg(feature = "parser")]
416typed_pixel_value_parser!(parse_layout_margin_right, LayoutMarginRight);
417#[cfg(feature = "parser")]
418typed_pixel_value_parser!(parse_layout_margin_bottom, LayoutMarginBottom);
419#[cfg(feature = "parser")]
420typed_pixel_value_parser!(parse_layout_margin_left, LayoutMarginLeft);
421
422#[cfg(feature = "parser")]
423typed_pixel_value_parser!(parse_layout_column_gap, LayoutColumnGap);
424#[cfg(feature = "parser")]
425typed_pixel_value_parser!(parse_layout_row_gap, LayoutRowGap);
426
427#[cfg(all(test, feature = "parser"))]
428mod tests {
429 use super::*;
430 use crate::props::basic::pixel::{PixelValue, PixelValueWithAuto};
431
432 #[test]
433 fn test_parse_layout_padding_shorthand() {
434 let result = parse_layout_padding("10px").unwrap();
436 assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(10.0)));
437 assert_eq!(
438 result.right,
439 PixelValueWithAuto::Exact(PixelValue::px(10.0))
440 );
441 assert_eq!(
442 result.bottom,
443 PixelValueWithAuto::Exact(PixelValue::px(10.0))
444 );
445 assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::px(10.0)));
446
447 let result = parse_layout_padding("5% 2em").unwrap();
449 assert_eq!(
450 result.top,
451 PixelValueWithAuto::Exact(PixelValue::percent(5.0))
452 );
453 assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::em(2.0)));
454 assert_eq!(
455 result.bottom,
456 PixelValueWithAuto::Exact(PixelValue::percent(5.0))
457 );
458 assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::em(2.0)));
459
460 let result = parse_layout_padding("1px 2px 3px").unwrap();
462 assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(1.0)));
463 assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
464 assert_eq!(
465 result.bottom,
466 PixelValueWithAuto::Exact(PixelValue::px(3.0))
467 );
468 assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
469
470 let result = parse_layout_padding("1px 2px 3px 4px").unwrap();
472 assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(1.0)));
473 assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
474 assert_eq!(
475 result.bottom,
476 PixelValueWithAuto::Exact(PixelValue::px(3.0))
477 );
478 assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::px(4.0)));
479
480 let result = parse_layout_padding(" 1px 2px ").unwrap();
482 assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(1.0)));
483 assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
484 }
485
486 #[test]
487 fn test_parse_layout_padding_errors() {
488 assert!(matches!(
489 parse_layout_padding("").err().unwrap(),
490 LayoutPaddingParseError::TooFewValues
491 ));
492 assert!(matches!(
493 parse_layout_padding("1px 2px 3px 4px 5px").err().unwrap(),
494 LayoutPaddingParseError::TooManyValues
495 ));
496 assert!(matches!(
497 parse_layout_padding("1px oops 3px").err().unwrap(),
498 LayoutPaddingParseError::PixelValueParseError(_)
499 ));
500 }
501
502 #[test]
503 fn test_parse_layout_margin_shorthand() {
504 let result = parse_layout_margin("auto").unwrap();
506 assert_eq!(result.top, PixelValueWithAuto::Auto);
507 assert_eq!(result.right, PixelValueWithAuto::Auto);
508 assert_eq!(result.bottom, PixelValueWithAuto::Auto);
509 assert_eq!(result.left, PixelValueWithAuto::Auto);
510
511 let result = parse_layout_margin("10px auto").unwrap();
513 assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(10.0)));
514 assert_eq!(result.right, PixelValueWithAuto::Auto);
515 assert_eq!(
516 result.bottom,
517 PixelValueWithAuto::Exact(PixelValue::px(10.0))
518 );
519 assert_eq!(result.left, PixelValueWithAuto::Auto);
520 }
521
522 #[test]
523 fn test_parse_layout_margin_errors() {
524 assert!(matches!(
525 parse_layout_margin("").err().unwrap(),
526 LayoutMarginParseError::TooFewValues
527 ));
528 assert!(matches!(
529 parse_layout_margin("1px 2px 3px 4px 5px").err().unwrap(),
530 LayoutMarginParseError::TooManyValues
531 ));
532 assert!(matches!(
533 parse_layout_margin("1px invalid").err().unwrap(),
534 LayoutMarginParseError::PixelValueParseError(_)
535 ));
536 }
537
538 #[test]
539 fn test_parse_longhand_spacing() {
540 assert_eq!(
541 parse_layout_padding_left("2em").unwrap(),
542 LayoutPaddingLeft {
543 inner: PixelValue::em(2.0)
544 }
545 );
546 assert!(parse_layout_margin_top("auto").is_err()); assert_eq!(
548 parse_layout_column_gap("20px").unwrap(),
549 LayoutColumnGap {
550 inner: PixelValue::px(20.0)
551 }
552 );
553 }
554}
555
556#[cfg(all(test, feature = "parser"))]
557mod autotest_generated {
558 #![allow(clippy::float_cmp)] use std::collections::hash_map::DefaultHasher;
561
562 #[allow(clippy::wildcard_imports)]
563 use super::*;
564 use alloc::format;
565 use core::{
566 fmt,
567 hash::{Hash, Hasher},
568 };
569
570 use crate::props::{
571 basic::{
572 length::SizeMetric,
573 pixel::{CssPixelValueParseError, PixelValue, PixelValueWithAuto},
574 },
575 formatter::FormatAsCssValue,
576 };
577
578 #[allow(missing_debug_implementations)]
581 struct AsCss<'a, T: FormatAsCssValue>(&'a T);
582
583 impl<T: FormatAsCssValue> fmt::Display for AsCss<'_, T> {
584 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
585 self.0.format_as_css_value(f)
586 }
587 }
588
589 fn hash_of<T: Hash>(value: &T) -> u64 {
590 let mut hasher = DefaultHasher::new();
591 value.hash(&mut hasher);
592 hasher.finish()
593 }
594
595 fn exact_px(value: f32) -> PixelValueWithAuto {
596 PixelValueWithAuto::Exact(PixelValue::px(value))
597 }
598
599 macro_rules! assert_css_roundtrip {
602 ($parse:ident, $input:expr) => {{
603 let parsed = $parse($input).expect("positive control must parse");
604 let printed = parsed.print_as_css_value();
605 let reparsed = $parse(printed.as_str()).expect("printed value must re-parse");
606 assert_eq!(
607 parsed, reparsed,
608 "{} did not survive {:?} -> {:?}",
609 stringify!($parse),
610 $input,
611 printed
612 );
613 assert_eq!(
614 printed,
615 format!("{}", AsCss(&parsed)),
616 "FormatAsCssValue and PrintAsCssValue disagree for {:?}",
617 $input
618 );
619 }};
620 }
621
622 macro_rules! assert_all_longhands_err {
623 ($input:expr) => {{
624 assert!(parse_layout_padding_top($input).is_err(), "padding-top accepted {:?}", $input);
625 assert!(parse_layout_padding_right($input).is_err(), "padding-right accepted {:?}", $input);
626 assert!(parse_layout_padding_bottom($input).is_err(), "padding-bottom accepted {:?}", $input);
627 assert!(parse_layout_padding_left($input).is_err(), "padding-left accepted {:?}", $input);
628 assert!(parse_layout_padding_inline_start($input).is_err(), "padding-inline-start accepted {:?}", $input);
629 assert!(parse_layout_padding_inline_end($input).is_err(), "padding-inline-end accepted {:?}", $input);
630 assert!(parse_layout_margin_top($input).is_err(), "margin-top accepted {:?}", $input);
631 assert!(parse_layout_margin_right($input).is_err(), "margin-right accepted {:?}", $input);
632 assert!(parse_layout_margin_bottom($input).is_err(), "margin-bottom accepted {:?}", $input);
633 assert!(parse_layout_margin_left($input).is_err(), "margin-left accepted {:?}", $input);
634 assert!(parse_layout_column_gap($input).is_err(), "column-gap accepted {:?}", $input);
635 assert!(parse_layout_row_gap($input).is_err(), "row-gap accepted {:?}", $input);
636 }};
637 }
638
639 #[test]
642 fn minimal_valid_inputs_parse_to_the_documented_values() {
643 assert_eq!(
644 parse_layout_padding("0").unwrap(),
645 LayoutPadding {
646 top: exact_px(0.0),
647 right: exact_px(0.0),
648 bottom: exact_px(0.0),
649 left: exact_px(0.0),
650 }
651 );
652 assert_eq!(
653 parse_layout_margin("1px").unwrap(),
654 LayoutMargin {
655 top: exact_px(1.0),
656 right: exact_px(1.0),
657 bottom: exact_px(1.0),
658 left: exact_px(1.0),
659 }
660 );
661 }
662
663 #[test]
664 fn shorthand_expansion_follows_the_css_1_to_4_value_rules() {
665 let one = parse_layout_padding("7px").unwrap();
666 assert_eq!(one.top, exact_px(7.0));
667 assert_eq!(one.right, one.top);
668 assert_eq!(one.bottom, one.top);
669 assert_eq!(one.left, one.top);
670
671 let two = parse_layout_padding("1px 2px").unwrap();
672 assert_eq!((two.top, two.bottom), (exact_px(1.0), exact_px(1.0)));
673 assert_eq!((two.right, two.left), (exact_px(2.0), exact_px(2.0)));
674
675 let three = parse_layout_padding("1px 2px 3px").unwrap();
678 assert_eq!(three.top, exact_px(1.0));
679 assert_eq!(three.right, exact_px(2.0));
680 assert_eq!(three.bottom, exact_px(3.0));
681 assert_eq!(three.left, exact_px(2.0));
682
683 let four = parse_layout_padding("1px 2px 3px 4px").unwrap();
685 assert_eq!(four.top, exact_px(1.0));
686 assert_eq!(four.right, exact_px(2.0));
687 assert_eq!(four.bottom, exact_px(3.0));
688 assert_eq!(four.left, exact_px(4.0));
689 }
690
691 #[test]
692 fn margin_is_a_faithful_mirror_of_padding() {
693 for input in [
694 "0",
695 "10px",
696 "5% 2em",
697 "1px 2px 3px",
698 "1px 2px 3px 4px",
699 "auto",
700 "10px auto",
701 "auto 0 inherit 2em",
702 ] {
703 let p = parse_layout_padding(input).unwrap();
704 let m = parse_layout_margin(input).unwrap();
705 assert_eq!(m.top, p.top, "top differs for {input:?}");
706 assert_eq!(m.right, p.right, "right differs for {input:?}");
707 assert_eq!(m.bottom, p.bottom, "bottom differs for {input:?}");
708 assert_eq!(m.left, p.left, "left differs for {input:?}");
709 }
710
711 assert!(matches!(
713 parse_layout_margin(""),
714 Err(LayoutMarginParseError::TooFewValues)
715 ));
716 assert!(matches!(
717 parse_layout_margin("1 2 3 4 5"),
718 Err(LayoutMarginParseError::TooManyValues)
719 ));
720 assert!(matches!(
721 parse_layout_margin("nope"),
722 Err(LayoutMarginParseError::PixelValueParseError(_))
723 ));
724 }
725
726 #[test]
729 fn empty_and_whitespace_only_input_is_too_few_values() {
730 for input in ["", " ", " ", "\t", "\n", "\r\n", "\x0b", "\x0c", " \t\r\n "] {
731 assert!(
732 matches!(
733 parse_layout_padding(input),
734 Err(LayoutPaddingParseError::TooFewValues)
735 ),
736 "padding {input:?}"
737 );
738 assert!(
739 matches!(
740 parse_layout_margin(input),
741 Err(LayoutMarginParseError::TooFewValues)
742 ),
743 "margin {input:?}"
744 );
745 }
746 }
747
748 #[test]
749 fn garbage_is_rejected_without_panicking() {
750 for input in [
751 "oops",
752 "px",
753 "%",
754 "-",
755 "+",
756 ".",
757 "e",
758 "--",
759 "10px;",
760 "10px,20px",
761 "10px, 20px",
762 "10px!important",
763 "calc(1px + 2px)",
764 "1px/2px",
765 "#10px",
766 "0x10px",
767 "1_000px",
768 "auto auto auto auto auto",
769 ] {
770 assert!(
771 parse_layout_padding(input).is_err(),
772 "padding accepted {input:?}"
773 );
774 assert!(
775 parse_layout_margin(input).is_err(),
776 "margin accepted {input:?}"
777 );
778 }
779 }
780
781 #[test]
782 fn shorthand_rejects_a_unit_split_from_its_number() {
783 let err = parse_layout_padding("10 px").unwrap_err();
787 assert!(
788 matches!(
789 err,
790 LayoutPaddingParseError::PixelValueParseError(
791 CssPixelValueParseError::NoValueGiven("px", SizeMetric::Px)
792 )
793 ),
794 "expected NoValueGiven(\"px\", Px), got {err:?}"
795 );
796 assert_eq!(
797 parse_layout_padding_top("10 px").unwrap(),
798 LayoutPaddingTop::px(10.0)
799 );
800 }
801
802 #[test]
803 fn value_errors_are_reported_before_arity_errors() {
804 assert!(matches!(
807 parse_layout_padding("1px 2px 3px 4px 5px"),
808 Err(LayoutPaddingParseError::TooManyValues)
809 ));
810 assert!(matches!(
811 parse_layout_padding("1px 2px 3px 4px bogus"),
812 Err(LayoutPaddingParseError::PixelValueParseError(_))
813 ));
814 assert!(matches!(
815 parse_layout_margin("1px 2px 3px 4px bogus"),
816 Err(LayoutMarginParseError::PixelValueParseError(_))
817 ));
818 }
819
820 #[test]
821 fn boundary_numbers_saturate_instead_of_overflowing() {
822 let zero = parse_layout_padding("0").unwrap();
824 assert_eq!(parse_layout_padding("-0").unwrap(), zero);
825 assert_eq!(zero.top, PixelValueWithAuto::Exact(PixelValue::zero()));
826
827 for input in [
830 "1e39px",
831 "-1e39px",
832 "inf",
833 "-inf",
834 "infinity",
835 "9223372036854775807",
836 "-9223372036854775808",
837 "340282350000000000000000000000000000000px",
838 ] {
839 let parsed =
840 parse_layout_padding(input).unwrap_or_else(|e| panic!("{input:?} failed: {e}"));
841 let PixelValueWithAuto::Exact(value) = parsed.top else {
842 panic!("{input:?} did not parse to an exact length");
843 };
844 let raw = value.number.get();
845 assert!(raw.is_finite(), "{input:?} produced a non-finite length: {raw}");
846 }
847
848 let nan = parse_layout_padding("NaN").unwrap();
853 assert_eq!(nan.top, PixelValueWithAuto::Exact(PixelValue::zero()));
854 assert_eq!(nan.right, nan.top);
855 assert_eq!(nan.bottom, nan.top);
856 assert_eq!(nan.left, nan.top);
857 }
858
859 #[test]
860 fn sub_milli_unit_values_truncate_toward_zero() {
861 assert_eq!(
863 parse_layout_padding_top("0.0004px").unwrap(),
864 LayoutPaddingTop::zero()
865 );
866 assert_eq!(
867 parse_layout_padding_top("-0.0009px").unwrap(),
868 LayoutPaddingTop::zero()
869 );
870 assert_eq!(
871 parse_layout_padding_top("1e-40px").unwrap(),
872 LayoutPaddingTop::zero()
873 );
874 assert_eq!(
876 parse_layout_padding_top("1.9999px").unwrap().inner.number.get(),
877 1.999
878 );
879 }
880
881 #[test]
882 fn unicode_junk_is_rejected_without_panicking() {
883 for input in [
884 "\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", ] {
892 assert!(
893 parse_layout_padding(input).is_err(),
894 "padding accepted {input:?}"
895 );
896 assert!(
897 parse_layout_margin(input).is_err(),
898 "margin accepted {input:?}"
899 );
900 }
901 }
902
903 #[test]
904 fn unicode_whitespace_separates_values_even_though_css_only_splits_on_ascii() {
905 for sep in [" ", "\u{a0}", "\u{2003}"] {
911 let input = format!("10px{sep}20px");
912 let tokens = input.split_whitespace().count();
913 let parsed = parse_layout_padding(&input);
914 assert_eq!(
915 parsed.is_ok(),
916 tokens == 2,
917 "{input:?} split into {tokens} token(s)"
918 );
919 if let Ok(p) = parsed {
920 assert_eq!(p.top, exact_px(10.0));
921 assert_eq!(p.right, exact_px(20.0));
922 assert_eq!(p.bottom, p.top);
923 assert_eq!(p.left, p.right);
924 }
925 }
926 }
927
928 #[test]
929 fn extremely_long_inputs_terminate_without_panicking() {
930 let many = "1px ".repeat(200_000);
932 assert!(matches!(
933 parse_layout_padding(&many),
934 Err(LayoutPaddingParseError::TooManyValues)
935 ));
936 assert!(matches!(
937 parse_layout_margin(&many),
938 Err(LayoutMarginParseError::TooManyValues)
939 ));
940
941 let huge = format!("{}px", "9".repeat(100_000));
943 let parsed = parse_layout_padding(&huge).unwrap();
944 let PixelValueWithAuto::Exact(value) = parsed.top else {
945 panic!("a huge number did not parse to an exact length");
946 };
947 assert!(value.number.get().is_finite());
948 assert!(value.number.get() > 0.0);
949
950 let junk = "z".repeat(1_000_000);
952 assert!(parse_layout_padding(&junk).is_err());
953 assert!(parse_layout_margin(&junk).is_err());
954 }
955
956 #[test]
957 fn deeply_nested_brackets_do_not_stack_overflow() {
958 let nested = format!("{}1px{}", "(".repeat(10_000), ")".repeat(10_000));
961 assert!(
962 matches!(
963 parse_layout_padding(&nested),
964 Err(LayoutPaddingParseError::PixelValueParseError(
965 CssPixelValueParseError::InvalidPixelValue(_)
966 ))
967 ),
968 "deeply nested input was not rejected as an invalid pixel value"
969 );
970
971 let spread = format!("{n} {n} {n} {n}", n = "(".repeat(1_000));
972 assert!(parse_layout_padding(&spread).is_err());
973 assert!(parse_layout_margin(&spread).is_err());
974 }
975
976 #[test]
977 fn shorthands_accept_css_wide_keywords_per_side() {
978 assert_eq!(
983 parse_layout_padding("auto").unwrap().top,
984 PixelValueWithAuto::Auto
985 );
986 assert_eq!(
987 parse_layout_padding("none").unwrap().top,
988 PixelValueWithAuto::None
989 );
990
991 let mixed = parse_layout_padding("initial 10px inherit auto").unwrap();
992 assert_eq!(mixed.top, PixelValueWithAuto::Initial);
993 assert_eq!(mixed.right, exact_px(10.0));
994 assert_eq!(mixed.bottom, PixelValueWithAuto::Inherit);
995 assert_eq!(mixed.left, PixelValueWithAuto::Auto);
996 }
997
998 #[test]
1001 fn arity_error_messages_name_the_right_property() {
1002 let pad_many = format!("{}", LayoutPaddingParseError::TooManyValues);
1005 let pad_few = format!("{}", LayoutPaddingParseError::TooFewValues);
1006 assert!(
1007 pad_many.contains("padding") && pad_many.contains("at most 4"),
1008 "{pad_many}"
1009 );
1010 assert!(pad_few.contains("padding"), "{pad_few}");
1011
1012 let margin_many = format!("{}", LayoutMarginParseError::TooManyValues);
1013 let margin_few = format!("{}", LayoutMarginParseError::TooFewValues);
1014 assert!(
1015 margin_many.contains("margin") && !margin_many.contains("padding"),
1016 "{margin_many}"
1017 );
1018 assert!(
1019 margin_few.contains("margin") && !margin_few.contains("padding"),
1020 "{margin_few}"
1021 );
1022
1023 assert_eq!(
1025 format!("{}", parse_layout_margin("1 2 3 4 5").unwrap_err()),
1026 margin_many
1027 );
1028 assert_eq!(
1029 format!("{}", parse_layout_padding("").unwrap_err()),
1030 pad_few
1031 );
1032 }
1033
1034 #[test]
1035 fn owned_and_shared_error_forms_round_trip() {
1036 assert_eq!(
1037 LayoutPaddingParseError::TooManyValues.to_contained(),
1038 LayoutPaddingParseErrorOwned::TooManyValues
1039 );
1040 assert_eq!(
1041 LayoutPaddingParseErrorOwned::TooFewValues.to_shared(),
1042 LayoutPaddingParseError::TooFewValues
1043 );
1044 assert_eq!(
1045 LayoutMarginParseError::TooFewValues.to_contained(),
1046 LayoutMarginParseErrorOwned::TooFewValues
1047 );
1048 assert_eq!(
1049 LayoutMarginParseErrorOwned::TooManyValues.to_shared(),
1050 LayoutMarginParseError::TooManyValues
1051 );
1052
1053 let owned = parse_layout_padding("1px oops").unwrap_err().to_contained();
1056 assert!(matches!(
1057 &owned,
1058 LayoutPaddingParseErrorOwned::PixelValueParseError(_)
1059 ));
1060 assert!(matches!(
1061 owned.to_shared(),
1062 LayoutPaddingParseError::PixelValueParseError(_)
1063 ));
1064
1065 let owned_margin = parse_layout_margin("1px oops").unwrap_err().to_contained();
1066 assert!(matches!(
1067 owned_margin.to_shared(),
1068 LayoutMarginParseError::PixelValueParseError(_)
1069 ));
1070 }
1071
1072 #[test]
1075 fn longhand_parsers_reject_keywords_and_empty_input() {
1076 for input in ["", " ", "auto", "none", "initial", "inherit", "oops", "10px 20px"] {
1079 assert_all_longhands_err!(input);
1080 }
1081 }
1082
1083 #[test]
1084 fn every_longhand_spacing_parser_accepts_a_minimal_value() {
1085 assert_eq!(parse_layout_padding_top("0").unwrap(), LayoutPaddingTop::px(0.0));
1086 assert_eq!(parse_layout_padding_right("1px").unwrap(), LayoutPaddingRight::px(1.0));
1087 assert_eq!(parse_layout_padding_bottom("2pt").unwrap(), LayoutPaddingBottom::pt(2.0));
1088 assert_eq!(parse_layout_padding_left("2em").unwrap(), LayoutPaddingLeft::em(2.0));
1089 assert_eq!(
1090 parse_layout_padding_inline_start("3px").unwrap(),
1091 LayoutPaddingInlineStart::px(3.0)
1092 );
1093 assert_eq!(
1094 parse_layout_padding_inline_end("4px").unwrap(),
1095 LayoutPaddingInlineEnd::px(4.0)
1096 );
1097 assert_eq!(parse_layout_margin_top("-5px").unwrap(), LayoutMarginTop::px(-5.0));
1098 assert_eq!(parse_layout_margin_right("6%").unwrap(), LayoutMarginRight::percent(6.0));
1099 assert_eq!(parse_layout_margin_bottom("7px").unwrap(), LayoutMarginBottom::px(7.0));
1100 assert_eq!(parse_layout_margin_left("8px").unwrap(), LayoutMarginLeft::px(8.0));
1101 assert_eq!(parse_layout_column_gap("20px").unwrap(), LayoutColumnGap::px(20.0));
1102 assert_eq!(parse_layout_row_gap("1.5em").unwrap(), LayoutRowGap::em(1.5));
1103 }
1104
1105 #[test]
1106 fn every_longhand_spacing_parser_round_trips_through_its_printed_form() {
1107 for input in [
1110 "0", "1px", "10.5px", "1.5em", "2rem", "-20pt", "50%", "0.125px", "3.25in", "12.75mm",
1111 "2.54cm", "0.5vmin", "4vmax", "8vw", "100vh",
1112 ] {
1113 assert_css_roundtrip!(parse_layout_padding_top, input);
1114 assert_css_roundtrip!(parse_layout_padding_right, input);
1115 assert_css_roundtrip!(parse_layout_padding_bottom, input);
1116 assert_css_roundtrip!(parse_layout_padding_left, input);
1117 assert_css_roundtrip!(parse_layout_padding_inline_start, input);
1118 assert_css_roundtrip!(parse_layout_padding_inline_end, input);
1119 assert_css_roundtrip!(parse_layout_margin_top, input);
1120 assert_css_roundtrip!(parse_layout_margin_right, input);
1121 assert_css_roundtrip!(parse_layout_margin_bottom, input);
1122 assert_css_roundtrip!(parse_layout_margin_left, input);
1123 assert_css_roundtrip!(parse_layout_column_gap, input);
1124 assert_css_roundtrip!(parse_layout_row_gap, input);
1125 }
1126 }
1127
1128 #[test]
1129 fn printed_css_matches_the_source_text_for_representable_values() {
1130 assert_eq!(
1131 parse_layout_padding_top("10px").unwrap().print_as_css_value(),
1132 "10px"
1133 );
1134 assert_eq!(
1135 parse_layout_column_gap("50%").unwrap().print_as_css_value(),
1136 "50%"
1137 );
1138 assert_eq!(
1139 parse_layout_margin_left("-2.5em").unwrap().print_as_css_value(),
1140 "-2.5em"
1141 );
1142 assert_eq!(LayoutRowGap::zero().print_as_css_value(), "0px");
1143 assert_eq!(
1145 parse_layout_padding_bottom("3").unwrap().print_as_css_value(),
1146 "3px"
1147 );
1148 }
1149
1150 #[test]
1153 fn const_and_runtime_constructors_agree() {
1154 assert_eq!(LayoutPaddingLeft::const_px(5), LayoutPaddingLeft::px(5.0));
1155 assert_eq!(LayoutPaddingLeft::const_em(2), LayoutPaddingLeft::em(2.0));
1156 assert_eq!(LayoutPaddingLeft::const_pt(-3), LayoutPaddingLeft::pt(-3.0));
1157 assert_eq!(
1158 LayoutPaddingLeft::const_percent(50),
1159 LayoutPaddingLeft::percent(50.0)
1160 );
1161 assert_eq!(
1162 LayoutColumnGap::const_from_metric(SizeMetric::Vh, 7),
1163 LayoutColumnGap::from_metric(SizeMetric::Vh, 7.0)
1164 );
1165 assert_eq!(
1166 LayoutColumnGap::const_in(1),
1167 LayoutColumnGap::from_metric(SizeMetric::In, 1.0)
1168 );
1169 assert_eq!(
1170 LayoutColumnGap::const_cm(2),
1171 LayoutColumnGap::from_metric(SizeMetric::Cm, 2.0)
1172 );
1173 assert_eq!(
1174 LayoutColumnGap::const_mm(3),
1175 LayoutColumnGap::from_metric(SizeMetric::Mm, 3.0)
1176 );
1177
1178 assert_eq!(
1180 LayoutRowGap::from_pixel_value(PixelValue::em(2.0)).inner,
1181 PixelValue::em(2.0)
1182 );
1183
1184 assert_eq!(LayoutPaddingBottom::zero(), LayoutPaddingBottom::default());
1185 assert_eq!(
1186 LayoutPaddingBottom::default().inner.metric,
1187 SizeMetric::Px,
1188 "the default spacing metric is px"
1189 );
1190 }
1191
1192 #[test]
1193 fn ordering_is_metric_major_not_physical_length() {
1194 assert!(LayoutPaddingTop::px(1000.0) < LayoutPaddingTop::pt(0.0));
1198 assert!(LayoutPaddingTop::pt(0.0) < LayoutPaddingTop::em(0.0));
1199
1200 assert!(LayoutPaddingTop::px(1.0) < LayoutPaddingTop::px(2.0));
1202 assert!(LayoutMarginLeft::const_px(-5) < LayoutMarginLeft::zero());
1203 }
1204
1205 #[test]
1206 fn equal_values_hash_equal_across_signed_zero_and_quantisation() {
1207 let pos = LayoutMarginTop::px(0.0);
1208 let neg = LayoutMarginTop::px(-0.0);
1209 assert_eq!(pos, neg, "signed zero must have one canonical encoding");
1210 assert_eq!(hash_of(&pos), hash_of(&neg));
1211 assert_eq!(pos, LayoutMarginTop::zero());
1212
1213 let a = LayoutMarginTop::px(1.0001);
1216 let b = LayoutMarginTop::px(1.0009);
1217 assert_eq!(a, b);
1218 assert_eq!(hash_of(&a), hash_of(&b));
1219
1220 assert_ne!(LayoutMarginTop::px(1.0), LayoutMarginTop::em(1.0));
1222 }
1223
1224 #[test]
1225 fn debug_renders_the_value_as_css() {
1226 assert_eq!(format!("{:?}", LayoutPaddingTop::px(10.0)), "10px");
1227 assert_eq!(format!("{:?}", LayoutColumnGap::percent(50.0)), "50%");
1228 assert_eq!(format!("{:?}", LayoutRowGap::zero()), "0px");
1229 }
1230
1231 #[test]
1232 fn interpolate_hits_its_endpoints_and_survives_nan_and_huge_t() {
1233 let a = LayoutRowGap::px(0.0);
1234 let b = LayoutRowGap::px(10.0);
1235 assert_eq!(a.interpolate(&b, 0.0), a);
1236 assert_eq!(a.interpolate(&b, 1.0), b);
1237 assert_eq!(a.interpolate(&b, 0.5), LayoutRowGap::px(5.0));
1238
1239 let nan = a.interpolate(&b, f32::NAN);
1242 assert_eq!(nan.inner.metric, SizeMetric::Px);
1243 assert_eq!(nan.inner.number.get(), 0.0);
1244
1245 for t in [1e30_f32, -1e30_f32, f32::INFINITY, f32::NEG_INFINITY] {
1247 let out = a.interpolate(&b, t);
1248 assert!(
1249 out.inner.number.get().is_finite(),
1250 "t = {t} produced a non-finite length"
1251 );
1252 }
1253
1254 let mixed = LayoutRowGap::px(0.0).interpolate(&LayoutRowGap::em(1.0), 1.0);
1256 assert_eq!(mixed.inner.metric, SizeMetric::Px);
1257 assert!(mixed.inner.number.get().is_finite());
1258 }
1259}