1use alloc::string::{String, ToString};
4use crate::corety::{AzString, OptionF32};
5
6use crate::props::formatter::PrintAsCssValue;
7
8#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
15#[repr(C)]
16pub enum LayoutOverflow {
17 Scroll,
19 Auto,
21 Hidden,
23 #[default]
26 Visible,
27 Clip,
29}
30
31impl LayoutOverflow {
32 #[must_use] pub const fn needs_scrollbar(&self, currently_overflowing: bool) -> bool {
41 match self {
42 Self::Scroll => true,
43 Self::Auto => currently_overflowing,
44 Self::Hidden | Self::Visible | Self::Clip => false,
45 }
46 }
47
48 #[must_use] pub const fn is_clipped(&self) -> bool {
54 matches!(
56 self,
57 Self::Hidden
58 | Self::Clip
59 | Self::Auto
60 | Self::Scroll
61 )
62 }
63
64 #[must_use] pub const fn is_scroll(&self) -> bool {
67 matches!(self, Self::Scroll)
68 }
69
70 #[must_use] pub fn is_overflow_visible(&self) -> bool {
73 *self == Self::Visible
74 }
75
76 #[must_use] pub fn is_overflow_hidden(&self) -> bool {
78 *self == Self::Hidden
79 }
80
81 #[must_use] pub const fn resolve_computed(self, other_axis: Self) -> Self {
86 let other_is_scrollable = !matches!(other_axis, Self::Visible | Self::Clip);
87 if other_is_scrollable {
88 match self {
89 Self::Visible => Self::Auto,
90 Self::Clip => Self::Hidden,
91 other => other,
92 }
93 } else {
94 self
95 }
96 }
97}
98
99impl PrintAsCssValue for LayoutOverflow {
100 fn print_as_css_value(&self) -> String {
101 String::from(match self {
102 Self::Scroll => "scroll",
103 Self::Auto => "auto",
104 Self::Hidden => "hidden",
105 Self::Visible => "visible",
106 Self::Clip => "clip",
107 })
108 }
109}
110
111#[derive(Clone, PartialEq, Eq)]
115pub enum LayoutOverflowParseError<'a> {
116 InvalidValue(&'a str),
118}
119
120impl_debug_as_display!(LayoutOverflowParseError<'a>);
121impl_display! { LayoutOverflowParseError<'a>, {
122 InvalidValue(val) => format!(
123 "Invalid overflow value: \"{}\". Expected 'scroll', 'auto', 'hidden', 'visible', or 'clip'.", val
124 ),
125}}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
129#[repr(C, u8)]
130pub enum LayoutOverflowParseErrorOwned {
131 InvalidValue(AzString),
132}
133
134impl LayoutOverflowParseError<'_> {
135 #[must_use] pub fn to_contained(&self) -> LayoutOverflowParseErrorOwned {
137 match self {
138 LayoutOverflowParseError::InvalidValue(s) => {
139 LayoutOverflowParseErrorOwned::InvalidValue((*s).to_string().into())
140 }
141 }
142 }
143}
144
145impl LayoutOverflowParseErrorOwned {
146 #[must_use] pub fn to_shared(&self) -> LayoutOverflowParseError<'_> {
148 match self {
149 Self::InvalidValue(s) => {
150 LayoutOverflowParseError::InvalidValue(s.as_str())
151 }
152 }
153 }
154}
155
156#[cfg(feature = "parser")]
157pub fn parse_layout_overflow(
162 input: &str,
163) -> Result<LayoutOverflow, LayoutOverflowParseError<'_>> {
164 let input_trimmed = input.trim();
165 match input_trimmed {
166 "scroll" => Ok(LayoutOverflow::Scroll),
167 "auto" | "overlay" => Ok(LayoutOverflow::Auto), "hidden" => Ok(LayoutOverflow::Hidden),
169 "visible" => Ok(LayoutOverflow::Visible),
170 "clip" => Ok(LayoutOverflow::Clip),
171 _ => Err(LayoutOverflowParseError::InvalidValue(input)),
172 }
173}
174
175#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
184#[repr(C)]
185pub enum StyleScrollbarGutter {
186 #[default]
188 Auto,
189 Stable,
191 StableBothEdges,
193}
194
195impl PrintAsCssValue for StyleScrollbarGutter {
196 fn print_as_css_value(&self) -> String {
197 String::from(match self {
198 Self::Auto => "auto",
199 Self::Stable => "stable",
200 Self::StableBothEdges => "stable both-edges",
201 })
202 }
203}
204
205#[derive(Clone, PartialEq, Eq)]
209pub enum StyleScrollbarGutterParseError<'a> {
210 InvalidValue(&'a str),
212}
213
214impl_debug_as_display!(StyleScrollbarGutterParseError<'a>);
215impl_display! { StyleScrollbarGutterParseError<'a>, {
216 InvalidValue(val) => format!(
217 "Invalid scrollbar-gutter value: \"{}\". Expected 'auto', 'stable', or 'stable both-edges'.", val
218 ),
219}}
220
221#[derive(Debug, Clone, PartialEq, Eq)]
223#[repr(C, u8)]
224pub enum StyleScrollbarGutterParseErrorOwned {
225 InvalidValue(AzString),
226}
227
228impl StyleScrollbarGutterParseError<'_> {
229 #[must_use] pub fn to_contained(&self) -> StyleScrollbarGutterParseErrorOwned {
231 match self {
232 StyleScrollbarGutterParseError::InvalidValue(s) => {
233 StyleScrollbarGutterParseErrorOwned::InvalidValue((*s).to_string().into())
234 }
235 }
236 }
237}
238
239impl StyleScrollbarGutterParseErrorOwned {
240 #[must_use] pub fn to_shared(&self) -> StyleScrollbarGutterParseError<'_> {
242 match self {
243 Self::InvalidValue(s) => {
244 StyleScrollbarGutterParseError::InvalidValue(s.as_str())
245 }
246 }
247 }
248}
249
250#[cfg(feature = "parser")]
251pub fn parse_style_scrollbar_gutter(
256 input: &str,
257) -> Result<StyleScrollbarGutter, StyleScrollbarGutterParseError<'_>> {
258 let input_trimmed = input.trim();
259 match input_trimmed {
260 "auto" => Ok(StyleScrollbarGutter::Auto),
261 "stable" => Ok(StyleScrollbarGutter::Stable),
262 "stable both-edges" => Ok(StyleScrollbarGutter::StableBothEdges),
263 _ => Err(StyleScrollbarGutterParseError::InvalidValue(input)),
264 }
265}
266
267#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
278#[repr(C)]
279pub enum StyleTextOverflow {
280 #[default]
282 Clip,
283 Ellipsis,
285}
286
287impl PrintAsCssValue for StyleTextOverflow {
288 fn print_as_css_value(&self) -> String {
289 String::from(match self {
290 Self::Clip => "clip",
291 Self::Ellipsis => "ellipsis",
292 })
293 }
294}
295
296#[derive(Clone, PartialEq, Eq)]
300pub enum StyleTextOverflowParseError<'a> {
301 InvalidValue(&'a str),
303}
304
305impl_debug_as_display!(StyleTextOverflowParseError<'a>);
306impl_display! { StyleTextOverflowParseError<'a>, {
307 InvalidValue(val) => format!(
308 "Invalid text-overflow value: \"{}\". Expected 'clip' or 'ellipsis'.", val
309 ),
310}}
311
312#[derive(Debug, Clone, PartialEq, Eq)]
314#[repr(C, u8)]
315pub enum StyleTextOverflowParseErrorOwned {
316 InvalidValue(AzString),
317}
318
319impl StyleTextOverflowParseError<'_> {
320 #[must_use] pub fn to_contained(&self) -> StyleTextOverflowParseErrorOwned {
322 match self {
323 StyleTextOverflowParseError::InvalidValue(s) => {
324 StyleTextOverflowParseErrorOwned::InvalidValue((*s).to_string().into())
325 }
326 }
327 }
328}
329
330impl StyleTextOverflowParseErrorOwned {
331 #[must_use] pub fn to_shared(&self) -> StyleTextOverflowParseError<'_> {
333 match self {
334 Self::InvalidValue(s) => {
335 StyleTextOverflowParseError::InvalidValue(s.as_str())
336 }
337 }
338 }
339}
340
341#[cfg(feature = "parser")]
342pub fn parse_style_text_overflow(
347 input: &str,
348) -> Result<StyleTextOverflow, StyleTextOverflowParseError<'_>> {
349 match input.trim() {
350 "clip" => Ok(StyleTextOverflow::Clip),
351 "ellipsis" => Ok(StyleTextOverflow::Ellipsis),
352 other => Err(StyleTextOverflowParseError::InvalidValue(other)),
353 }
354}
355
356#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
364#[repr(C)]
365pub enum VisualBox {
366 ContentBox,
368 #[default]
370 PaddingBox,
371 BorderBox,
373}
374
375impl PrintAsCssValue for VisualBox {
376 fn print_as_css_value(&self) -> String {
377 String::from(match self {
378 Self::ContentBox => "content-box",
379 Self::PaddingBox => "padding-box",
380 Self::BorderBox => "border-box",
381 })
382 }
383}
384
385#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
394#[repr(C)]
395pub struct StyleOverflowClipMargin {
396 pub clip_edge: VisualBox,
398 pub inner: crate::props::basic::pixel::PixelValue,
400}
401
402impl PrintAsCssValue for StyleOverflowClipMargin {
403 fn print_as_css_value(&self) -> String {
404 let edge = self.clip_edge.print_as_css_value();
405 let len = self.inner.print_as_css_value();
406 #[allow(clippy::float_cmp)] if self.inner.number.get() == 0.0 {
408 edge
409 } else if self.clip_edge == VisualBox::PaddingBox {
410 len
411 } else {
412 format!("{edge} {len}")
413 }
414 }
415}
416
417#[derive(Clone, PartialEq, Eq)]
419pub enum StyleOverflowClipMarginParseError<'a> {
420 InvalidValue(&'a str),
422}
423
424impl_debug_as_display!(StyleOverflowClipMarginParseError<'a>);
425impl_display! { StyleOverflowClipMarginParseError<'a>, {
426 InvalidValue(val) => format!("Invalid overflow-clip-margin value: \"{}\"", val),
427}}
428
429#[derive(Debug, Clone, PartialEq, Eq)]
431#[repr(C, u8)]
432pub enum StyleOverflowClipMarginParseErrorOwned {
433 InvalidValue(AzString),
434}
435
436impl StyleOverflowClipMarginParseError<'_> {
437 #[must_use] pub fn to_contained(&self) -> StyleOverflowClipMarginParseErrorOwned {
439 match self {
440 StyleOverflowClipMarginParseError::InvalidValue(s) => {
441 StyleOverflowClipMarginParseErrorOwned::InvalidValue((*s).to_string().into())
442 }
443 }
444 }
445}
446
447impl StyleOverflowClipMarginParseErrorOwned {
448 #[must_use] pub fn to_shared(&self) -> StyleOverflowClipMarginParseError<'_> {
450 match self {
451 Self::InvalidValue(s) => {
452 StyleOverflowClipMarginParseError::InvalidValue(s.as_str())
453 }
454 }
455 }
456}
457
458#[cfg(feature = "parser")]
459pub fn parse_style_overflow_clip_margin(
468 input: &str,
469) -> Result<StyleOverflowClipMargin, StyleOverflowClipMarginParseError<'_>> {
470 use crate::props::basic::pixel::parse_pixel_value;
471
472 let input_trimmed = input.trim();
473 let mut clip_edge = None;
474 let mut length = None;
475
476 for token in input_trimmed.split_whitespace() {
477 match token {
478 "content-box" if clip_edge.is_none() => clip_edge = Some(VisualBox::ContentBox),
479 "padding-box" if clip_edge.is_none() => clip_edge = Some(VisualBox::PaddingBox),
480 "border-box" if clip_edge.is_none() => clip_edge = Some(VisualBox::BorderBox),
481 _ if length.is_none() => {
482 match parse_pixel_value(token) {
483 Ok(pv) => length = Some(pv),
484 Err(_) => return Err(StyleOverflowClipMarginParseError::InvalidValue(input)),
485 }
486 }
487 _ => return Err(StyleOverflowClipMarginParseError::InvalidValue(input)),
488 }
489 }
490
491 if clip_edge.is_none() && length.is_none() {
492 return Err(StyleOverflowClipMarginParseError::InvalidValue(input));
493 }
494
495 Ok(StyleOverflowClipMargin {
496 clip_edge: clip_edge.unwrap_or_default(),
497 inner: length.unwrap_or_default(),
498 })
499}
500
501#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
514#[repr(C)]
515pub struct StyleClipRect {
516 pub top: OptionF32,
518 pub right: OptionF32,
520 pub bottom: OptionF32,
522 pub left: OptionF32,
524}
525
526impl StyleClipRect {
527 #[must_use] pub fn resolve(
532 &self,
533 used_width: f32,
534 used_height: f32,
535 padding_left: f32,
536 padding_right: f32,
537 padding_top: f32,
538 padding_bottom: f32,
539 border_left: f32,
540 border_right: f32,
541 border_top: f32,
542 border_bottom: f32,
543 ) -> (f32, f32, f32, f32) {
544 let top = self.top.into_option().unwrap_or(0.0);
545 let left = self.left.into_option().unwrap_or(0.0);
546 let bottom = self
547 .bottom
548 .into_option()
549 .unwrap_or(used_height + padding_top + padding_bottom + border_top + border_bottom);
550 let right = self
551 .right
552 .into_option()
553 .unwrap_or(used_width + padding_left + padding_right + border_left + border_right);
554 (top, right, bottom, left)
555 }
556}
557
558impl PrintAsCssValue for StyleClipRect {
559 fn print_as_css_value(&self) -> String {
560 fn fmt_edge(o: OptionF32) -> String {
561 o.into_option()
562 .map_or_else(|| String::from("auto"), |v| format!("{v}px"))
563 }
564 format!(
565 "rect({}, {}, {}, {})",
566 fmt_edge(self.top),
567 fmt_edge(self.right),
568 fmt_edge(self.bottom),
569 fmt_edge(self.left)
570 )
571 }
572}
573
574#[derive(Clone, PartialEq, Eq)]
578pub enum StyleClipRectParseError<'a> {
579 InvalidValue(&'a str),
581}
582
583impl_debug_as_display!(StyleClipRectParseError<'a>);
584impl_display! { StyleClipRectParseError<'a>, {
585 InvalidValue(val) => format!(
586 "Invalid clip value: \"{}\". Expected 'auto' or 'rect(<top>, <right>, <bottom>, <left>)'.", val
587 ),
588}}
589
590#[derive(Debug, Clone, PartialEq, Eq)]
592#[repr(C, u8)]
593pub enum StyleClipRectParseErrorOwned {
594 InvalidValue(AzString),
595}
596
597impl StyleClipRectParseError<'_> {
598 #[must_use] pub fn to_contained(&self) -> StyleClipRectParseErrorOwned {
600 match self {
601 StyleClipRectParseError::InvalidValue(s) => {
602 StyleClipRectParseErrorOwned::InvalidValue((*s).to_string().into())
603 }
604 }
605 }
606}
607
608impl StyleClipRectParseErrorOwned {
609 #[must_use] pub fn to_shared(&self) -> StyleClipRectParseError<'_> {
611 match self {
612 Self::InvalidValue(s) => {
613 StyleClipRectParseError::InvalidValue(s.as_str())
614 }
615 }
616 }
617}
618
619#[cfg(feature = "parser")]
620fn parse_clip_edge(token: &str) -> Result<OptionF32, StyleClipRectParseError<'_>> {
621 use crate::props::basic::pixel::parse_pixel_value;
622
623 let token = token.trim();
624 if token.eq_ignore_ascii_case("auto") {
625 return Ok(OptionF32::None);
626 }
627 let pv = parse_pixel_value(token)
628 .map_err(|_| StyleClipRectParseError::InvalidValue(token))?;
629 Ok(OptionF32::Some(pv.number.get()))
630}
631
632#[cfg(feature = "parser")]
633pub fn parse_clip_rect(input: &str) -> Result<StyleClipRect, StyleClipRectParseError<'_>> {
645 let trimmed = input.trim();
646
647 if trimmed.eq_ignore_ascii_case("auto") {
648 return Ok(StyleClipRect::default());
649 }
650
651 let inner = trimmed
652 .strip_prefix("rect(")
653 .or_else(|| trimmed.strip_prefix("RECT("))
654 .and_then(|s| s.strip_suffix(')'))
655 .ok_or(StyleClipRectParseError::InvalidValue(input))?;
656
657 let inner = inner.trim();
658 let parts: Vec<&str> = if inner.contains(',') {
659 inner.split(',').map(str::trim).collect()
660 } else {
661 inner.split_whitespace().collect()
662 };
663
664 if parts.len() != 4 {
665 return Err(StyleClipRectParseError::InvalidValue(input));
666 }
667
668 Ok(StyleClipRect {
669 top: parse_clip_edge(parts[0])?,
670 right: parse_clip_edge(parts[1])?,
671 bottom: parse_clip_edge(parts[2])?,
672 left: parse_clip_edge(parts[3])?,
673 })
674}
675
676#[cfg(all(test, feature = "parser"))]
677mod tests {
678 use super::*;
679
680 #[test]
681 fn test_parse_layout_overflow_valid() {
682 assert_eq!(
683 parse_layout_overflow("visible").unwrap(),
684 LayoutOverflow::Visible
685 );
686 assert_eq!(
687 parse_layout_overflow("hidden").unwrap(),
688 LayoutOverflow::Hidden
689 );
690 assert_eq!(parse_layout_overflow("clip").unwrap(), LayoutOverflow::Clip);
691 assert_eq!(
692 parse_layout_overflow("scroll").unwrap(),
693 LayoutOverflow::Scroll
694 );
695 assert_eq!(parse_layout_overflow("auto").unwrap(), LayoutOverflow::Auto);
696 }
697
698 #[test]
699 fn test_parse_style_text_overflow_valid() {
700 assert_eq!(
701 parse_style_text_overflow("clip").unwrap(),
702 StyleTextOverflow::Clip
703 );
704 assert_eq!(
705 parse_style_text_overflow("ellipsis").unwrap(),
706 StyleTextOverflow::Ellipsis
707 );
708 assert_eq!(
710 parse_style_text_overflow(" ellipsis ").unwrap(),
711 StyleTextOverflow::Ellipsis
712 );
713 assert_eq!(StyleTextOverflow::default(), StyleTextOverflow::Clip);
715 }
716
717 #[test]
718 fn test_parse_style_text_overflow_invalid() {
719 assert!(parse_style_text_overflow("none").is_err());
720 assert!(parse_style_text_overflow("").is_err());
721 assert!(parse_style_text_overflow("fade").is_err());
722 let msg = format!(
724 "{}",
725 StyleTextOverflowParseError::InvalidValue("fade")
726 );
727 assert!(msg.contains("text-overflow") && msg.contains("fade"), "{msg}");
728 let e = parse_style_text_overflow("fade").unwrap_err();
730 assert_eq!(e.to_contained().to_shared(), e);
731 }
732
733 #[test]
734 fn test_style_text_overflow_print_round_trip() {
735 for v in [StyleTextOverflow::Clip, StyleTextOverflow::Ellipsis] {
736 let printed = v.print_as_css_value();
737 assert_eq!(parse_style_text_overflow(&printed).unwrap(), v);
738 }
739 }
740
741 #[test]
742 fn test_parse_layout_overflow_whitespace() {
743 assert_eq!(
744 parse_layout_overflow(" scroll ").unwrap(),
745 LayoutOverflow::Scroll
746 );
747 }
748
749 #[test]
750 fn test_parse_layout_overflow_invalid() {
751 assert!(parse_layout_overflow("none").is_err());
752 assert!(parse_layout_overflow("").is_err());
753 assert!(parse_layout_overflow("auto scroll").is_err());
754 assert!(parse_layout_overflow("hidden-x").is_err());
755 }
756
757 #[test]
758 fn test_needs_scrollbar() {
759 assert!(LayoutOverflow::Scroll.needs_scrollbar(false));
760 assert!(LayoutOverflow::Scroll.needs_scrollbar(true));
761 assert!(LayoutOverflow::Auto.needs_scrollbar(true));
762 assert!(!LayoutOverflow::Auto.needs_scrollbar(false));
763 assert!(!LayoutOverflow::Hidden.needs_scrollbar(true));
764 assert!(!LayoutOverflow::Visible.needs_scrollbar(true));
765 assert!(!LayoutOverflow::Clip.needs_scrollbar(true));
766 }
767
768 #[test]
769 fn test_parse_clip_rect_auto_keyword() {
770 let r = parse_clip_rect("auto").unwrap();
771 assert_eq!(r.top, OptionF32::None);
772 assert_eq!(r.right, OptionF32::None);
773 assert_eq!(r.bottom, OptionF32::None);
774 assert_eq!(r.left, OptionF32::None);
775 }
776
777 #[test]
778 fn test_parse_clip_rect_all_auto_in_rect() {
779 let r = parse_clip_rect("rect(auto, auto, auto, auto)").unwrap();
780 assert_eq!(r.top, OptionF32::None);
781 assert_eq!(r.right, OptionF32::None);
782 assert_eq!(r.bottom, OptionF32::None);
783 assert_eq!(r.left, OptionF32::None);
784 }
785
786 #[test]
787 fn test_parse_clip_rect_mixed_auto_and_lengths() {
788 let r = parse_clip_rect("rect(10px, auto, 30px, auto)").unwrap();
789 assert_eq!(r.top, OptionF32::Some(10.0));
790 assert_eq!(r.right, OptionF32::None);
791 assert_eq!(r.bottom, OptionF32::Some(30.0));
792 assert_eq!(r.left, OptionF32::None);
793 }
794
795 #[test]
796 fn test_parse_clip_rect_negative_lengths() {
797 let r = parse_clip_rect("rect(-5px, 0px, -10px, 0px)").unwrap();
798 assert_eq!(r.top, OptionF32::Some(-5.0));
799 assert_eq!(r.right, OptionF32::Some(0.0));
800 assert_eq!(r.bottom, OptionF32::Some(-10.0));
801 assert_eq!(r.left, OptionF32::Some(0.0));
802 }
803
804 #[test]
805 fn test_parse_clip_rect_legacy_space_separated() {
806 let r = parse_clip_rect("rect(1px 2px 3px 4px)").unwrap();
808 assert_eq!(r.top, OptionF32::Some(1.0));
809 assert_eq!(r.right, OptionF32::Some(2.0));
810 assert_eq!(r.bottom, OptionF32::Some(3.0));
811 assert_eq!(r.left, OptionF32::Some(4.0));
812 }
813
814 #[test]
815 fn test_parse_clip_rect_malformed() {
816 assert!(parse_clip_rect("").is_err());
817 assert!(parse_clip_rect("none").is_err());
818 assert!(parse_clip_rect("rect(10px, 20px, 30px)").is_err());
820 assert!(parse_clip_rect("rect(10px, 20px, 30px, 40px").is_err());
822 assert!(parse_clip_rect("rect(10px, abc, 30px, 40px)").is_err());
824 }
825}
826
827#[cfg(all(test, feature = "parser"))]
828mod autotest_generated {
829 use crate::props::basic::pixel::PixelValue;
830 use crate::props::basic::length::SizeMetric;
831
832 use super::*;
833
834 const ALL_OVERFLOW: [LayoutOverflow; 5] = [
840 LayoutOverflow::Scroll,
841 LayoutOverflow::Auto,
842 LayoutOverflow::Hidden,
843 LayoutOverflow::Visible,
844 LayoutOverflow::Clip,
845 ];
846
847 const fn overflow_variant_index(o: LayoutOverflow) -> usize {
848 match o {
849 LayoutOverflow::Scroll => 0,
850 LayoutOverflow::Auto => 1,
851 LayoutOverflow::Hidden => 2,
852 LayoutOverflow::Visible => 3,
853 LayoutOverflow::Clip => 4,
854 }
855 }
856
857 const ALL_GUTTER: [StyleScrollbarGutter; 3] = [
858 StyleScrollbarGutter::Auto,
859 StyleScrollbarGutter::Stable,
860 StyleScrollbarGutter::StableBothEdges,
861 ];
862
863 const fn gutter_variant_index(g: StyleScrollbarGutter) -> usize {
864 match g {
865 StyleScrollbarGutter::Auto => 0,
866 StyleScrollbarGutter::Stable => 1,
867 StyleScrollbarGutter::StableBothEdges => 2,
868 }
869 }
870
871 const ALL_VISUAL_BOX: [VisualBox; 3] = [
872 VisualBox::ContentBox,
873 VisualBox::PaddingBox,
874 VisualBox::BorderBox,
875 ];
876
877 const fn visual_box_variant_index(v: VisualBox) -> usize {
878 match v {
879 VisualBox::ContentBox => 0,
880 VisualBox::PaddingBox => 1,
881 VisualBox::BorderBox => 2,
882 }
883 }
884
885 const fn is_scrollable(o: LayoutOverflow) -> bool {
888 !matches!(o, LayoutOverflow::Visible | LayoutOverflow::Clip)
889 }
890
891 #[test]
892 fn variant_tables_cover_every_variant_exactly_once() {
893 for (i, o) in ALL_OVERFLOW.iter().enumerate() {
894 assert_eq!(overflow_variant_index(*o), i);
895 }
896 for (i, g) in ALL_GUTTER.iter().enumerate() {
897 assert_eq!(gutter_variant_index(*g), i);
898 }
899 for (i, v) in ALL_VISUAL_BOX.iter().enumerate() {
900 assert_eq!(visual_box_variant_index(*v), i);
901 }
902 }
903
904 #[test]
909 fn needs_scrollbar_truth_table_is_monotone_in_currently_overflowing() {
910 for o in ALL_OVERFLOW {
911 let idle = o.needs_scrollbar(false);
912 let overflowing = o.needs_scrollbar(true);
913
914 assert!(
917 !idle || overflowing,
918 "{o:?} shows a scrollbar when idle but hides it when overflowing"
919 );
920
921 let (expect_idle, expect_overflowing) = match o {
924 LayoutOverflow::Scroll => (true, true),
925 LayoutOverflow::Auto => (false, true),
926 LayoutOverflow::Hidden | LayoutOverflow::Visible | LayoutOverflow::Clip => {
927 (false, false)
928 }
929 };
930 assert_eq!(idle, expect_idle, "needs_scrollbar(false) wrong for {o:?}");
931 assert_eq!(
932 overflowing, expect_overflowing,
933 "needs_scrollbar(true) wrong for {o:?}"
934 );
935
936 assert!(!overflowing || o.is_clipped());
938 }
939 }
940
941 #[test]
942 fn is_clipped_is_exactly_the_negation_of_is_overflow_visible() {
943 for o in ALL_OVERFLOW {
944 assert_eq!(
945 o.is_clipped(),
946 !o.is_overflow_visible(),
947 "is_clipped/is_overflow_visible disagree for {o:?}"
948 );
949 assert_eq!(o.is_clipped(), o.is_clipped());
951 }
952 assert!(!LayoutOverflow::Visible.is_clipped());
953 assert!(LayoutOverflow::Hidden.is_clipped());
954 }
955
956 #[test]
957 fn is_scroll_and_is_overflow_hidden_match_exactly_one_variant_each() {
958 let scrolls: Vec<LayoutOverflow> =
959 ALL_OVERFLOW.into_iter().filter(LayoutOverflow::is_scroll).collect();
960 assert_eq!(scrolls, vec![LayoutOverflow::Scroll]);
961
962 let hiddens: Vec<LayoutOverflow> = ALL_OVERFLOW
963 .into_iter()
964 .filter(LayoutOverflow::is_overflow_hidden)
965 .collect();
966 assert_eq!(hiddens, vec![LayoutOverflow::Hidden]);
967
968 assert!(!LayoutOverflow::Auto.is_scroll());
970 assert!(LayoutOverflow::Auto.needs_scrollbar(true));
971 }
972
973 #[test]
974 fn default_overflow_is_visible_and_neither_clips_nor_scrolls() {
975 let d = LayoutOverflow::default();
976 assert_eq!(d, LayoutOverflow::Visible);
977 assert!(d.is_overflow_visible());
978 assert!(!d.is_clipped());
979 assert!(!d.is_scroll());
980 assert!(!d.is_overflow_hidden());
981 assert!(!d.needs_scrollbar(false));
982 assert!(!d.needs_scrollbar(true));
983 }
984
985 #[test]
990 fn resolve_computed_is_identity_when_the_other_axis_is_not_scrollable() {
991 for other in [LayoutOverflow::Visible, LayoutOverflow::Clip] {
992 for o in ALL_OVERFLOW {
993 assert_eq!(
994 o.resolve_computed(other),
995 o,
996 "{o:?} must be untouched when the other axis is {other:?}"
997 );
998 }
999 }
1000 }
1001
1002 #[test]
1003 fn resolve_computed_promotes_visible_to_auto_and_clip_to_hidden() {
1004 for other in [
1005 LayoutOverflow::Scroll,
1006 LayoutOverflow::Auto,
1007 LayoutOverflow::Hidden,
1008 ] {
1009 assert_eq!(
1010 LayoutOverflow::Visible.resolve_computed(other),
1011 LayoutOverflow::Auto
1012 );
1013 assert_eq!(
1014 LayoutOverflow::Clip.resolve_computed(other),
1015 LayoutOverflow::Hidden
1016 );
1017 for o in [
1019 LayoutOverflow::Scroll,
1020 LayoutOverflow::Auto,
1021 LayoutOverflow::Hidden,
1022 ] {
1023 assert_eq!(o.resolve_computed(other), o);
1024 }
1025 }
1026 }
1027
1028 #[test]
1029 fn resolve_computed_is_idempotent_and_never_removes_clipping() {
1030 for o in ALL_OVERFLOW {
1031 for other in ALL_OVERFLOW {
1032 let once = o.resolve_computed(other);
1033 assert_eq!(
1034 once.resolve_computed(other),
1035 once,
1036 "resolve_computed not idempotent for ({o:?}, {other:?})"
1037 );
1038 assert!(
1040 !o.is_clipped() || once.is_clipped(),
1041 "({o:?}, {other:?}) lost clipping"
1042 );
1043 assert!(!is_scrollable(o) || is_scrollable(once));
1045 }
1046 }
1047 }
1048
1049 #[test]
1050 fn resolve_computed_leaves_both_axes_consistently_scrollable() {
1051 for x in ALL_OVERFLOW {
1055 for y in ALL_OVERFLOW {
1056 let rx = x.resolve_computed(y);
1057 let ry = y.resolve_computed(x);
1058 assert_eq!(
1059 is_scrollable(rx),
1060 is_scrollable(ry),
1061 "({x:?}, {y:?}) resolved to the mismatched pair ({rx:?}, {ry:?})"
1062 );
1063 }
1064 }
1065
1066 assert_eq!(
1068 LayoutOverflow::Visible.resolve_computed(LayoutOverflow::Scroll),
1069 LayoutOverflow::Auto
1070 );
1071 assert_eq!(
1072 LayoutOverflow::Scroll.resolve_computed(LayoutOverflow::Visible),
1073 LayoutOverflow::Scroll
1074 );
1075 assert_eq!(
1077 LayoutOverflow::Visible.resolve_computed(LayoutOverflow::Clip),
1078 LayoutOverflow::Visible
1079 );
1080 assert_eq!(
1081 LayoutOverflow::Clip.resolve_computed(LayoutOverflow::Visible),
1082 LayoutOverflow::Clip
1083 );
1084 }
1085
1086 #[test]
1091 fn layout_overflow_round_trips_through_print_as_css_value() {
1092 for o in ALL_OVERFLOW {
1093 let printed = o.print_as_css_value();
1094 assert_eq!(
1095 parse_layout_overflow(&printed).unwrap(),
1096 o,
1097 "{o:?} printed as {printed:?} did not round-trip"
1098 );
1099 assert!(!printed.is_empty());
1101 assert!(!printed.contains(char::is_whitespace));
1102 assert_eq!(printed, printed.to_lowercase());
1103 }
1104 }
1105
1106 #[test]
1107 fn parse_layout_overflow_treats_overlay_as_a_one_way_alias_of_auto() {
1108 assert_eq!(parse_layout_overflow("overlay").unwrap(), LayoutOverflow::Auto);
1111 let normalised = parse_layout_overflow("overlay").unwrap().print_as_css_value();
1112 assert_eq!(normalised, "auto");
1113 assert_eq!(
1114 parse_layout_overflow(&normalised).unwrap(),
1115 LayoutOverflow::Auto
1116 );
1117 for o in ALL_OVERFLOW {
1118 assert_ne!(o.print_as_css_value(), "overlay");
1119 }
1120 }
1121
1122 #[test]
1123 fn parse_layout_overflow_rejects_empty_and_whitespace_only_input() {
1124 for input in ["", " ", " ", "\t", "\n", "\r\n", "\t \n \r", "\u{00A0}"] {
1125 assert!(
1126 parse_layout_overflow(input).is_err(),
1127 "{input:?} must not parse"
1128 );
1129 }
1130 }
1131
1132 #[test]
1133 fn parse_layout_overflow_error_carries_the_untrimmed_input() {
1134 let err = parse_layout_overflow(" bogus ").unwrap_err();
1136 assert_eq!(err, LayoutOverflowParseError::InvalidValue(" bogus "));
1137 let msg = format!("{err}");
1138 assert!(msg.contains("bogus"), "{msg}");
1139 assert!(msg.contains("scroll"), "error should list the valid keywords: {msg}");
1140 }
1141
1142 #[test]
1143 fn parse_layout_overflow_is_ascii_case_sensitive() {
1144 for input in ["SCROLL", "Scroll", "sCrOlL", "AUTO", "Hidden", "VISIBLE", "Clip"] {
1149 assert!(
1150 parse_layout_overflow(input).is_err(),
1151 "{input:?} unexpectedly parsed"
1152 );
1153 }
1154 assert_eq!(parse_layout_overflow("scroll").unwrap(), LayoutOverflow::Scroll);
1155 }
1156
1157 #[test]
1158 fn parse_layout_overflow_trims_unicode_whitespace_but_not_zero_width_chars() {
1159 assert_eq!(
1162 parse_layout_overflow("\u{00A0}scroll\u{00A0}").unwrap(),
1163 LayoutOverflow::Scroll
1164 );
1165 assert_eq!(
1166 parse_layout_overflow("\u{3000}auto").unwrap(),
1167 LayoutOverflow::Auto
1168 );
1169 assert!(parse_layout_overflow("\u{200B}scroll").is_err());
1171 assert!(parse_layout_overflow("scroll\u{FEFF}").is_err());
1172 }
1173
1174 #[test]
1175 fn parse_layout_overflow_rejects_garbage_unicode_and_boundary_numbers() {
1176 for input in [
1177 "none",
1178 "hidden-x",
1179 "auto scroll",
1180 "scroll;",
1181 "scroll garbage",
1182 "visible !important",
1183 "\0",
1184 "scroll\0",
1185 "!@#$%^&*()",
1186 "\u{1F600}",
1187 "scroll\u{1F600}",
1188 "e\u{0301}",
1189 "scroll",
1190 "скролл",
1191 "0",
1192 "-0",
1193 "0.0",
1194 "NaN",
1195 "nan",
1196 "inf",
1197 "-inf",
1198 "infinity",
1199 "9223372036854775807",
1200 "-9223372036854775808",
1201 "1e400",
1202 "1e-400",
1203 ] {
1204 assert!(
1205 parse_layout_overflow(input).is_err(),
1206 "{input:?} unexpectedly parsed"
1207 );
1208 }
1209 }
1210
1211 #[test]
1212 fn parse_layout_overflow_survives_extremely_long_and_deeply_nested_input() {
1213 let long = "scroll".repeat(200_000);
1214 assert!(parse_layout_overflow(&long).is_err());
1215
1216 let junk = "a".repeat(1_000_000);
1217 assert!(parse_layout_overflow(&junk).is_err());
1218
1219 let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
1220 assert!(parse_layout_overflow(&nested).is_err());
1221
1222 let padded = format!("{}scroll{}", " ".repeat(500_000), " ".repeat(500_000));
1224 assert_eq!(parse_layout_overflow(&padded).unwrap(), LayoutOverflow::Scroll);
1225 }
1226
1227 #[test]
1232 fn scrollbar_gutter_round_trips_through_print_as_css_value() {
1233 for g in ALL_GUTTER {
1234 let printed = g.print_as_css_value();
1235 assert_eq!(
1236 parse_style_scrollbar_gutter(&printed).unwrap(),
1237 g,
1238 "{g:?} printed as {printed:?} did not round-trip"
1239 );
1240 }
1241 assert_eq!(
1242 StyleScrollbarGutter::StableBothEdges.print_as_css_value(),
1243 "stable both-edges"
1244 );
1245 assert_eq!(StyleScrollbarGutter::default(), StyleScrollbarGutter::Auto);
1246 }
1247
1248 #[test]
1249 fn parse_style_scrollbar_gutter_matches_the_keyword_string_verbatim() {
1250 assert_eq!(
1255 parse_style_scrollbar_gutter("stable both-edges").unwrap(),
1256 StyleScrollbarGutter::StableBothEdges
1257 );
1258 for rejected in [
1259 "stable both-edges", "stable\tboth-edges",
1261 "stable\nboth-edges",
1262 "both-edges stable", "both-edges",
1264 "STABLE",
1265 "Stable Both-Edges",
1266 "stable both-edges stable",
1267 ] {
1268 assert!(
1269 parse_style_scrollbar_gutter(rejected).is_err(),
1270 "{rejected:?} unexpectedly parsed"
1271 );
1272 }
1273 assert_eq!(
1275 parse_style_scrollbar_gutter(" stable both-edges \n").unwrap(),
1276 StyleScrollbarGutter::StableBothEdges
1277 );
1278 }
1279
1280 #[test]
1281 fn parse_style_scrollbar_gutter_rejects_empty_garbage_unicode_and_numbers() {
1282 for input in [
1283 "", " ", "\t\n", "none", "auto stable", "auto;", "stable;", "0", "-0", "NaN", "inf",
1284 "9223372036854775807", "\u{1F600}", "stable", "stable\0",
1285 ] {
1286 assert!(
1287 parse_style_scrollbar_gutter(input).is_err(),
1288 "{input:?} unexpectedly parsed"
1289 );
1290 }
1291 let err = parse_style_scrollbar_gutter(" nope ").unwrap_err();
1292 assert_eq!(
1293 err,
1294 StyleScrollbarGutterParseError::InvalidValue(" nope ")
1295 );
1296 assert!(format!("{err}").contains("scrollbar-gutter"));
1297 }
1298
1299 #[test]
1300 fn parse_style_scrollbar_gutter_survives_long_and_nested_input() {
1301 let long = "stable ".repeat(200_000);
1302 assert!(parse_style_scrollbar_gutter(&long).is_err());
1303 let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
1304 assert!(parse_style_scrollbar_gutter(&nested).is_err());
1305 }
1306
1307 #[test]
1312 fn parse_style_overflow_clip_margin_accepts_either_component_in_either_order() {
1313 let only_box = parse_style_overflow_clip_margin("content-box").unwrap();
1315 assert_eq!(only_box.clip_edge, VisualBox::ContentBox);
1316 assert_eq!(only_box.inner, PixelValue::default());
1317
1318 let only_len = parse_style_overflow_clip_margin("20px").unwrap();
1320 assert_eq!(only_len.clip_edge, VisualBox::PaddingBox);
1321 assert_eq!(only_len.inner, PixelValue::const_px(20));
1322
1323 let a = parse_style_overflow_clip_margin("border-box 10px").unwrap();
1325 let b = parse_style_overflow_clip_margin("10px border-box").unwrap();
1326 assert_eq!(a, b);
1327 assert_eq!(a.clip_edge, VisualBox::BorderBox);
1328 assert_eq!(a.inner, PixelValue::const_px(10));
1329
1330 let c = parse_style_overflow_clip_margin(" border-box \t\n 10px ").unwrap();
1332 assert_eq!(c, a);
1333
1334 assert_eq!(VisualBox::default(), VisualBox::PaddingBox);
1335 }
1336
1337 #[test]
1338 fn parse_style_overflow_clip_margin_rejects_empty_duplicates_and_garbage() {
1339 for input in [
1340 "",
1341 " ",
1342 "\t\n",
1343 "content-box content-box", "10px 20px", "content-box 10px 20px",
1346 "content-box padding-box",
1347 "content-box 10px border-box",
1348 "none",
1349 "auto",
1350 "margin-box",
1351 "10px;",
1352 "10 px extra",
1353 "px",
1354 "\u{1F600}",
1355 "10\u{1F600}",
1356 "content-box",
1357 "content_box",
1358 "CONTENT-BOX",
1359 ] {
1360 assert!(
1361 parse_style_overflow_clip_margin(input).is_err(),
1362 "{input:?} unexpectedly parsed"
1363 );
1364 }
1365 let err = parse_style_overflow_clip_margin(" nope ").unwrap_err();
1366 assert_eq!(
1367 err,
1368 StyleOverflowClipMarginParseError::InvalidValue(" nope ")
1369 );
1370 assert!(format!("{err}").contains("overflow-clip-margin"));
1371 }
1372
1373 #[test]
1374 fn parse_style_overflow_clip_margin_accepts_out_of_range_lengths() {
1375 let neg = parse_style_overflow_clip_margin("-5px").unwrap();
1379 assert!(neg.inner.number.get() < 0.0);
1380
1381 let pct = parse_style_overflow_clip_margin("50%").unwrap();
1382 assert_eq!(pct.inner.metric, SizeMetric::Percent);
1383 assert_eq!(pct.inner.number.get(), 50.0);
1384
1385 let unitless = parse_style_overflow_clip_margin("7").unwrap();
1387 assert_eq!(unitless.inner.metric, SizeMetric::Px);
1388 assert_eq!(unitless.inner.number.get(), 7.0);
1389 }
1390
1391 #[test]
1392 fn parse_style_overflow_clip_margin_saturates_nan_and_infinity() {
1393 let nan = parse_style_overflow_clip_margin("NaN").unwrap();
1398 assert!(!nan.inner.number.get().is_nan());
1399 assert_eq!(nan.inner.number.get(), 0.0);
1400
1401 let pos_inf = parse_style_overflow_clip_margin("inf").unwrap();
1402 assert!(pos_inf.inner.number.get().is_finite());
1403 assert!(pos_inf.inner.number.get() > 0.0);
1404
1405 let neg_inf = parse_style_overflow_clip_margin("-inf").unwrap();
1406 assert!(neg_inf.inner.number.get().is_finite());
1407 assert!(neg_inf.inner.number.get() < 0.0);
1408
1409 let huge = format!("{}px", "9".repeat(4096));
1412 let huge = parse_style_overflow_clip_margin(&huge).unwrap();
1413 assert!(huge.inner.number.get().is_finite());
1414
1415 let tiny = parse_style_overflow_clip_margin("0.0001px").unwrap();
1417 assert_eq!(tiny.inner.number.get(), 0.0);
1418 }
1419
1420 #[test]
1421 fn parse_style_overflow_clip_margin_survives_long_and_nested_input() {
1422 let long_token = format!("{}px", "a".repeat(1_000_000));
1423 assert!(parse_style_overflow_clip_margin(&long_token).is_err());
1424
1425 let many_tokens = "content-box ".repeat(100_000);
1426 assert!(parse_style_overflow_clip_margin(&many_tokens).is_err());
1427
1428 let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
1429 assert!(parse_style_overflow_clip_margin(&nested).is_err());
1430 }
1431
1432 #[test]
1433 fn overflow_clip_margin_round_trips_through_print_as_css_value() {
1434 let lengths = [
1435 PixelValue::const_px(12),
1436 PixelValue::px(1.5),
1437 PixelValue::const_em(2),
1438 PixelValue::const_percent(50),
1439 PixelValue::px(-3.25),
1440 ];
1441 for edge in ALL_VISUAL_BOX {
1442 for inner in lengths {
1443 let original = StyleOverflowClipMargin {
1444 clip_edge: edge,
1445 inner,
1446 };
1447 let printed = original.print_as_css_value();
1448 let reparsed = parse_style_overflow_clip_margin(&printed).unwrap_or_else(|e| {
1449 panic!("{original:?} printed as {printed:?} but failed to reparse: {e}")
1450 });
1451 assert_eq!(reparsed, original, "round-trip broke via {printed:?}");
1452 }
1453 }
1454 }
1455
1456 #[test]
1457 fn overflow_clip_margin_zero_length_prints_only_the_box_and_forgets_the_unit() {
1458 let zero_em = StyleOverflowClipMargin {
1463 clip_edge: VisualBox::ContentBox,
1464 inner: PixelValue::const_em(0),
1465 };
1466 assert_eq!(zero_em.print_as_css_value(), "content-box");
1467 let back = parse_style_overflow_clip_margin(&zero_em.print_as_css_value()).unwrap();
1468 assert_eq!(back.clip_edge, VisualBox::ContentBox);
1469 assert_eq!(back.inner.number.get(), 0.0);
1470 assert_eq!(back.inner.metric, SizeMetric::Px);
1471 assert_ne!(back, zero_em);
1472
1473 let default = StyleOverflowClipMargin::default();
1475 assert_eq!(default.print_as_css_value(), "padding-box");
1476 assert_eq!(
1477 parse_style_overflow_clip_margin(&default.print_as_css_value()).unwrap(),
1478 default
1479 );
1480
1481 let padding_len = StyleOverflowClipMargin {
1483 clip_edge: VisualBox::PaddingBox,
1484 inner: PixelValue::const_px(4),
1485 };
1486 assert_eq!(padding_len.print_as_css_value(), "4px");
1487 }
1488
1489 #[test]
1490 fn visual_box_round_trips_through_the_clip_margin_parser() {
1491 for v in ALL_VISUAL_BOX {
1492 let printed = v.print_as_css_value();
1493 let parsed = parse_style_overflow_clip_margin(&printed).unwrap();
1494 assert_eq!(parsed.clip_edge, v, "{printed:?} did not round-trip");
1495 }
1496 }
1497
1498 #[test]
1503 fn parse_clip_edge_auto_is_ascii_case_insensitive_and_trimmed() {
1504 for input in ["auto", "AUTO", "Auto", "aUtO", " auto ", "\tauto\n"] {
1505 assert_eq!(
1506 parse_clip_edge(input).unwrap(),
1507 OptionF32::None,
1508 "{input:?} should be auto"
1509 );
1510 }
1511 assert!(parse_clip_edge("auto5").is_err());
1513 assert!(parse_clip_edge("autopx").is_err());
1514 assert!(parse_clip_edge("auto auto").is_err());
1515 }
1516
1517 #[test]
1518 fn parse_clip_edge_silently_discards_the_unit() {
1519 for input in ["5px", "5em", "5rem", "5pt", "5in", "5cm", "5mm", "5vw", "5vh", "5%"] {
1523 assert_eq!(
1524 parse_clip_edge(input).unwrap(),
1525 OptionF32::Some(5.0),
1526 "{input:?} did not collapse to a bare 5.0"
1527 );
1528 }
1529 assert_eq!(parse_clip_edge("5").unwrap(), OptionF32::Some(5.0));
1531 assert_eq!(parse_clip_edge("5 px").unwrap(), OptionF32::Some(5.0));
1533 }
1534
1535 #[test]
1536 fn parse_clip_edge_quantises_to_thousandths_and_normalises_negative_zero() {
1537 assert_eq!(parse_clip_edge("0.001px").unwrap(), OptionF32::Some(0.001));
1540 assert_eq!(parse_clip_edge("0.0001px").unwrap(), OptionF32::Some(0.0));
1541 assert_eq!(parse_clip_edge("-0.0009px").unwrap(), OptionF32::Some(0.0));
1542 assert_eq!(parse_clip_edge("1.9999px").unwrap(), OptionF32::Some(1.999));
1543
1544 let minus_zero = parse_clip_edge("-0px").unwrap().into_option().unwrap();
1546 assert_eq!(minus_zero, 0.0);
1547 assert!(minus_zero.is_sign_positive());
1548
1549 assert_eq!(parse_clip_edge("-10px").unwrap(), OptionF32::Some(-10.0));
1551 }
1552
1553 #[test]
1554 fn parse_clip_edge_saturates_nan_and_infinity_to_finite_values() {
1555 let nan = parse_clip_edge("NaN").unwrap().into_option().unwrap();
1556 assert!(!nan.is_nan(), "NaN must not survive into a clip edge");
1557 assert_eq!(nan, 0.0);
1558
1559 let pos_inf = parse_clip_edge("inf").unwrap().into_option().unwrap();
1560 assert!(pos_inf.is_finite());
1561 assert!(pos_inf > 0.0);
1562
1563 let neg_inf = parse_clip_edge("-infinity").unwrap().into_option().unwrap();
1564 assert!(neg_inf.is_finite());
1565 assert!(neg_inf < 0.0);
1566
1567 let huge = format!("{}px", "9".repeat(4096));
1568 let huge = parse_clip_edge(&huge).unwrap().into_option().unwrap();
1569 assert!(huge.is_finite());
1570 }
1571
1572 #[test]
1573 fn parse_clip_edge_rejects_empty_bare_units_and_garbage() {
1574 for input in [
1575 "",
1576 " ",
1577 "\t\n",
1578 "px",
1579 "em",
1580 "%",
1581 "abc",
1582 "10px;",
1583 "10px 20px",
1584 "(10px)",
1585 "\0",
1586 "\u{1F600}",
1587 "1px",
1588 "1px\u{0301}",
1589 "0x10",
1590 ] {
1591 assert!(parse_clip_edge(input).is_err(), "{input:?} unexpectedly parsed");
1592 }
1593 assert_eq!(
1595 parse_clip_edge(" abc ").unwrap_err(),
1596 StyleClipRectParseError::InvalidValue("abc")
1597 );
1598 }
1599
1600 #[test]
1605 fn clip_rect_round_trips_through_print_as_css_value() {
1606 let rects = [
1607 StyleClipRect::default(),
1608 StyleClipRect {
1609 top: OptionF32::Some(0.0),
1610 right: OptionF32::Some(-2.25),
1611 bottom: OptionF32::Some(1.5),
1612 left: OptionF32::None,
1613 },
1614 StyleClipRect {
1615 top: OptionF32::Some(10.0),
1616 right: OptionF32::Some(20.0),
1617 bottom: OptionF32::Some(30.0),
1618 left: OptionF32::Some(40.0),
1619 },
1620 StyleClipRect {
1621 top: OptionF32::None,
1622 right: OptionF32::Some(-1.0),
1623 bottom: OptionF32::None,
1624 left: OptionF32::Some(-1.0),
1625 },
1626 ];
1627 for original in rects {
1628 let printed = original.print_as_css_value();
1629 let reparsed = parse_clip_rect(&printed).unwrap_or_else(|e| {
1630 panic!("{original:?} printed as {printed:?} but failed to reparse: {e}")
1631 });
1632 assert_eq!(reparsed, original, "round-trip broke via {printed:?}");
1633 }
1634 assert_eq!(
1635 StyleClipRect::default().print_as_css_value(),
1636 "rect(auto, auto, auto, auto)"
1637 );
1638 }
1639
1640 #[test]
1641 fn parse_clip_rect_accepts_the_auto_comma_and_legacy_space_forms() {
1642 let all_auto = StyleClipRect::default();
1643 for input in [
1644 "auto",
1645 "AUTO",
1646 " auto ",
1647 "\u{00A0}auto", "rect(auto, auto, auto, auto)",
1649 "rect(auto auto auto auto)",
1650 "RECT(auto, auto, auto, auto)",
1651 " rect( auto , auto , auto , auto ) ",
1652 ] {
1653 assert_eq!(
1654 parse_clip_rect(input).unwrap(),
1655 all_auto,
1656 "{input:?} should be all-auto"
1657 );
1658 }
1659
1660 let mixed = parse_clip_rect("rect(1px, auto, -3px, 4px)").unwrap();
1661 assert_eq!(mixed.top, OptionF32::Some(1.0));
1662 assert_eq!(mixed.right, OptionF32::None);
1663 assert_eq!(mixed.bottom, OptionF32::Some(-3.0));
1664 assert_eq!(mixed.left, OptionF32::Some(4.0));
1665
1666 assert_eq!(
1668 parse_clip_rect("rect(1px,2px,3px,4px)").unwrap(),
1669 StyleClipRect {
1670 top: OptionF32::Some(1.0),
1671 right: OptionF32::Some(2.0),
1672 bottom: OptionF32::Some(3.0),
1673 left: OptionF32::Some(4.0),
1674 }
1675 );
1676 }
1677
1678 #[test]
1679 fn parse_clip_rect_rejects_wrong_arity_mixed_separators_and_trailing_junk() {
1680 for input in [
1681 "rect()",
1682 "rect(,,,)",
1683 "rect(1px)",
1684 "rect(1px, 2px, 3px)",
1685 "rect(1px, 2px, 3px, 4px, 5px)",
1686 "rect(1px, 2px, 3px, 4px,)",
1687 "rect(1px 2px, 3px 4px)", "rect(1px 2px 3px)",
1689 "rect(1px 2px 3px 4px 5px)",
1690 "rect(1px, 2px, 3px, 4px", "rect 1px, 2px, 3px, 4px)", "rect (1px, 2px, 3px, 4px)", "rect(1px, 2px, 3px, 4px) trailing",
1694 "rect(1px, 2px, 3px, 4px);",
1695 "junk rect(1px, 2px, 3px, 4px)",
1696 "rect(auto, auto, auto, abc)",
1697 "",
1698 " ",
1699 "none",
1700 "inherit",
1701 "0",
1702 ] {
1703 assert!(parse_clip_rect(input).is_err(), "{input:?} unexpectedly parsed");
1704 }
1705 }
1706
1707 #[test]
1708 fn parse_clip_rect_function_name_accepts_only_all_lower_or_all_upper_case() {
1709 assert!(parse_clip_rect("rect(auto, auto, auto, auto)").is_ok());
1712 assert!(parse_clip_rect("RECT(auto, auto, auto, auto)").is_ok());
1713 for input in [
1714 "Rect(auto, auto, auto, auto)",
1715 "rECT(auto, auto, auto, auto)",
1716 "ReCt(auto, auto, auto, auto)",
1717 ] {
1718 assert!(parse_clip_rect(input).is_err(), "{input:?} unexpectedly parsed");
1719 }
1720 }
1721
1722 #[test]
1723 fn parse_clip_rect_errors_point_at_the_offending_token() {
1724 let err = parse_clip_rect("rect(1px, abc, 3px, 4px)").unwrap_err();
1726 assert_eq!(err, StyleClipRectParseError::InvalidValue("abc"));
1727 let msg = format!("{err}");
1728 assert!(msg.contains("abc"), "{msg}");
1729 assert!(!msg.contains("1px"), "message leaked the whole input: {msg}");
1732
1733 let err = parse_clip_rect(" rect(1px) ").unwrap_err();
1735 assert_eq!(err, StyleClipRectParseError::InvalidValue(" rect(1px) "));
1736 }
1737
1738 #[test]
1739 fn parse_clip_rect_survives_deep_nesting_and_huge_input() {
1740 let nested = format!("{}{}", "rect(".repeat(10_000), ")".repeat(10_000));
1742 assert!(parse_clip_rect(&nested).is_err());
1743
1744 let parens = format!("{}{}", "(".repeat(100_000), ")".repeat(100_000));
1745 assert!(parse_clip_rect(&parens).is_err());
1746
1747 let wide = format!("rect({})", "1px,".repeat(50_000));
1749 assert!(parse_clip_rect(&wide).is_err());
1750
1751 let long_token = format!("rect({}, auto, auto, auto)", "a".repeat(1_000_000));
1752 assert!(parse_clip_rect(&long_token).is_err());
1753
1754 let huge = format!("rect({}px, auto, auto, auto)", "9".repeat(4096));
1756 let huge = parse_clip_rect(&huge).unwrap();
1757 let top = huge.top.into_option().unwrap();
1758 assert!(top.is_finite());
1759 assert!(top > 0.0);
1760 }
1761
1762 #[test]
1763 fn parse_clip_rect_does_not_panic_on_multibyte_input() {
1764 for input in [
1765 "rect(\u{1F600}, \u{1F600}, \u{1F600}, \u{1F600})",
1766 "rect(1px\u{0301}, auto, auto, auto)",
1767 "réct(1px, 2px, 3px, 4px)",
1768 "rect(1px, auto, auto, auto)", "rect(1px, auto, auto, auto\u{200B})",
1770 "\u{1F600}",
1771 "автo",
1772 "rect(٣px, auto, auto, auto)", ] {
1774 assert!(parse_clip_rect(input).is_err(), "{input:?} unexpectedly parsed");
1775 }
1776 }
1777
1778 #[test]
1783 fn clip_rect_default_is_all_auto() {
1784 let d = StyleClipRect::default();
1785 assert_eq!(d.top, OptionF32::None);
1786 assert_eq!(d.right, OptionF32::None);
1787 assert_eq!(d.bottom, OptionF32::None);
1788 assert_eq!(d.left, OptionF32::None);
1789 }
1790
1791 #[test]
1792 fn clip_rect_resolve_expands_auto_edges_to_the_border_box() {
1793 let (top, right, bottom, left) = StyleClipRect::default().resolve(
1795 100.0, 50.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, );
1799 assert_eq!(top, 0.0);
1800 assert_eq!(left, 0.0);
1801 assert_eq!(right, 100.0 + 1.0 + 2.0 + 5.0 + 6.0);
1802 assert_eq!(bottom, 50.0 + 3.0 + 4.0 + 7.0 + 8.0);
1803 }
1804
1805 #[test]
1806 fn clip_rect_resolve_at_zero_and_with_negative_geometry() {
1807 let all_zero = StyleClipRect::default().resolve(
1808 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
1809 );
1810 assert_eq!(all_zero, (0.0, 0.0, 0.0, 0.0));
1811
1812 let (top, right, bottom, left) = StyleClipRect::default().resolve(
1814 -10.0, -20.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0,
1815 );
1816 assert_eq!(top, 0.0);
1817 assert_eq!(left, 0.0);
1818 assert_eq!(right, -14.0);
1819 assert_eq!(bottom, -24.0);
1820 }
1821
1822 #[test]
1823 fn clip_rect_resolve_ignores_the_geometry_for_explicit_edges() {
1824 let explicit = StyleClipRect {
1825 top: OptionF32::Some(1.0),
1826 right: OptionF32::Some(2.0),
1827 bottom: OptionF32::Some(3.0),
1828 left: OptionF32::Some(4.0),
1829 };
1830 for geometry in [
1832 f32::NAN,
1833 f32::INFINITY,
1834 f32::NEG_INFINITY,
1835 f32::MAX,
1836 f32::MIN,
1837 f32::MIN_POSITIVE,
1838 ] {
1839 let resolved = explicit.resolve(
1840 geometry, geometry, geometry, geometry, geometry, geometry, geometry, geometry,
1841 geometry, geometry,
1842 );
1843 assert_eq!(
1844 resolved,
1845 (1.0, 2.0, 3.0, 4.0),
1846 "explicit edges were perturbed by geometry {geometry:?}"
1847 );
1848 }
1849 }
1850
1851 #[test]
1852 fn clip_rect_resolve_saturates_at_f32_max_and_keeps_nan_contained() {
1853 let (top, right, bottom, left) = StyleClipRect::default().resolve(
1855 f32::MAX,
1856 f32::MAX,
1857 f32::MAX,
1858 f32::MAX,
1859 f32::MAX,
1860 f32::MAX,
1861 f32::MAX,
1862 f32::MAX,
1863 f32::MAX,
1864 f32::MAX,
1865 );
1866 assert_eq!(top, 0.0);
1867 assert_eq!(left, 0.0);
1868 assert!(right.is_infinite() && right.is_sign_positive());
1869 assert!(bottom.is_infinite() && bottom.is_sign_positive());
1870
1871 let (top, right, bottom, left) = StyleClipRect::default().resolve(
1874 f32::NAN,
1875 f32::NAN,
1876 0.0,
1877 0.0,
1878 0.0,
1879 0.0,
1880 0.0,
1881 0.0,
1882 0.0,
1883 0.0,
1884 );
1885 assert_eq!(top, 0.0);
1886 assert_eq!(left, 0.0);
1887 assert!(right.is_nan());
1888 assert!(bottom.is_nan());
1889
1890 let (_, right, bottom, _) = StyleClipRect::default().resolve(
1892 f32::INFINITY,
1893 f32::INFINITY,
1894 f32::NEG_INFINITY,
1895 0.0,
1896 f32::NEG_INFINITY,
1897 0.0,
1898 0.0,
1899 0.0,
1900 0.0,
1901 0.0,
1902 );
1903 assert!(right.is_nan());
1904 assert!(bottom.is_nan());
1905 }
1906
1907 fn error_payloads() -> Vec<String> {
1914 vec![
1915 String::new(),
1916 String::from(" "),
1917 String::from("bogus"),
1918 String::from("\u{1F600}\u{0301}"),
1919 String::from("a\0b"),
1920 String::from("rect(1px, 2px, 3px, 4px)"),
1921 "x".repeat(100_000),
1922 ]
1923 }
1924
1925 macro_rules! assert_error_round_trips {
1926 ($borrowed:ident) => {{
1927 for payload in error_payloads() {
1928 let borrowed = $borrowed::InvalidValue(payload.as_str());
1929 let owned = borrowed.to_contained();
1930 let back = owned.to_shared();
1931 assert_eq!(
1932 back, borrowed,
1933 "{}::InvalidValue({payload:?}) lost data on to_contained/to_shared",
1934 stringify!($borrowed)
1935 );
1936 assert_eq!(owned.to_shared().to_contained(), owned);
1938 }
1939 }};
1940 }
1941
1942 #[test]
1943 fn parse_errors_round_trip_between_borrowed_and_owned_forms() {
1944 assert_error_round_trips!(LayoutOverflowParseError);
1945 assert_error_round_trips!(StyleScrollbarGutterParseError);
1946 assert_error_round_trips!(StyleOverflowClipMarginParseError);
1947 assert_error_round_trips!(StyleClipRectParseError);
1948 }
1949
1950 #[test]
1951 fn parse_errors_produced_by_the_parsers_round_trip_too() {
1952 let e = parse_layout_overflow("nope").unwrap_err();
1953 assert_eq!(e.to_contained().to_shared(), e);
1954
1955 let e = parse_style_scrollbar_gutter("nope").unwrap_err();
1956 assert_eq!(e.to_contained().to_shared(), e);
1957
1958 let e = parse_style_overflow_clip_margin("nope nope").unwrap_err();
1959 assert_eq!(e.to_contained().to_shared(), e);
1960
1961 let e = parse_clip_rect("rect(nope)").unwrap_err();
1962 assert_eq!(e.to_contained().to_shared(), e);
1963 }
1964
1965 #[test]
1966 fn parse_error_messages_name_the_property_and_quote_the_value() {
1967 let msg = format!("{}", LayoutOverflowParseError::InvalidValue("zzz"));
1968 assert!(msg.contains("overflow") && msg.contains("zzz"), "{msg}");
1969
1970 let msg = format!("{}", StyleScrollbarGutterParseError::InvalidValue("zzz"));
1971 assert!(msg.contains("scrollbar-gutter") && msg.contains("zzz"), "{msg}");
1972
1973 let msg = format!("{}", StyleOverflowClipMarginParseError::InvalidValue("zzz"));
1974 assert!(
1975 msg.contains("overflow-clip-margin") && msg.contains("zzz"),
1976 "{msg}"
1977 );
1978
1979 let msg = format!("{}", StyleClipRectParseError::InvalidValue("zzz"));
1980 assert!(msg.contains("clip") && msg.contains("zzz"), "{msg}");
1981
1982 let weird = StyleClipRectParseError::InvalidValue("\u{1F600}\0\u{0301}");
1984 assert!(!format!("{weird:?}").is_empty());
1985 }
1986}