1use crate::corety::{AzString, OptionF32};
4use alloc::string::{String, ToString};
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]
41 pub const fn needs_scrollbar(&self, currently_overflowing: bool) -> bool {
42 match self {
43 Self::Scroll => true,
44 Self::Auto => currently_overflowing,
45 Self::Hidden | Self::Visible | Self::Clip => false,
46 }
47 }
48
49 #[must_use]
55 pub const fn is_clipped(&self) -> bool {
56 matches!(self, Self::Hidden | Self::Clip | Self::Auto | Self::Scroll)
58 }
59
60 #[must_use]
62 pub const fn is_scroll(&self) -> bool {
63 matches!(self, Self::Scroll)
64 }
65
66 #[must_use]
74 pub const fn is_scroll_container(&self) -> bool {
75 matches!(self, Self::Hidden | Self::Scroll | Self::Auto)
76 }
77
78 #[must_use]
82 pub const fn allows_user_scrolling(&self) -> bool {
83 matches!(self, Self::Scroll | Self::Auto)
84 }
85
86 #[must_use]
89 pub fn is_overflow_visible(&self) -> bool {
90 *self == Self::Visible
91 }
92
93 #[must_use]
95 pub fn is_overflow_hidden(&self) -> bool {
96 *self == Self::Hidden
97 }
98
99 #[must_use]
104 pub const fn resolve_computed(self, other_axis: Self) -> Self {
105 let other_is_scrollable = !matches!(other_axis, Self::Visible | Self::Clip);
106 if other_is_scrollable {
107 match self {
108 Self::Visible => Self::Auto,
109 Self::Clip => Self::Hidden,
110 other => other,
111 }
112 } else {
113 self
114 }
115 }
116}
117
118impl PrintAsCssValue for LayoutOverflow {
119 fn print_as_css_value(&self) -> String {
120 String::from(match self {
121 Self::Scroll => "scroll",
122 Self::Auto => "auto",
123 Self::Hidden => "hidden",
124 Self::Visible => "visible",
125 Self::Clip => "clip",
126 })
127 }
128}
129
130#[derive(Clone, PartialEq, Eq)]
134pub enum LayoutOverflowParseError<'a> {
135 InvalidValue(&'a str),
137}
138
139impl_debug_as_display!(LayoutOverflowParseError<'a>);
140impl_display! { LayoutOverflowParseError<'a>, {
141 InvalidValue(val) => format!(
142 "Invalid overflow value: \"{}\". Expected 'scroll', 'auto', 'hidden', 'visible', or 'clip'.", val
143 ),
144}}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
148#[repr(C, u8)]
149pub enum LayoutOverflowParseErrorOwned {
150 InvalidValue(AzString),
151}
152
153impl LayoutOverflowParseError<'_> {
154 #[must_use]
156 pub fn to_contained(&self) -> LayoutOverflowParseErrorOwned {
157 match self {
158 LayoutOverflowParseError::InvalidValue(s) => {
159 LayoutOverflowParseErrorOwned::InvalidValue((*s).to_string().into())
160 }
161 }
162 }
163}
164
165impl LayoutOverflowParseErrorOwned {
166 #[must_use]
168 pub fn to_shared(&self) -> LayoutOverflowParseError<'_> {
169 match self {
170 Self::InvalidValue(s) => LayoutOverflowParseError::InvalidValue(s.as_str()),
171 }
172 }
173}
174
175#[cfg(feature = "parser")]
176pub fn parse_layout_overflow(input: &str) -> Result<LayoutOverflow, LayoutOverflowParseError<'_>> {
181 let input_trimmed = input.trim();
182 match input_trimmed {
183 "scroll" => Ok(LayoutOverflow::Scroll),
184 "auto" | "overlay" => Ok(LayoutOverflow::Auto), "hidden" => Ok(LayoutOverflow::Hidden),
186 "visible" => Ok(LayoutOverflow::Visible),
187 "clip" => Ok(LayoutOverflow::Clip),
188 _ => Err(LayoutOverflowParseError::InvalidValue(input)),
189 }
190}
191
192#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
201#[repr(C)]
202pub enum StyleScrollbarGutter {
203 #[default]
205 Auto,
206 Stable,
208 StableBothEdges,
210}
211
212impl PrintAsCssValue for StyleScrollbarGutter {
213 fn print_as_css_value(&self) -> String {
214 String::from(match self {
215 Self::Auto => "auto",
216 Self::Stable => "stable",
217 Self::StableBothEdges => "stable both-edges",
218 })
219 }
220}
221
222#[derive(Clone, PartialEq, Eq)]
226pub enum StyleScrollbarGutterParseError<'a> {
227 InvalidValue(&'a str),
229}
230
231impl_debug_as_display!(StyleScrollbarGutterParseError<'a>);
232impl_display! { StyleScrollbarGutterParseError<'a>, {
233 InvalidValue(val) => format!(
234 "Invalid scrollbar-gutter value: \"{}\". Expected 'auto', 'stable', or 'stable both-edges'.", val
235 ),
236}}
237
238#[derive(Debug, Clone, PartialEq, Eq)]
240#[repr(C, u8)]
241pub enum StyleScrollbarGutterParseErrorOwned {
242 InvalidValue(AzString),
243}
244
245impl StyleScrollbarGutterParseError<'_> {
246 #[must_use]
248 pub fn to_contained(&self) -> StyleScrollbarGutterParseErrorOwned {
249 match self {
250 StyleScrollbarGutterParseError::InvalidValue(s) => {
251 StyleScrollbarGutterParseErrorOwned::InvalidValue((*s).to_string().into())
252 }
253 }
254 }
255}
256
257impl StyleScrollbarGutterParseErrorOwned {
258 #[must_use]
260 pub fn to_shared(&self) -> StyleScrollbarGutterParseError<'_> {
261 match self {
262 Self::InvalidValue(s) => StyleScrollbarGutterParseError::InvalidValue(s.as_str()),
263 }
264 }
265}
266
267#[cfg(feature = "parser")]
268pub fn parse_style_scrollbar_gutter(
273 input: &str,
274) -> Result<StyleScrollbarGutter, StyleScrollbarGutterParseError<'_>> {
275 let input_trimmed = input.trim();
276 match input_trimmed {
277 "auto" => Ok(StyleScrollbarGutter::Auto),
278 "stable" => Ok(StyleScrollbarGutter::Stable),
279 "stable both-edges" => Ok(StyleScrollbarGutter::StableBothEdges),
280 _ => Err(StyleScrollbarGutterParseError::InvalidValue(input)),
281 }
282}
283
284#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
295#[repr(C)]
296pub enum StyleTextOverflow {
297 #[default]
299 Clip,
300 Ellipsis,
302}
303
304impl PrintAsCssValue for StyleTextOverflow {
305 fn print_as_css_value(&self) -> String {
306 String::from(match self {
307 Self::Clip => "clip",
308 Self::Ellipsis => "ellipsis",
309 })
310 }
311}
312
313#[derive(Clone, PartialEq, Eq)]
317pub enum StyleTextOverflowParseError<'a> {
318 InvalidValue(&'a str),
320}
321
322impl_debug_as_display!(StyleTextOverflowParseError<'a>);
323impl_display! { StyleTextOverflowParseError<'a>, {
324 InvalidValue(val) => format!(
325 "Invalid text-overflow value: \"{}\". Expected 'clip' or 'ellipsis'.", val
326 ),
327}}
328
329#[derive(Debug, Clone, PartialEq, Eq)]
331#[repr(C, u8)]
332pub enum StyleTextOverflowParseErrorOwned {
333 InvalidValue(AzString),
334}
335
336impl StyleTextOverflowParseError<'_> {
337 #[must_use]
339 pub fn to_contained(&self) -> StyleTextOverflowParseErrorOwned {
340 match self {
341 StyleTextOverflowParseError::InvalidValue(s) => {
342 StyleTextOverflowParseErrorOwned::InvalidValue((*s).to_string().into())
343 }
344 }
345 }
346}
347
348impl StyleTextOverflowParseErrorOwned {
349 #[must_use]
351 pub fn to_shared(&self) -> StyleTextOverflowParseError<'_> {
352 match self {
353 Self::InvalidValue(s) => StyleTextOverflowParseError::InvalidValue(s.as_str()),
354 }
355 }
356}
357
358#[cfg(feature = "parser")]
359pub fn parse_style_text_overflow(
364 input: &str,
365) -> Result<StyleTextOverflow, StyleTextOverflowParseError<'_>> {
366 match input.trim() {
367 "clip" => Ok(StyleTextOverflow::Clip),
368 "ellipsis" => Ok(StyleTextOverflow::Ellipsis),
369 other => Err(StyleTextOverflowParseError::InvalidValue(other)),
370 }
371}
372
373#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
381#[repr(C)]
382pub enum VisualBox {
383 ContentBox,
385 #[default]
387 PaddingBox,
388 BorderBox,
390}
391
392impl PrintAsCssValue for VisualBox {
393 fn print_as_css_value(&self) -> String {
394 String::from(match self {
395 Self::ContentBox => "content-box",
396 Self::PaddingBox => "padding-box",
397 Self::BorderBox => "border-box",
398 })
399 }
400}
401
402#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
411#[repr(C)]
412pub struct StyleOverflowClipMargin {
413 pub clip_edge: VisualBox,
415 pub inner: crate::props::basic::pixel::PixelValue,
417}
418
419impl PrintAsCssValue for StyleOverflowClipMargin {
420 fn print_as_css_value(&self) -> String {
421 let edge = self.clip_edge.print_as_css_value();
422 let len = self.inner.print_as_css_value();
423 #[allow(clippy::float_cmp)] if self.inner.number.get() == 0.0 {
425 edge
426 } else if self.clip_edge == VisualBox::PaddingBox {
427 len
428 } else {
429 format!("{edge} {len}")
430 }
431 }
432}
433
434#[derive(Clone, PartialEq, Eq)]
436pub enum StyleOverflowClipMarginParseError<'a> {
437 InvalidValue(&'a str),
439}
440
441impl_debug_as_display!(StyleOverflowClipMarginParseError<'a>);
442impl_display! { StyleOverflowClipMarginParseError<'a>, {
443 InvalidValue(val) => format!("Invalid overflow-clip-margin value: \"{}\"", val),
444}}
445
446#[derive(Debug, Clone, PartialEq, Eq)]
448#[repr(C, u8)]
449pub enum StyleOverflowClipMarginParseErrorOwned {
450 InvalidValue(AzString),
451}
452
453impl StyleOverflowClipMarginParseError<'_> {
454 #[must_use]
456 pub fn to_contained(&self) -> StyleOverflowClipMarginParseErrorOwned {
457 match self {
458 StyleOverflowClipMarginParseError::InvalidValue(s) => {
459 StyleOverflowClipMarginParseErrorOwned::InvalidValue((*s).to_string().into())
460 }
461 }
462 }
463}
464
465impl StyleOverflowClipMarginParseErrorOwned {
466 #[must_use]
468 pub fn to_shared(&self) -> StyleOverflowClipMarginParseError<'_> {
469 match self {
470 Self::InvalidValue(s) => StyleOverflowClipMarginParseError::InvalidValue(s.as_str()),
471 }
472 }
473}
474
475#[cfg(feature = "parser")]
476pub fn parse_style_overflow_clip_margin(
485 input: &str,
486) -> Result<StyleOverflowClipMargin, StyleOverflowClipMarginParseError<'_>> {
487 use crate::props::basic::pixel::parse_pixel_value;
488
489 let input_trimmed = input.trim();
490 let mut clip_edge = None;
491 let mut length = None;
492
493 for token in input_trimmed.split_whitespace() {
494 match token {
495 "content-box" if clip_edge.is_none() => clip_edge = Some(VisualBox::ContentBox),
496 "padding-box" if clip_edge.is_none() => clip_edge = Some(VisualBox::PaddingBox),
497 "border-box" if clip_edge.is_none() => clip_edge = Some(VisualBox::BorderBox),
498 _ if length.is_none() => match parse_pixel_value(token) {
499 Ok(pv) => length = Some(pv),
500 Err(_) => return Err(StyleOverflowClipMarginParseError::InvalidValue(input)),
501 },
502 _ => return Err(StyleOverflowClipMarginParseError::InvalidValue(input)),
503 }
504 }
505
506 if clip_edge.is_none() && length.is_none() {
507 return Err(StyleOverflowClipMarginParseError::InvalidValue(input));
508 }
509
510 Ok(StyleOverflowClipMargin {
511 clip_edge: clip_edge.unwrap_or_default(),
512 inner: length.unwrap_or_default(),
513 })
514}
515
516#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
529#[repr(C)]
530pub struct StyleClipRect {
531 pub top: OptionF32,
533 pub right: OptionF32,
535 pub bottom: OptionF32,
537 pub left: OptionF32,
539}
540
541impl StyleClipRect {
542 #[must_use]
547 pub fn resolve(
548 &self,
549 used_width: f32,
550 used_height: f32,
551 padding_left: f32,
552 padding_right: f32,
553 padding_top: f32,
554 padding_bottom: f32,
555 border_left: f32,
556 border_right: f32,
557 border_top: f32,
558 border_bottom: f32,
559 ) -> (f32, f32, f32, f32) {
560 let top = self.top.into_option().unwrap_or(0.0);
561 let left = self.left.into_option().unwrap_or(0.0);
562 let bottom = self
563 .bottom
564 .into_option()
565 .unwrap_or(used_height + padding_top + padding_bottom + border_top + border_bottom);
566 let right = self
567 .right
568 .into_option()
569 .unwrap_or(used_width + padding_left + padding_right + border_left + border_right);
570 (top, right, bottom, left)
571 }
572}
573
574impl PrintAsCssValue for StyleClipRect {
575 fn print_as_css_value(&self) -> String {
576 fn fmt_edge(o: OptionF32) -> String {
577 o.into_option()
578 .map_or_else(|| String::from("auto"), |v| format!("{v}px"))
579 }
580 format!(
581 "rect({}, {}, {}, {})",
582 fmt_edge(self.top),
583 fmt_edge(self.right),
584 fmt_edge(self.bottom),
585 fmt_edge(self.left)
586 )
587 }
588}
589
590#[derive(Clone, PartialEq, Eq)]
594pub enum StyleClipRectParseError<'a> {
595 InvalidValue(&'a str),
597}
598
599impl_debug_as_display!(StyleClipRectParseError<'a>);
600impl_display! { StyleClipRectParseError<'a>, {
601 InvalidValue(val) => format!(
602 "Invalid clip value: \"{}\". Expected 'auto' or 'rect(<top>, <right>, <bottom>, <left>)'.", val
603 ),
604}}
605
606#[derive(Debug, Clone, PartialEq, Eq)]
608#[repr(C, u8)]
609pub enum StyleClipRectParseErrorOwned {
610 InvalidValue(AzString),
611}
612
613impl StyleClipRectParseError<'_> {
614 #[must_use]
616 pub fn to_contained(&self) -> StyleClipRectParseErrorOwned {
617 match self {
618 StyleClipRectParseError::InvalidValue(s) => {
619 StyleClipRectParseErrorOwned::InvalidValue((*s).to_string().into())
620 }
621 }
622 }
623}
624
625impl StyleClipRectParseErrorOwned {
626 #[must_use]
628 pub fn to_shared(&self) -> StyleClipRectParseError<'_> {
629 match self {
630 Self::InvalidValue(s) => StyleClipRectParseError::InvalidValue(s.as_str()),
631 }
632 }
633}
634
635#[cfg(feature = "parser")]
636fn parse_clip_edge(token: &str) -> Result<OptionF32, StyleClipRectParseError<'_>> {
637 use crate::props::basic::pixel::parse_pixel_value;
638
639 let token = token.trim();
640 if token.eq_ignore_ascii_case("auto") {
641 return Ok(OptionF32::None);
642 }
643 let pv = parse_pixel_value(token).map_err(|_| StyleClipRectParseError::InvalidValue(token))?;
644 Ok(OptionF32::Some(pv.number.get()))
645}
646
647#[cfg(feature = "parser")]
648pub fn parse_clip_rect(input: &str) -> Result<StyleClipRect, StyleClipRectParseError<'_>> {
660 let trimmed = input.trim();
661
662 if trimmed.eq_ignore_ascii_case("auto") {
663 return Ok(StyleClipRect::default());
664 }
665
666 let inner = trimmed
667 .strip_prefix("rect(")
668 .or_else(|| trimmed.strip_prefix("RECT("))
669 .and_then(|s| s.strip_suffix(')'))
670 .ok_or(StyleClipRectParseError::InvalidValue(input))?;
671
672 let inner = inner.trim();
673 let parts: Vec<&str> = if inner.contains(',') {
674 inner.split(',').map(str::trim).collect()
675 } else {
676 inner.split_whitespace().collect()
677 };
678
679 if parts.len() != 4 {
680 return Err(StyleClipRectParseError::InvalidValue(input));
681 }
682
683 Ok(StyleClipRect {
684 top: parse_clip_edge(parts[0])?,
685 right: parse_clip_edge(parts[1])?,
686 bottom: parse_clip_edge(parts[2])?,
687 left: parse_clip_edge(parts[3])?,
688 })
689}
690
691#[cfg(all(test, feature = "parser"))]
692mod tests {
693 use super::*;
694
695 #[test]
696 fn test_parse_layout_overflow_valid() {
697 assert_eq!(
698 parse_layout_overflow("visible").unwrap(),
699 LayoutOverflow::Visible
700 );
701 assert_eq!(
702 parse_layout_overflow("hidden").unwrap(),
703 LayoutOverflow::Hidden
704 );
705 assert_eq!(parse_layout_overflow("clip").unwrap(), LayoutOverflow::Clip);
706 assert_eq!(
707 parse_layout_overflow("scroll").unwrap(),
708 LayoutOverflow::Scroll
709 );
710 assert_eq!(parse_layout_overflow("auto").unwrap(), LayoutOverflow::Auto);
711 }
712
713 #[test]
714 fn test_parse_style_text_overflow_valid() {
715 assert_eq!(
716 parse_style_text_overflow("clip").unwrap(),
717 StyleTextOverflow::Clip
718 );
719 assert_eq!(
720 parse_style_text_overflow("ellipsis").unwrap(),
721 StyleTextOverflow::Ellipsis
722 );
723 assert_eq!(
725 parse_style_text_overflow(" ellipsis ").unwrap(),
726 StyleTextOverflow::Ellipsis
727 );
728 assert_eq!(StyleTextOverflow::default(), StyleTextOverflow::Clip);
730 }
731
732 #[test]
733 fn test_parse_style_text_overflow_invalid() {
734 assert!(parse_style_text_overflow("none").is_err());
735 assert!(parse_style_text_overflow("").is_err());
736 assert!(parse_style_text_overflow("fade").is_err());
737 let msg = format!("{}", StyleTextOverflowParseError::InvalidValue("fade"));
739 assert!(
740 msg.contains("text-overflow") && msg.contains("fade"),
741 "{msg}"
742 );
743 let e = parse_style_text_overflow("fade").unwrap_err();
745 assert_eq!(e.to_contained().to_shared(), e);
746 }
747
748 #[test]
749 fn test_style_text_overflow_print_round_trip() {
750 for v in [StyleTextOverflow::Clip, StyleTextOverflow::Ellipsis] {
751 let printed = v.print_as_css_value();
752 assert_eq!(parse_style_text_overflow(&printed).unwrap(), v);
753 }
754 }
755
756 #[test]
757 fn test_parse_layout_overflow_whitespace() {
758 assert_eq!(
759 parse_layout_overflow(" scroll ").unwrap(),
760 LayoutOverflow::Scroll
761 );
762 }
763
764 #[test]
765 fn test_parse_layout_overflow_invalid() {
766 assert!(parse_layout_overflow("none").is_err());
767 assert!(parse_layout_overflow("").is_err());
768 assert!(parse_layout_overflow("auto scroll").is_err());
769 assert!(parse_layout_overflow("hidden-x").is_err());
770 }
771
772 #[test]
773 fn test_needs_scrollbar() {
774 assert!(LayoutOverflow::Scroll.needs_scrollbar(false));
775 assert!(LayoutOverflow::Scroll.needs_scrollbar(true));
776 assert!(LayoutOverflow::Auto.needs_scrollbar(true));
777 assert!(!LayoutOverflow::Auto.needs_scrollbar(false));
778 assert!(!LayoutOverflow::Hidden.needs_scrollbar(true));
779 assert!(!LayoutOverflow::Visible.needs_scrollbar(true));
780 assert!(!LayoutOverflow::Clip.needs_scrollbar(true));
781 }
782
783 #[test]
784 fn test_parse_clip_rect_auto_keyword() {
785 let r = parse_clip_rect("auto").unwrap();
786 assert_eq!(r.top, OptionF32::None);
787 assert_eq!(r.right, OptionF32::None);
788 assert_eq!(r.bottom, OptionF32::None);
789 assert_eq!(r.left, OptionF32::None);
790 }
791
792 #[test]
793 fn test_parse_clip_rect_all_auto_in_rect() {
794 let r = parse_clip_rect("rect(auto, auto, auto, auto)").unwrap();
795 assert_eq!(r.top, OptionF32::None);
796 assert_eq!(r.right, OptionF32::None);
797 assert_eq!(r.bottom, OptionF32::None);
798 assert_eq!(r.left, OptionF32::None);
799 }
800
801 #[test]
802 fn test_parse_clip_rect_mixed_auto_and_lengths() {
803 let r = parse_clip_rect("rect(10px, auto, 30px, auto)").unwrap();
804 assert_eq!(r.top, OptionF32::Some(10.0));
805 assert_eq!(r.right, OptionF32::None);
806 assert_eq!(r.bottom, OptionF32::Some(30.0));
807 assert_eq!(r.left, OptionF32::None);
808 }
809
810 #[test]
811 fn test_parse_clip_rect_negative_lengths() {
812 let r = parse_clip_rect("rect(-5px, 0px, -10px, 0px)").unwrap();
813 assert_eq!(r.top, OptionF32::Some(-5.0));
814 assert_eq!(r.right, OptionF32::Some(0.0));
815 assert_eq!(r.bottom, OptionF32::Some(-10.0));
816 assert_eq!(r.left, OptionF32::Some(0.0));
817 }
818
819 #[test]
820 fn test_parse_clip_rect_legacy_space_separated() {
821 let r = parse_clip_rect("rect(1px 2px 3px 4px)").unwrap();
823 assert_eq!(r.top, OptionF32::Some(1.0));
824 assert_eq!(r.right, OptionF32::Some(2.0));
825 assert_eq!(r.bottom, OptionF32::Some(3.0));
826 assert_eq!(r.left, OptionF32::Some(4.0));
827 }
828
829 #[test]
830 fn test_parse_clip_rect_malformed() {
831 assert!(parse_clip_rect("").is_err());
832 assert!(parse_clip_rect("none").is_err());
833 assert!(parse_clip_rect("rect(10px, 20px, 30px)").is_err());
835 assert!(parse_clip_rect("rect(10px, 20px, 30px, 40px").is_err());
837 assert!(parse_clip_rect("rect(10px, abc, 30px, 40px)").is_err());
839 }
840}
841
842#[cfg(all(test, feature = "parser"))]
843mod autotest_generated {
844 use crate::props::basic::length::SizeMetric;
845 use crate::props::basic::pixel::PixelValue;
846
847 use super::*;
848
849 const ALL_OVERFLOW: [LayoutOverflow; 5] = [
855 LayoutOverflow::Scroll,
856 LayoutOverflow::Auto,
857 LayoutOverflow::Hidden,
858 LayoutOverflow::Visible,
859 LayoutOverflow::Clip,
860 ];
861
862 const fn overflow_variant_index(o: LayoutOverflow) -> usize {
863 match o {
864 LayoutOverflow::Scroll => 0,
865 LayoutOverflow::Auto => 1,
866 LayoutOverflow::Hidden => 2,
867 LayoutOverflow::Visible => 3,
868 LayoutOverflow::Clip => 4,
869 }
870 }
871
872 const ALL_GUTTER: [StyleScrollbarGutter; 3] = [
873 StyleScrollbarGutter::Auto,
874 StyleScrollbarGutter::Stable,
875 StyleScrollbarGutter::StableBothEdges,
876 ];
877
878 const fn gutter_variant_index(g: StyleScrollbarGutter) -> usize {
879 match g {
880 StyleScrollbarGutter::Auto => 0,
881 StyleScrollbarGutter::Stable => 1,
882 StyleScrollbarGutter::StableBothEdges => 2,
883 }
884 }
885
886 const ALL_VISUAL_BOX: [VisualBox; 3] = [
887 VisualBox::ContentBox,
888 VisualBox::PaddingBox,
889 VisualBox::BorderBox,
890 ];
891
892 const fn visual_box_variant_index(v: VisualBox) -> usize {
893 match v {
894 VisualBox::ContentBox => 0,
895 VisualBox::PaddingBox => 1,
896 VisualBox::BorderBox => 2,
897 }
898 }
899
900 const fn is_scrollable(o: LayoutOverflow) -> bool {
903 !matches!(o, LayoutOverflow::Visible | LayoutOverflow::Clip)
904 }
905
906 #[test]
907 fn variant_tables_cover_every_variant_exactly_once() {
908 for (i, o) in ALL_OVERFLOW.iter().enumerate() {
909 assert_eq!(overflow_variant_index(*o), i);
910 }
911 for (i, g) in ALL_GUTTER.iter().enumerate() {
912 assert_eq!(gutter_variant_index(*g), i);
913 }
914 for (i, v) in ALL_VISUAL_BOX.iter().enumerate() {
915 assert_eq!(visual_box_variant_index(*v), i);
916 }
917 }
918
919 #[test]
924 fn needs_scrollbar_truth_table_is_monotone_in_currently_overflowing() {
925 for o in ALL_OVERFLOW {
926 let idle = o.needs_scrollbar(false);
927 let overflowing = o.needs_scrollbar(true);
928
929 assert!(
932 !idle || overflowing,
933 "{o:?} shows a scrollbar when idle but hides it when overflowing"
934 );
935
936 let (expect_idle, expect_overflowing) = match o {
939 LayoutOverflow::Scroll => (true, true),
940 LayoutOverflow::Auto => (false, true),
941 LayoutOverflow::Hidden | LayoutOverflow::Visible | LayoutOverflow::Clip => {
942 (false, false)
943 }
944 };
945 assert_eq!(idle, expect_idle, "needs_scrollbar(false) wrong for {o:?}");
946 assert_eq!(
947 overflowing, expect_overflowing,
948 "needs_scrollbar(true) wrong for {o:?}"
949 );
950
951 assert!(!overflowing || o.is_clipped());
953 }
954 }
955
956 #[test]
957 fn is_clipped_is_exactly_the_negation_of_is_overflow_visible() {
958 for o in ALL_OVERFLOW {
959 assert_eq!(
960 o.is_clipped(),
961 !o.is_overflow_visible(),
962 "is_clipped/is_overflow_visible disagree for {o:?}"
963 );
964 assert_eq!(o.is_clipped(), o.is_clipped());
966 }
967 assert!(!LayoutOverflow::Visible.is_clipped());
968 assert!(LayoutOverflow::Hidden.is_clipped());
969 }
970
971 #[test]
972 fn is_scroll_and_is_overflow_hidden_match_exactly_one_variant_each() {
973 let scrolls: Vec<LayoutOverflow> = ALL_OVERFLOW
974 .into_iter()
975 .filter(LayoutOverflow::is_scroll)
976 .collect();
977 assert_eq!(scrolls, vec![LayoutOverflow::Scroll]);
978
979 let hiddens: Vec<LayoutOverflow> = ALL_OVERFLOW
980 .into_iter()
981 .filter(LayoutOverflow::is_overflow_hidden)
982 .collect();
983 assert_eq!(hiddens, vec![LayoutOverflow::Hidden]);
984
985 assert!(!LayoutOverflow::Auto.is_scroll());
987 assert!(LayoutOverflow::Auto.needs_scrollbar(true));
988 }
989
990 #[test]
991 fn default_overflow_is_visible_and_neither_clips_nor_scrolls() {
992 let d = LayoutOverflow::default();
993 assert_eq!(d, LayoutOverflow::Visible);
994 assert!(d.is_overflow_visible());
995 assert!(!d.is_clipped());
996 assert!(!d.is_scroll());
997 assert!(!d.is_overflow_hidden());
998 assert!(!d.needs_scrollbar(false));
999 assert!(!d.needs_scrollbar(true));
1000 }
1001
1002 #[test]
1007 fn resolve_computed_is_identity_when_the_other_axis_is_not_scrollable() {
1008 for other in [LayoutOverflow::Visible, LayoutOverflow::Clip] {
1009 for o in ALL_OVERFLOW {
1010 assert_eq!(
1011 o.resolve_computed(other),
1012 o,
1013 "{o:?} must be untouched when the other axis is {other:?}"
1014 );
1015 }
1016 }
1017 }
1018
1019 #[test]
1020 fn resolve_computed_promotes_visible_to_auto_and_clip_to_hidden() {
1021 for other in [
1022 LayoutOverflow::Scroll,
1023 LayoutOverflow::Auto,
1024 LayoutOverflow::Hidden,
1025 ] {
1026 assert_eq!(
1027 LayoutOverflow::Visible.resolve_computed(other),
1028 LayoutOverflow::Auto
1029 );
1030 assert_eq!(
1031 LayoutOverflow::Clip.resolve_computed(other),
1032 LayoutOverflow::Hidden
1033 );
1034 for o in [
1036 LayoutOverflow::Scroll,
1037 LayoutOverflow::Auto,
1038 LayoutOverflow::Hidden,
1039 ] {
1040 assert_eq!(o.resolve_computed(other), o);
1041 }
1042 }
1043 }
1044
1045 #[test]
1046 fn resolve_computed_is_idempotent_and_never_removes_clipping() {
1047 for o in ALL_OVERFLOW {
1048 for other in ALL_OVERFLOW {
1049 let once = o.resolve_computed(other);
1050 assert_eq!(
1051 once.resolve_computed(other),
1052 once,
1053 "resolve_computed not idempotent for ({o:?}, {other:?})"
1054 );
1055 assert!(
1057 !o.is_clipped() || once.is_clipped(),
1058 "({o:?}, {other:?}) lost clipping"
1059 );
1060 assert!(!is_scrollable(o) || is_scrollable(once));
1062 }
1063 }
1064 }
1065
1066 #[test]
1067 fn resolve_computed_leaves_both_axes_consistently_scrollable() {
1068 for x in ALL_OVERFLOW {
1072 for y in ALL_OVERFLOW {
1073 let rx = x.resolve_computed(y);
1074 let ry = y.resolve_computed(x);
1075 assert_eq!(
1076 is_scrollable(rx),
1077 is_scrollable(ry),
1078 "({x:?}, {y:?}) resolved to the mismatched pair ({rx:?}, {ry:?})"
1079 );
1080 }
1081 }
1082
1083 assert_eq!(
1085 LayoutOverflow::Visible.resolve_computed(LayoutOverflow::Scroll),
1086 LayoutOverflow::Auto
1087 );
1088 assert_eq!(
1089 LayoutOverflow::Scroll.resolve_computed(LayoutOverflow::Visible),
1090 LayoutOverflow::Scroll
1091 );
1092 assert_eq!(
1094 LayoutOverflow::Visible.resolve_computed(LayoutOverflow::Clip),
1095 LayoutOverflow::Visible
1096 );
1097 assert_eq!(
1098 LayoutOverflow::Clip.resolve_computed(LayoutOverflow::Visible),
1099 LayoutOverflow::Clip
1100 );
1101 }
1102
1103 #[test]
1108 fn layout_overflow_round_trips_through_print_as_css_value() {
1109 for o in ALL_OVERFLOW {
1110 let printed = o.print_as_css_value();
1111 assert_eq!(
1112 parse_layout_overflow(&printed).unwrap(),
1113 o,
1114 "{o:?} printed as {printed:?} did not round-trip"
1115 );
1116 assert!(!printed.is_empty());
1118 assert!(!printed.contains(char::is_whitespace));
1119 assert_eq!(printed, printed.to_lowercase());
1120 }
1121 }
1122
1123 #[test]
1124 fn parse_layout_overflow_treats_overlay_as_a_one_way_alias_of_auto() {
1125 assert_eq!(
1128 parse_layout_overflow("overlay").unwrap(),
1129 LayoutOverflow::Auto
1130 );
1131 let normalised = parse_layout_overflow("overlay")
1132 .unwrap()
1133 .print_as_css_value();
1134 assert_eq!(normalised, "auto");
1135 assert_eq!(
1136 parse_layout_overflow(&normalised).unwrap(),
1137 LayoutOverflow::Auto
1138 );
1139 for o in ALL_OVERFLOW {
1140 assert_ne!(o.print_as_css_value(), "overlay");
1141 }
1142 }
1143
1144 #[test]
1145 fn parse_layout_overflow_rejects_empty_and_whitespace_only_input() {
1146 for input in ["", " ", " ", "\t", "\n", "\r\n", "\t \n \r", "\u{00A0}"] {
1147 assert!(
1148 parse_layout_overflow(input).is_err(),
1149 "{input:?} must not parse"
1150 );
1151 }
1152 }
1153
1154 #[test]
1155 fn parse_layout_overflow_error_carries_the_untrimmed_input() {
1156 let err = parse_layout_overflow(" bogus ").unwrap_err();
1158 assert_eq!(err, LayoutOverflowParseError::InvalidValue(" bogus "));
1159 let msg = format!("{err}");
1160 assert!(msg.contains("bogus"), "{msg}");
1161 assert!(
1162 msg.contains("scroll"),
1163 "error should list the valid keywords: {msg}"
1164 );
1165 }
1166
1167 #[test]
1168 fn parse_layout_overflow_is_ascii_case_sensitive() {
1169 for input in [
1174 "SCROLL", "Scroll", "sCrOlL", "AUTO", "Hidden", "VISIBLE", "Clip",
1175 ] {
1176 assert!(
1177 parse_layout_overflow(input).is_err(),
1178 "{input:?} unexpectedly parsed"
1179 );
1180 }
1181 assert_eq!(
1182 parse_layout_overflow("scroll").unwrap(),
1183 LayoutOverflow::Scroll
1184 );
1185 }
1186
1187 #[test]
1188 fn parse_layout_overflow_trims_unicode_whitespace_but_not_zero_width_chars() {
1189 assert_eq!(
1192 parse_layout_overflow("\u{00A0}scroll\u{00A0}").unwrap(),
1193 LayoutOverflow::Scroll
1194 );
1195 assert_eq!(
1196 parse_layout_overflow("\u{3000}auto").unwrap(),
1197 LayoutOverflow::Auto
1198 );
1199 assert!(parse_layout_overflow("\u{200B}scroll").is_err());
1201 assert!(parse_layout_overflow("scroll\u{FEFF}").is_err());
1202 }
1203
1204 #[test]
1205 fn parse_layout_overflow_rejects_garbage_unicode_and_boundary_numbers() {
1206 for input in [
1207 "none",
1208 "hidden-x",
1209 "auto scroll",
1210 "scroll;",
1211 "scroll garbage",
1212 "visible !important",
1213 "\0",
1214 "scroll\0",
1215 "!@#$%^&*()",
1216 "\u{1F600}",
1217 "scroll\u{1F600}",
1218 "e\u{0301}",
1219 "scroll",
1220 "скролл",
1221 "0",
1222 "-0",
1223 "0.0",
1224 "NaN",
1225 "nan",
1226 "inf",
1227 "-inf",
1228 "infinity",
1229 "9223372036854775807",
1230 "-9223372036854775808",
1231 "1e400",
1232 "1e-400",
1233 ] {
1234 assert!(
1235 parse_layout_overflow(input).is_err(),
1236 "{input:?} unexpectedly parsed"
1237 );
1238 }
1239 }
1240
1241 #[test]
1242 fn parse_layout_overflow_survives_extremely_long_and_deeply_nested_input() {
1243 let long = "scroll".repeat(200_000);
1244 assert!(parse_layout_overflow(&long).is_err());
1245
1246 let junk = "a".repeat(1_000_000);
1247 assert!(parse_layout_overflow(&junk).is_err());
1248
1249 let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
1250 assert!(parse_layout_overflow(&nested).is_err());
1251
1252 let padded = format!("{}scroll{}", " ".repeat(500_000), " ".repeat(500_000));
1254 assert_eq!(
1255 parse_layout_overflow(&padded).unwrap(),
1256 LayoutOverflow::Scroll
1257 );
1258 }
1259
1260 #[test]
1265 fn scrollbar_gutter_round_trips_through_print_as_css_value() {
1266 for g in ALL_GUTTER {
1267 let printed = g.print_as_css_value();
1268 assert_eq!(
1269 parse_style_scrollbar_gutter(&printed).unwrap(),
1270 g,
1271 "{g:?} printed as {printed:?} did not round-trip"
1272 );
1273 }
1274 assert_eq!(
1275 StyleScrollbarGutter::StableBothEdges.print_as_css_value(),
1276 "stable both-edges"
1277 );
1278 assert_eq!(StyleScrollbarGutter::default(), StyleScrollbarGutter::Auto);
1279 }
1280
1281 #[test]
1282 fn parse_style_scrollbar_gutter_matches_the_keyword_string_verbatim() {
1283 assert_eq!(
1288 parse_style_scrollbar_gutter("stable both-edges").unwrap(),
1289 StyleScrollbarGutter::StableBothEdges
1290 );
1291 for rejected in [
1292 "stable both-edges", "stable\tboth-edges",
1294 "stable\nboth-edges",
1295 "both-edges stable", "both-edges",
1297 "STABLE",
1298 "Stable Both-Edges",
1299 "stable both-edges stable",
1300 ] {
1301 assert!(
1302 parse_style_scrollbar_gutter(rejected).is_err(),
1303 "{rejected:?} unexpectedly parsed"
1304 );
1305 }
1306 assert_eq!(
1308 parse_style_scrollbar_gutter(" stable both-edges \n").unwrap(),
1309 StyleScrollbarGutter::StableBothEdges
1310 );
1311 }
1312
1313 #[test]
1314 fn parse_style_scrollbar_gutter_rejects_empty_garbage_unicode_and_numbers() {
1315 for input in [
1316 "",
1317 " ",
1318 "\t\n",
1319 "none",
1320 "auto stable",
1321 "auto;",
1322 "stable;",
1323 "0",
1324 "-0",
1325 "NaN",
1326 "inf",
1327 "9223372036854775807",
1328 "\u{1F600}",
1329 "stable",
1330 "stable\0",
1331 ] {
1332 assert!(
1333 parse_style_scrollbar_gutter(input).is_err(),
1334 "{input:?} unexpectedly parsed"
1335 );
1336 }
1337 let err = parse_style_scrollbar_gutter(" nope ").unwrap_err();
1338 assert_eq!(
1339 err,
1340 StyleScrollbarGutterParseError::InvalidValue(" nope ")
1341 );
1342 assert!(format!("{err}").contains("scrollbar-gutter"));
1343 }
1344
1345 #[test]
1346 fn parse_style_scrollbar_gutter_survives_long_and_nested_input() {
1347 let long = "stable ".repeat(200_000);
1348 assert!(parse_style_scrollbar_gutter(&long).is_err());
1349 let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
1350 assert!(parse_style_scrollbar_gutter(&nested).is_err());
1351 }
1352
1353 #[test]
1358 fn parse_style_overflow_clip_margin_accepts_either_component_in_either_order() {
1359 let only_box = parse_style_overflow_clip_margin("content-box").unwrap();
1361 assert_eq!(only_box.clip_edge, VisualBox::ContentBox);
1362 assert_eq!(only_box.inner, PixelValue::default());
1363
1364 let only_len = parse_style_overflow_clip_margin("20px").unwrap();
1366 assert_eq!(only_len.clip_edge, VisualBox::PaddingBox);
1367 assert_eq!(only_len.inner, PixelValue::const_px(20));
1368
1369 let a = parse_style_overflow_clip_margin("border-box 10px").unwrap();
1371 let b = parse_style_overflow_clip_margin("10px border-box").unwrap();
1372 assert_eq!(a, b);
1373 assert_eq!(a.clip_edge, VisualBox::BorderBox);
1374 assert_eq!(a.inner, PixelValue::const_px(10));
1375
1376 let c = parse_style_overflow_clip_margin(" border-box \t\n 10px ").unwrap();
1378 assert_eq!(c, a);
1379
1380 assert_eq!(VisualBox::default(), VisualBox::PaddingBox);
1381 }
1382
1383 #[test]
1384 fn parse_style_overflow_clip_margin_rejects_empty_duplicates_and_garbage() {
1385 for input in [
1386 "",
1387 " ",
1388 "\t\n",
1389 "content-box content-box", "10px 20px", "content-box 10px 20px",
1392 "content-box padding-box",
1393 "content-box 10px border-box",
1394 "none",
1395 "auto",
1396 "margin-box",
1397 "10px;",
1398 "10 px extra",
1399 "px",
1400 "\u{1F600}",
1401 "10\u{1F600}",
1402 "content-box",
1403 "content_box",
1404 "CONTENT-BOX",
1405 ] {
1406 assert!(
1407 parse_style_overflow_clip_margin(input).is_err(),
1408 "{input:?} unexpectedly parsed"
1409 );
1410 }
1411 let err = parse_style_overflow_clip_margin(" nope ").unwrap_err();
1412 assert_eq!(
1413 err,
1414 StyleOverflowClipMarginParseError::InvalidValue(" nope ")
1415 );
1416 assert!(format!("{err}").contains("overflow-clip-margin"));
1417 }
1418
1419 #[test]
1420 fn parse_style_overflow_clip_margin_accepts_out_of_range_lengths() {
1421 let neg = parse_style_overflow_clip_margin("-5px").unwrap();
1425 assert!(neg.inner.number.get() < 0.0);
1426
1427 let pct = parse_style_overflow_clip_margin("50%").unwrap();
1428 assert_eq!(pct.inner.metric, SizeMetric::Percent);
1429 assert_eq!(pct.inner.number.get(), 50.0);
1430
1431 let unitless = parse_style_overflow_clip_margin("7").unwrap();
1433 assert_eq!(unitless.inner.metric, SizeMetric::Px);
1434 assert_eq!(unitless.inner.number.get(), 7.0);
1435 }
1436
1437 #[test]
1438 fn parse_style_overflow_clip_margin_saturates_nan_and_infinity() {
1439 let nan = parse_style_overflow_clip_margin("NaN").unwrap();
1444 assert!(!nan.inner.number.get().is_nan());
1445 assert_eq!(nan.inner.number.get(), 0.0);
1446
1447 let pos_inf = parse_style_overflow_clip_margin("inf").unwrap();
1448 assert!(pos_inf.inner.number.get().is_finite());
1449 assert!(pos_inf.inner.number.get() > 0.0);
1450
1451 let neg_inf = parse_style_overflow_clip_margin("-inf").unwrap();
1452 assert!(neg_inf.inner.number.get().is_finite());
1453 assert!(neg_inf.inner.number.get() < 0.0);
1454
1455 let huge = format!("{}px", "9".repeat(4096));
1458 let huge = parse_style_overflow_clip_margin(&huge).unwrap();
1459 assert!(huge.inner.number.get().is_finite());
1460
1461 let tiny = parse_style_overflow_clip_margin("0.0001px").unwrap();
1463 assert_eq!(tiny.inner.number.get(), 0.0);
1464 }
1465
1466 #[test]
1467 fn parse_style_overflow_clip_margin_survives_long_and_nested_input() {
1468 let long_token = format!("{}px", "a".repeat(1_000_000));
1469 assert!(parse_style_overflow_clip_margin(&long_token).is_err());
1470
1471 let many_tokens = "content-box ".repeat(100_000);
1472 assert!(parse_style_overflow_clip_margin(&many_tokens).is_err());
1473
1474 let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
1475 assert!(parse_style_overflow_clip_margin(&nested).is_err());
1476 }
1477
1478 #[test]
1479 fn overflow_clip_margin_round_trips_through_print_as_css_value() {
1480 let lengths = [
1481 PixelValue::const_px(12),
1482 PixelValue::px(1.5),
1483 PixelValue::const_em(2),
1484 PixelValue::const_percent(50),
1485 PixelValue::px(-3.25),
1486 ];
1487 for edge in ALL_VISUAL_BOX {
1488 for inner in lengths {
1489 let original = StyleOverflowClipMargin {
1490 clip_edge: edge,
1491 inner,
1492 };
1493 let printed = original.print_as_css_value();
1494 let reparsed = parse_style_overflow_clip_margin(&printed).unwrap_or_else(|e| {
1495 panic!("{original:?} printed as {printed:?} but failed to reparse: {e}")
1496 });
1497 assert_eq!(reparsed, original, "round-trip broke via {printed:?}");
1498 }
1499 }
1500 }
1501
1502 #[test]
1503 fn overflow_clip_margin_zero_length_prints_only_the_box_and_forgets_the_unit() {
1504 let zero_em = StyleOverflowClipMargin {
1509 clip_edge: VisualBox::ContentBox,
1510 inner: PixelValue::const_em(0),
1511 };
1512 assert_eq!(zero_em.print_as_css_value(), "content-box");
1513 let back = parse_style_overflow_clip_margin(&zero_em.print_as_css_value()).unwrap();
1514 assert_eq!(back.clip_edge, VisualBox::ContentBox);
1515 assert_eq!(back.inner.number.get(), 0.0);
1516 assert_eq!(back.inner.metric, SizeMetric::Px);
1517 assert_ne!(back, zero_em);
1518
1519 let default = StyleOverflowClipMargin::default();
1521 assert_eq!(default.print_as_css_value(), "padding-box");
1522 assert_eq!(
1523 parse_style_overflow_clip_margin(&default.print_as_css_value()).unwrap(),
1524 default
1525 );
1526
1527 let padding_len = StyleOverflowClipMargin {
1529 clip_edge: VisualBox::PaddingBox,
1530 inner: PixelValue::const_px(4),
1531 };
1532 assert_eq!(padding_len.print_as_css_value(), "4px");
1533 }
1534
1535 #[test]
1536 fn visual_box_round_trips_through_the_clip_margin_parser() {
1537 for v in ALL_VISUAL_BOX {
1538 let printed = v.print_as_css_value();
1539 let parsed = parse_style_overflow_clip_margin(&printed).unwrap();
1540 assert_eq!(parsed.clip_edge, v, "{printed:?} did not round-trip");
1541 }
1542 }
1543
1544 #[test]
1549 fn parse_clip_edge_auto_is_ascii_case_insensitive_and_trimmed() {
1550 for input in ["auto", "AUTO", "Auto", "aUtO", " auto ", "\tauto\n"] {
1551 assert_eq!(
1552 parse_clip_edge(input).unwrap(),
1553 OptionF32::None,
1554 "{input:?} should be auto"
1555 );
1556 }
1557 assert!(parse_clip_edge("auto5").is_err());
1559 assert!(parse_clip_edge("autopx").is_err());
1560 assert!(parse_clip_edge("auto auto").is_err());
1561 }
1562
1563 #[test]
1564 fn parse_clip_edge_silently_discards_the_unit() {
1565 for input in [
1569 "5px", "5em", "5rem", "5pt", "5in", "5cm", "5mm", "5vw", "5vh", "5%",
1570 ] {
1571 assert_eq!(
1572 parse_clip_edge(input).unwrap(),
1573 OptionF32::Some(5.0),
1574 "{input:?} did not collapse to a bare 5.0"
1575 );
1576 }
1577 assert_eq!(parse_clip_edge("5").unwrap(), OptionF32::Some(5.0));
1579 assert_eq!(parse_clip_edge("5 px").unwrap(), OptionF32::Some(5.0));
1581 }
1582
1583 #[test]
1584 fn parse_clip_edge_quantises_to_thousandths_and_normalises_negative_zero() {
1585 assert_eq!(parse_clip_edge("0.001px").unwrap(), OptionF32::Some(0.001));
1588 assert_eq!(parse_clip_edge("0.0001px").unwrap(), OptionF32::Some(0.0));
1589 assert_eq!(parse_clip_edge("-0.0009px").unwrap(), OptionF32::Some(0.0));
1590 assert_eq!(parse_clip_edge("1.9999px").unwrap(), OptionF32::Some(1.999));
1591
1592 let minus_zero = parse_clip_edge("-0px").unwrap().into_option().unwrap();
1594 assert_eq!(minus_zero, 0.0);
1595 assert!(minus_zero.is_sign_positive());
1596
1597 assert_eq!(parse_clip_edge("-10px").unwrap(), OptionF32::Some(-10.0));
1599 }
1600
1601 #[test]
1602 fn parse_clip_edge_saturates_nan_and_infinity_to_finite_values() {
1603 let nan = parse_clip_edge("NaN").unwrap().into_option().unwrap();
1604 assert!(!nan.is_nan(), "NaN must not survive into a clip edge");
1605 assert_eq!(nan, 0.0);
1606
1607 let pos_inf = parse_clip_edge("inf").unwrap().into_option().unwrap();
1608 assert!(pos_inf.is_finite());
1609 assert!(pos_inf > 0.0);
1610
1611 let neg_inf = parse_clip_edge("-infinity").unwrap().into_option().unwrap();
1612 assert!(neg_inf.is_finite());
1613 assert!(neg_inf < 0.0);
1614
1615 let huge = format!("{}px", "9".repeat(4096));
1616 let huge = parse_clip_edge(&huge).unwrap().into_option().unwrap();
1617 assert!(huge.is_finite());
1618 }
1619
1620 #[test]
1621 fn parse_clip_edge_rejects_empty_bare_units_and_garbage() {
1622 for input in [
1623 "",
1624 " ",
1625 "\t\n",
1626 "px",
1627 "em",
1628 "%",
1629 "abc",
1630 "10px;",
1631 "10px 20px",
1632 "(10px)",
1633 "\0",
1634 "\u{1F600}",
1635 "1px",
1636 "1px\u{0301}",
1637 "0x10",
1638 ] {
1639 assert!(
1640 parse_clip_edge(input).is_err(),
1641 "{input:?} unexpectedly parsed"
1642 );
1643 }
1644 assert_eq!(
1646 parse_clip_edge(" abc ").unwrap_err(),
1647 StyleClipRectParseError::InvalidValue("abc")
1648 );
1649 }
1650
1651 #[test]
1656 fn clip_rect_round_trips_through_print_as_css_value() {
1657 let rects = [
1658 StyleClipRect::default(),
1659 StyleClipRect {
1660 top: OptionF32::Some(0.0),
1661 right: OptionF32::Some(-2.25),
1662 bottom: OptionF32::Some(1.5),
1663 left: OptionF32::None,
1664 },
1665 StyleClipRect {
1666 top: OptionF32::Some(10.0),
1667 right: OptionF32::Some(20.0),
1668 bottom: OptionF32::Some(30.0),
1669 left: OptionF32::Some(40.0),
1670 },
1671 StyleClipRect {
1672 top: OptionF32::None,
1673 right: OptionF32::Some(-1.0),
1674 bottom: OptionF32::None,
1675 left: OptionF32::Some(-1.0),
1676 },
1677 ];
1678 for original in rects {
1679 let printed = original.print_as_css_value();
1680 let reparsed = parse_clip_rect(&printed).unwrap_or_else(|e| {
1681 panic!("{original:?} printed as {printed:?} but failed to reparse: {e}")
1682 });
1683 assert_eq!(reparsed, original, "round-trip broke via {printed:?}");
1684 }
1685 assert_eq!(
1686 StyleClipRect::default().print_as_css_value(),
1687 "rect(auto, auto, auto, auto)"
1688 );
1689 }
1690
1691 #[test]
1692 fn parse_clip_rect_accepts_the_auto_comma_and_legacy_space_forms() {
1693 let all_auto = StyleClipRect::default();
1694 for input in [
1695 "auto",
1696 "AUTO",
1697 " auto ",
1698 "\u{00A0}auto", "rect(auto, auto, auto, auto)",
1700 "rect(auto auto auto auto)",
1701 "RECT(auto, auto, auto, auto)",
1702 " rect( auto , auto , auto , auto ) ",
1703 ] {
1704 assert_eq!(
1705 parse_clip_rect(input).unwrap(),
1706 all_auto,
1707 "{input:?} should be all-auto"
1708 );
1709 }
1710
1711 let mixed = parse_clip_rect("rect(1px, auto, -3px, 4px)").unwrap();
1712 assert_eq!(mixed.top, OptionF32::Some(1.0));
1713 assert_eq!(mixed.right, OptionF32::None);
1714 assert_eq!(mixed.bottom, OptionF32::Some(-3.0));
1715 assert_eq!(mixed.left, OptionF32::Some(4.0));
1716
1717 assert_eq!(
1719 parse_clip_rect("rect(1px,2px,3px,4px)").unwrap(),
1720 StyleClipRect {
1721 top: OptionF32::Some(1.0),
1722 right: OptionF32::Some(2.0),
1723 bottom: OptionF32::Some(3.0),
1724 left: OptionF32::Some(4.0),
1725 }
1726 );
1727 }
1728
1729 #[test]
1730 fn parse_clip_rect_rejects_wrong_arity_mixed_separators_and_trailing_junk() {
1731 for input in [
1732 "rect()",
1733 "rect(,,,)",
1734 "rect(1px)",
1735 "rect(1px, 2px, 3px)",
1736 "rect(1px, 2px, 3px, 4px, 5px)",
1737 "rect(1px, 2px, 3px, 4px,)",
1738 "rect(1px 2px, 3px 4px)", "rect(1px 2px 3px)",
1740 "rect(1px 2px 3px 4px 5px)",
1741 "rect(1px, 2px, 3px, 4px", "rect 1px, 2px, 3px, 4px)", "rect (1px, 2px, 3px, 4px)", "rect(1px, 2px, 3px, 4px) trailing",
1745 "rect(1px, 2px, 3px, 4px);",
1746 "junk rect(1px, 2px, 3px, 4px)",
1747 "rect(auto, auto, auto, abc)",
1748 "",
1749 " ",
1750 "none",
1751 "inherit",
1752 "0",
1753 ] {
1754 assert!(
1755 parse_clip_rect(input).is_err(),
1756 "{input:?} unexpectedly parsed"
1757 );
1758 }
1759 }
1760
1761 #[test]
1762 fn parse_clip_rect_function_name_accepts_only_all_lower_or_all_upper_case() {
1763 assert!(parse_clip_rect("rect(auto, auto, auto, auto)").is_ok());
1766 assert!(parse_clip_rect("RECT(auto, auto, auto, auto)").is_ok());
1767 for input in [
1768 "Rect(auto, auto, auto, auto)",
1769 "rECT(auto, auto, auto, auto)",
1770 "ReCt(auto, auto, auto, auto)",
1771 ] {
1772 assert!(
1773 parse_clip_rect(input).is_err(),
1774 "{input:?} unexpectedly parsed"
1775 );
1776 }
1777 }
1778
1779 #[test]
1780 fn parse_clip_rect_errors_point_at_the_offending_token() {
1781 let err = parse_clip_rect("rect(1px, abc, 3px, 4px)").unwrap_err();
1783 assert_eq!(err, StyleClipRectParseError::InvalidValue("abc"));
1784 let msg = format!("{err}");
1785 assert!(msg.contains("abc"), "{msg}");
1786 assert!(
1789 !msg.contains("1px"),
1790 "message leaked the whole input: {msg}"
1791 );
1792
1793 let err = parse_clip_rect(" rect(1px) ").unwrap_err();
1795 assert_eq!(err, StyleClipRectParseError::InvalidValue(" rect(1px) "));
1796 }
1797
1798 #[test]
1799 fn parse_clip_rect_survives_deep_nesting_and_huge_input() {
1800 let nested = format!("{}{}", "rect(".repeat(10_000), ")".repeat(10_000));
1802 assert!(parse_clip_rect(&nested).is_err());
1803
1804 let parens = format!("{}{}", "(".repeat(100_000), ")".repeat(100_000));
1805 assert!(parse_clip_rect(&parens).is_err());
1806
1807 let wide = format!("rect({})", "1px,".repeat(50_000));
1809 assert!(parse_clip_rect(&wide).is_err());
1810
1811 let long_token = format!("rect({}, auto, auto, auto)", "a".repeat(1_000_000));
1812 assert!(parse_clip_rect(&long_token).is_err());
1813
1814 let huge = format!("rect({}px, auto, auto, auto)", "9".repeat(4096));
1816 let huge = parse_clip_rect(&huge).unwrap();
1817 let top = huge.top.into_option().unwrap();
1818 assert!(top.is_finite());
1819 assert!(top > 0.0);
1820 }
1821
1822 #[test]
1823 fn parse_clip_rect_does_not_panic_on_multibyte_input() {
1824 for input in [
1825 "rect(\u{1F600}, \u{1F600}, \u{1F600}, \u{1F600})",
1826 "rect(1px\u{0301}, auto, auto, auto)",
1827 "réct(1px, 2px, 3px, 4px)",
1828 "rect(1px, auto, auto, auto)", "rect(1px, auto, auto, auto\u{200B})",
1830 "\u{1F600}",
1831 "автo",
1832 "rect(٣px, auto, auto, auto)", ] {
1834 assert!(
1835 parse_clip_rect(input).is_err(),
1836 "{input:?} unexpectedly parsed"
1837 );
1838 }
1839 }
1840
1841 #[test]
1846 fn clip_rect_default_is_all_auto() {
1847 let d = StyleClipRect::default();
1848 assert_eq!(d.top, OptionF32::None);
1849 assert_eq!(d.right, OptionF32::None);
1850 assert_eq!(d.bottom, OptionF32::None);
1851 assert_eq!(d.left, OptionF32::None);
1852 }
1853
1854 #[test]
1855 fn clip_rect_resolve_expands_auto_edges_to_the_border_box() {
1856 let (top, right, bottom, left) = StyleClipRect::default().resolve(
1858 100.0, 50.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, );
1862 assert_eq!(top, 0.0);
1863 assert_eq!(left, 0.0);
1864 assert_eq!(right, 100.0 + 1.0 + 2.0 + 5.0 + 6.0);
1865 assert_eq!(bottom, 50.0 + 3.0 + 4.0 + 7.0 + 8.0);
1866 }
1867
1868 #[test]
1869 fn clip_rect_resolve_at_zero_and_with_negative_geometry() {
1870 let all_zero =
1871 StyleClipRect::default().resolve(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
1872 assert_eq!(all_zero, (0.0, 0.0, 0.0, 0.0));
1873
1874 let (top, right, bottom, left) = StyleClipRect::default()
1876 .resolve(-10.0, -20.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0);
1877 assert_eq!(top, 0.0);
1878 assert_eq!(left, 0.0);
1879 assert_eq!(right, -14.0);
1880 assert_eq!(bottom, -24.0);
1881 }
1882
1883 #[test]
1884 fn clip_rect_resolve_ignores_the_geometry_for_explicit_edges() {
1885 let explicit = StyleClipRect {
1886 top: OptionF32::Some(1.0),
1887 right: OptionF32::Some(2.0),
1888 bottom: OptionF32::Some(3.0),
1889 left: OptionF32::Some(4.0),
1890 };
1891 for geometry in [
1893 f32::NAN,
1894 f32::INFINITY,
1895 f32::NEG_INFINITY,
1896 f32::MAX,
1897 f32::MIN,
1898 f32::MIN_POSITIVE,
1899 ] {
1900 let resolved = explicit.resolve(
1901 geometry, geometry, geometry, geometry, geometry, geometry, geometry, geometry,
1902 geometry, geometry,
1903 );
1904 assert_eq!(
1905 resolved,
1906 (1.0, 2.0, 3.0, 4.0),
1907 "explicit edges were perturbed by geometry {geometry:?}"
1908 );
1909 }
1910 }
1911
1912 #[test]
1913 fn clip_rect_resolve_saturates_at_f32_max_and_keeps_nan_contained() {
1914 let (top, right, bottom, left) = StyleClipRect::default().resolve(
1916 f32::MAX,
1917 f32::MAX,
1918 f32::MAX,
1919 f32::MAX,
1920 f32::MAX,
1921 f32::MAX,
1922 f32::MAX,
1923 f32::MAX,
1924 f32::MAX,
1925 f32::MAX,
1926 );
1927 assert_eq!(top, 0.0);
1928 assert_eq!(left, 0.0);
1929 assert!(right.is_infinite() && right.is_sign_positive());
1930 assert!(bottom.is_infinite() && bottom.is_sign_positive());
1931
1932 let (top, right, bottom, left) = StyleClipRect::default().resolve(
1935 f32::NAN,
1936 f32::NAN,
1937 0.0,
1938 0.0,
1939 0.0,
1940 0.0,
1941 0.0,
1942 0.0,
1943 0.0,
1944 0.0,
1945 );
1946 assert_eq!(top, 0.0);
1947 assert_eq!(left, 0.0);
1948 assert!(right.is_nan());
1949 assert!(bottom.is_nan());
1950
1951 let (_, right, bottom, _) = StyleClipRect::default().resolve(
1953 f32::INFINITY,
1954 f32::INFINITY,
1955 f32::NEG_INFINITY,
1956 0.0,
1957 f32::NEG_INFINITY,
1958 0.0,
1959 0.0,
1960 0.0,
1961 0.0,
1962 0.0,
1963 );
1964 assert!(right.is_nan());
1965 assert!(bottom.is_nan());
1966 }
1967
1968 fn error_payloads() -> Vec<String> {
1975 vec![
1976 String::new(),
1977 String::from(" "),
1978 String::from("bogus"),
1979 String::from("\u{1F600}\u{0301}"),
1980 String::from("a\0b"),
1981 String::from("rect(1px, 2px, 3px, 4px)"),
1982 "x".repeat(100_000),
1983 ]
1984 }
1985
1986 macro_rules! assert_error_round_trips {
1987 ($borrowed:ident) => {{
1988 for payload in error_payloads() {
1989 let borrowed = $borrowed::InvalidValue(payload.as_str());
1990 let owned = borrowed.to_contained();
1991 let back = owned.to_shared();
1992 assert_eq!(
1993 back,
1994 borrowed,
1995 "{}::InvalidValue({payload:?}) lost data on to_contained/to_shared",
1996 stringify!($borrowed)
1997 );
1998 assert_eq!(owned.to_shared().to_contained(), owned);
2000 }
2001 }};
2002 }
2003
2004 #[test]
2005 fn parse_errors_round_trip_between_borrowed_and_owned_forms() {
2006 assert_error_round_trips!(LayoutOverflowParseError);
2007 assert_error_round_trips!(StyleScrollbarGutterParseError);
2008 assert_error_round_trips!(StyleOverflowClipMarginParseError);
2009 assert_error_round_trips!(StyleClipRectParseError);
2010 }
2011
2012 #[test]
2013 fn parse_errors_produced_by_the_parsers_round_trip_too() {
2014 let e = parse_layout_overflow("nope").unwrap_err();
2015 assert_eq!(e.to_contained().to_shared(), e);
2016
2017 let e = parse_style_scrollbar_gutter("nope").unwrap_err();
2018 assert_eq!(e.to_contained().to_shared(), e);
2019
2020 let e = parse_style_overflow_clip_margin("nope nope").unwrap_err();
2021 assert_eq!(e.to_contained().to_shared(), e);
2022
2023 let e = parse_clip_rect("rect(nope)").unwrap_err();
2024 assert_eq!(e.to_contained().to_shared(), e);
2025 }
2026
2027 #[test]
2028 fn parse_error_messages_name_the_property_and_quote_the_value() {
2029 let msg = format!("{}", LayoutOverflowParseError::InvalidValue("zzz"));
2030 assert!(msg.contains("overflow") && msg.contains("zzz"), "{msg}");
2031
2032 let msg = format!("{}", StyleScrollbarGutterParseError::InvalidValue("zzz"));
2033 assert!(
2034 msg.contains("scrollbar-gutter") && msg.contains("zzz"),
2035 "{msg}"
2036 );
2037
2038 let msg = format!("{}", StyleOverflowClipMarginParseError::InvalidValue("zzz"));
2039 assert!(
2040 msg.contains("overflow-clip-margin") && msg.contains("zzz"),
2041 "{msg}"
2042 );
2043
2044 let msg = format!("{}", StyleClipRectParseError::InvalidValue("zzz"));
2045 assert!(msg.contains("clip") && msg.contains("zzz"), "{msg}");
2046
2047 let weird = StyleClipRectParseError::InvalidValue("\u{1F600}\0\u{0301}");
2049 assert!(!format!("{weird:?}").is_empty());
2050 }
2051}