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 {
66 matches!(self, Self::Scroll)
67 }
68
69 #[must_use] pub const fn is_scroll_container(&self) -> bool {
77 matches!(self, Self::Hidden | Self::Scroll | Self::Auto)
78 }
79
80 #[must_use] pub const fn allows_user_scrolling(&self) -> bool {
84 matches!(self, Self::Scroll | Self::Auto)
85 }
86
87 #[must_use] pub fn is_overflow_visible(&self) -> bool {
90 *self == Self::Visible
91 }
92
93 #[must_use] pub fn is_overflow_hidden(&self) -> bool {
95 *self == Self::Hidden
96 }
97
98 #[must_use] pub const fn resolve_computed(self, other_axis: Self) -> Self {
103 let other_is_scrollable = !matches!(other_axis, Self::Visible | Self::Clip);
104 if other_is_scrollable {
105 match self {
106 Self::Visible => Self::Auto,
107 Self::Clip => Self::Hidden,
108 other => other,
109 }
110 } else {
111 self
112 }
113 }
114}
115
116impl PrintAsCssValue for LayoutOverflow {
117 fn print_as_css_value(&self) -> String {
118 String::from(match self {
119 Self::Scroll => "scroll",
120 Self::Auto => "auto",
121 Self::Hidden => "hidden",
122 Self::Visible => "visible",
123 Self::Clip => "clip",
124 })
125 }
126}
127
128#[derive(Clone, PartialEq, Eq)]
132pub enum LayoutOverflowParseError<'a> {
133 InvalidValue(&'a str),
135}
136
137impl_debug_as_display!(LayoutOverflowParseError<'a>);
138impl_display! { LayoutOverflowParseError<'a>, {
139 InvalidValue(val) => format!(
140 "Invalid overflow value: \"{}\". Expected 'scroll', 'auto', 'hidden', 'visible', or 'clip'.", val
141 ),
142}}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
146#[repr(C, u8)]
147pub enum LayoutOverflowParseErrorOwned {
148 InvalidValue(AzString),
149}
150
151impl LayoutOverflowParseError<'_> {
152 #[must_use] pub fn to_contained(&self) -> LayoutOverflowParseErrorOwned {
154 match self {
155 LayoutOverflowParseError::InvalidValue(s) => {
156 LayoutOverflowParseErrorOwned::InvalidValue((*s).to_string().into())
157 }
158 }
159 }
160}
161
162impl LayoutOverflowParseErrorOwned {
163 #[must_use] pub fn to_shared(&self) -> LayoutOverflowParseError<'_> {
165 match self {
166 Self::InvalidValue(s) => {
167 LayoutOverflowParseError::InvalidValue(s.as_str())
168 }
169 }
170 }
171}
172
173#[cfg(feature = "parser")]
174pub fn parse_layout_overflow(
179 input: &str,
180) -> 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] pub fn to_contained(&self) -> StyleScrollbarGutterParseErrorOwned {
248 match self {
249 StyleScrollbarGutterParseError::InvalidValue(s) => {
250 StyleScrollbarGutterParseErrorOwned::InvalidValue((*s).to_string().into())
251 }
252 }
253 }
254}
255
256impl StyleScrollbarGutterParseErrorOwned {
257 #[must_use] pub fn to_shared(&self) -> StyleScrollbarGutterParseError<'_> {
259 match self {
260 Self::InvalidValue(s) => {
261 StyleScrollbarGutterParseError::InvalidValue(s.as_str())
262 }
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] pub fn to_contained(&self) -> StyleTextOverflowParseErrorOwned {
339 match self {
340 StyleTextOverflowParseError::InvalidValue(s) => {
341 StyleTextOverflowParseErrorOwned::InvalidValue((*s).to_string().into())
342 }
343 }
344 }
345}
346
347impl StyleTextOverflowParseErrorOwned {
348 #[must_use] pub fn to_shared(&self) -> StyleTextOverflowParseError<'_> {
350 match self {
351 Self::InvalidValue(s) => {
352 StyleTextOverflowParseError::InvalidValue(s.as_str())
353 }
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] pub fn to_contained(&self) -> StyleOverflowClipMarginParseErrorOwned {
456 match self {
457 StyleOverflowClipMarginParseError::InvalidValue(s) => {
458 StyleOverflowClipMarginParseErrorOwned::InvalidValue((*s).to_string().into())
459 }
460 }
461 }
462}
463
464impl StyleOverflowClipMarginParseErrorOwned {
465 #[must_use] pub fn to_shared(&self) -> StyleOverflowClipMarginParseError<'_> {
467 match self {
468 Self::InvalidValue(s) => {
469 StyleOverflowClipMarginParseError::InvalidValue(s.as_str())
470 }
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() => {
499 match parse_pixel_value(token) {
500 Ok(pv) => length = Some(pv),
501 Err(_) => return Err(StyleOverflowClipMarginParseError::InvalidValue(input)),
502 }
503 }
504 _ => return Err(StyleOverflowClipMarginParseError::InvalidValue(input)),
505 }
506 }
507
508 if clip_edge.is_none() && length.is_none() {
509 return Err(StyleOverflowClipMarginParseError::InvalidValue(input));
510 }
511
512 Ok(StyleOverflowClipMargin {
513 clip_edge: clip_edge.unwrap_or_default(),
514 inner: length.unwrap_or_default(),
515 })
516}
517
518#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
531#[repr(C)]
532pub struct StyleClipRect {
533 pub top: OptionF32,
535 pub right: OptionF32,
537 pub bottom: OptionF32,
539 pub left: OptionF32,
541}
542
543impl StyleClipRect {
544 #[must_use] pub fn resolve(
549 &self,
550 used_width: f32,
551 used_height: f32,
552 padding_left: f32,
553 padding_right: f32,
554 padding_top: f32,
555 padding_bottom: f32,
556 border_left: f32,
557 border_right: f32,
558 border_top: f32,
559 border_bottom: f32,
560 ) -> (f32, f32, f32, f32) {
561 let top = self.top.into_option().unwrap_or(0.0);
562 let left = self.left.into_option().unwrap_or(0.0);
563 let bottom = self
564 .bottom
565 .into_option()
566 .unwrap_or(used_height + padding_top + padding_bottom + border_top + border_bottom);
567 let right = self
568 .right
569 .into_option()
570 .unwrap_or(used_width + padding_left + padding_right + border_left + border_right);
571 (top, right, bottom, left)
572 }
573}
574
575impl PrintAsCssValue for StyleClipRect {
576 fn print_as_css_value(&self) -> String {
577 fn fmt_edge(o: OptionF32) -> String {
578 o.into_option()
579 .map_or_else(|| String::from("auto"), |v| format!("{v}px"))
580 }
581 format!(
582 "rect({}, {}, {}, {})",
583 fmt_edge(self.top),
584 fmt_edge(self.right),
585 fmt_edge(self.bottom),
586 fmt_edge(self.left)
587 )
588 }
589}
590
591#[derive(Clone, PartialEq, Eq)]
595pub enum StyleClipRectParseError<'a> {
596 InvalidValue(&'a str),
598}
599
600impl_debug_as_display!(StyleClipRectParseError<'a>);
601impl_display! { StyleClipRectParseError<'a>, {
602 InvalidValue(val) => format!(
603 "Invalid clip value: \"{}\". Expected 'auto' or 'rect(<top>, <right>, <bottom>, <left>)'.", val
604 ),
605}}
606
607#[derive(Debug, Clone, PartialEq, Eq)]
609#[repr(C, u8)]
610pub enum StyleClipRectParseErrorOwned {
611 InvalidValue(AzString),
612}
613
614impl StyleClipRectParseError<'_> {
615 #[must_use] 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] pub fn to_shared(&self) -> StyleClipRectParseError<'_> {
628 match self {
629 Self::InvalidValue(s) => {
630 StyleClipRectParseError::InvalidValue(s.as_str())
631 }
632 }
633 }
634}
635
636#[cfg(feature = "parser")]
637fn parse_clip_edge(token: &str) -> Result<OptionF32, StyleClipRectParseError<'_>> {
638 use crate::props::basic::pixel::parse_pixel_value;
639
640 let token = token.trim();
641 if token.eq_ignore_ascii_case("auto") {
642 return Ok(OptionF32::None);
643 }
644 let pv = parse_pixel_value(token)
645 .map_err(|_| StyleClipRectParseError::InvalidValue(token))?;
646 Ok(OptionF32::Some(pv.number.get()))
647}
648
649#[cfg(feature = "parser")]
650pub fn parse_clip_rect(input: &str) -> Result<StyleClipRect, StyleClipRectParseError<'_>> {
662 let trimmed = input.trim();
663
664 if trimmed.eq_ignore_ascii_case("auto") {
665 return Ok(StyleClipRect::default());
666 }
667
668 let inner = trimmed
669 .strip_prefix("rect(")
670 .or_else(|| trimmed.strip_prefix("RECT("))
671 .and_then(|s| s.strip_suffix(')'))
672 .ok_or(StyleClipRectParseError::InvalidValue(input))?;
673
674 let inner = inner.trim();
675 let parts: Vec<&str> = if inner.contains(',') {
676 inner.split(',').map(str::trim).collect()
677 } else {
678 inner.split_whitespace().collect()
679 };
680
681 if parts.len() != 4 {
682 return Err(StyleClipRectParseError::InvalidValue(input));
683 }
684
685 Ok(StyleClipRect {
686 top: parse_clip_edge(parts[0])?,
687 right: parse_clip_edge(parts[1])?,
688 bottom: parse_clip_edge(parts[2])?,
689 left: parse_clip_edge(parts[3])?,
690 })
691}
692
693#[cfg(all(test, feature = "parser"))]
694mod tests {
695 use super::*;
696
697 #[test]
698 fn test_parse_layout_overflow_valid() {
699 assert_eq!(
700 parse_layout_overflow("visible").unwrap(),
701 LayoutOverflow::Visible
702 );
703 assert_eq!(
704 parse_layout_overflow("hidden").unwrap(),
705 LayoutOverflow::Hidden
706 );
707 assert_eq!(parse_layout_overflow("clip").unwrap(), LayoutOverflow::Clip);
708 assert_eq!(
709 parse_layout_overflow("scroll").unwrap(),
710 LayoutOverflow::Scroll
711 );
712 assert_eq!(parse_layout_overflow("auto").unwrap(), LayoutOverflow::Auto);
713 }
714
715 #[test]
716 fn test_parse_style_text_overflow_valid() {
717 assert_eq!(
718 parse_style_text_overflow("clip").unwrap(),
719 StyleTextOverflow::Clip
720 );
721 assert_eq!(
722 parse_style_text_overflow("ellipsis").unwrap(),
723 StyleTextOverflow::Ellipsis
724 );
725 assert_eq!(
727 parse_style_text_overflow(" ellipsis ").unwrap(),
728 StyleTextOverflow::Ellipsis
729 );
730 assert_eq!(StyleTextOverflow::default(), StyleTextOverflow::Clip);
732 }
733
734 #[test]
735 fn test_parse_style_text_overflow_invalid() {
736 assert!(parse_style_text_overflow("none").is_err());
737 assert!(parse_style_text_overflow("").is_err());
738 assert!(parse_style_text_overflow("fade").is_err());
739 let msg = format!(
741 "{}",
742 StyleTextOverflowParseError::InvalidValue("fade")
743 );
744 assert!(msg.contains("text-overflow") && msg.contains("fade"), "{msg}");
745 let e = parse_style_text_overflow("fade").unwrap_err();
747 assert_eq!(e.to_contained().to_shared(), e);
748 }
749
750 #[test]
751 fn test_style_text_overflow_print_round_trip() {
752 for v in [StyleTextOverflow::Clip, StyleTextOverflow::Ellipsis] {
753 let printed = v.print_as_css_value();
754 assert_eq!(parse_style_text_overflow(&printed).unwrap(), v);
755 }
756 }
757
758 #[test]
759 fn test_parse_layout_overflow_whitespace() {
760 assert_eq!(
761 parse_layout_overflow(" scroll ").unwrap(),
762 LayoutOverflow::Scroll
763 );
764 }
765
766 #[test]
767 fn test_parse_layout_overflow_invalid() {
768 assert!(parse_layout_overflow("none").is_err());
769 assert!(parse_layout_overflow("").is_err());
770 assert!(parse_layout_overflow("auto scroll").is_err());
771 assert!(parse_layout_overflow("hidden-x").is_err());
772 }
773
774 #[test]
775 fn test_needs_scrollbar() {
776 assert!(LayoutOverflow::Scroll.needs_scrollbar(false));
777 assert!(LayoutOverflow::Scroll.needs_scrollbar(true));
778 assert!(LayoutOverflow::Auto.needs_scrollbar(true));
779 assert!(!LayoutOverflow::Auto.needs_scrollbar(false));
780 assert!(!LayoutOverflow::Hidden.needs_scrollbar(true));
781 assert!(!LayoutOverflow::Visible.needs_scrollbar(true));
782 assert!(!LayoutOverflow::Clip.needs_scrollbar(true));
783 }
784
785 #[test]
786 fn test_parse_clip_rect_auto_keyword() {
787 let r = parse_clip_rect("auto").unwrap();
788 assert_eq!(r.top, OptionF32::None);
789 assert_eq!(r.right, OptionF32::None);
790 assert_eq!(r.bottom, OptionF32::None);
791 assert_eq!(r.left, OptionF32::None);
792 }
793
794 #[test]
795 fn test_parse_clip_rect_all_auto_in_rect() {
796 let r = parse_clip_rect("rect(auto, auto, auto, auto)").unwrap();
797 assert_eq!(r.top, OptionF32::None);
798 assert_eq!(r.right, OptionF32::None);
799 assert_eq!(r.bottom, OptionF32::None);
800 assert_eq!(r.left, OptionF32::None);
801 }
802
803 #[test]
804 fn test_parse_clip_rect_mixed_auto_and_lengths() {
805 let r = parse_clip_rect("rect(10px, auto, 30px, auto)").unwrap();
806 assert_eq!(r.top, OptionF32::Some(10.0));
807 assert_eq!(r.right, OptionF32::None);
808 assert_eq!(r.bottom, OptionF32::Some(30.0));
809 assert_eq!(r.left, OptionF32::None);
810 }
811
812 #[test]
813 fn test_parse_clip_rect_negative_lengths() {
814 let r = parse_clip_rect("rect(-5px, 0px, -10px, 0px)").unwrap();
815 assert_eq!(r.top, OptionF32::Some(-5.0));
816 assert_eq!(r.right, OptionF32::Some(0.0));
817 assert_eq!(r.bottom, OptionF32::Some(-10.0));
818 assert_eq!(r.left, OptionF32::Some(0.0));
819 }
820
821 #[test]
822 fn test_parse_clip_rect_legacy_space_separated() {
823 let r = parse_clip_rect("rect(1px 2px 3px 4px)").unwrap();
825 assert_eq!(r.top, OptionF32::Some(1.0));
826 assert_eq!(r.right, OptionF32::Some(2.0));
827 assert_eq!(r.bottom, OptionF32::Some(3.0));
828 assert_eq!(r.left, OptionF32::Some(4.0));
829 }
830
831 #[test]
832 fn test_parse_clip_rect_malformed() {
833 assert!(parse_clip_rect("").is_err());
834 assert!(parse_clip_rect("none").is_err());
835 assert!(parse_clip_rect("rect(10px, 20px, 30px)").is_err());
837 assert!(parse_clip_rect("rect(10px, 20px, 30px, 40px").is_err());
839 assert!(parse_clip_rect("rect(10px, abc, 30px, 40px)").is_err());
841 }
842}
843
844#[cfg(all(test, feature = "parser"))]
845mod autotest_generated {
846 use crate::props::basic::pixel::PixelValue;
847 use crate::props::basic::length::SizeMetric;
848
849 use super::*;
850
851 const ALL_OVERFLOW: [LayoutOverflow; 5] = [
857 LayoutOverflow::Scroll,
858 LayoutOverflow::Auto,
859 LayoutOverflow::Hidden,
860 LayoutOverflow::Visible,
861 LayoutOverflow::Clip,
862 ];
863
864 const fn overflow_variant_index(o: LayoutOverflow) -> usize {
865 match o {
866 LayoutOverflow::Scroll => 0,
867 LayoutOverflow::Auto => 1,
868 LayoutOverflow::Hidden => 2,
869 LayoutOverflow::Visible => 3,
870 LayoutOverflow::Clip => 4,
871 }
872 }
873
874 const ALL_GUTTER: [StyleScrollbarGutter; 3] = [
875 StyleScrollbarGutter::Auto,
876 StyleScrollbarGutter::Stable,
877 StyleScrollbarGutter::StableBothEdges,
878 ];
879
880 const fn gutter_variant_index(g: StyleScrollbarGutter) -> usize {
881 match g {
882 StyleScrollbarGutter::Auto => 0,
883 StyleScrollbarGutter::Stable => 1,
884 StyleScrollbarGutter::StableBothEdges => 2,
885 }
886 }
887
888 const ALL_VISUAL_BOX: [VisualBox; 3] = [
889 VisualBox::ContentBox,
890 VisualBox::PaddingBox,
891 VisualBox::BorderBox,
892 ];
893
894 const fn visual_box_variant_index(v: VisualBox) -> usize {
895 match v {
896 VisualBox::ContentBox => 0,
897 VisualBox::PaddingBox => 1,
898 VisualBox::BorderBox => 2,
899 }
900 }
901
902 const fn is_scrollable(o: LayoutOverflow) -> bool {
905 !matches!(o, LayoutOverflow::Visible | LayoutOverflow::Clip)
906 }
907
908 #[test]
909 fn variant_tables_cover_every_variant_exactly_once() {
910 for (i, o) in ALL_OVERFLOW.iter().enumerate() {
911 assert_eq!(overflow_variant_index(*o), i);
912 }
913 for (i, g) in ALL_GUTTER.iter().enumerate() {
914 assert_eq!(gutter_variant_index(*g), i);
915 }
916 for (i, v) in ALL_VISUAL_BOX.iter().enumerate() {
917 assert_eq!(visual_box_variant_index(*v), i);
918 }
919 }
920
921 #[test]
926 fn needs_scrollbar_truth_table_is_monotone_in_currently_overflowing() {
927 for o in ALL_OVERFLOW {
928 let idle = o.needs_scrollbar(false);
929 let overflowing = o.needs_scrollbar(true);
930
931 assert!(
934 !idle || overflowing,
935 "{o:?} shows a scrollbar when idle but hides it when overflowing"
936 );
937
938 let (expect_idle, expect_overflowing) = match o {
941 LayoutOverflow::Scroll => (true, true),
942 LayoutOverflow::Auto => (false, true),
943 LayoutOverflow::Hidden | LayoutOverflow::Visible | LayoutOverflow::Clip => {
944 (false, false)
945 }
946 };
947 assert_eq!(idle, expect_idle, "needs_scrollbar(false) wrong for {o:?}");
948 assert_eq!(
949 overflowing, expect_overflowing,
950 "needs_scrollbar(true) wrong for {o:?}"
951 );
952
953 assert!(!overflowing || o.is_clipped());
955 }
956 }
957
958 #[test]
959 fn is_clipped_is_exactly_the_negation_of_is_overflow_visible() {
960 for o in ALL_OVERFLOW {
961 assert_eq!(
962 o.is_clipped(),
963 !o.is_overflow_visible(),
964 "is_clipped/is_overflow_visible disagree for {o:?}"
965 );
966 assert_eq!(o.is_clipped(), o.is_clipped());
968 }
969 assert!(!LayoutOverflow::Visible.is_clipped());
970 assert!(LayoutOverflow::Hidden.is_clipped());
971 }
972
973 #[test]
974 fn is_scroll_and_is_overflow_hidden_match_exactly_one_variant_each() {
975 let scrolls: Vec<LayoutOverflow> =
976 ALL_OVERFLOW.into_iter().filter(LayoutOverflow::is_scroll).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!(parse_layout_overflow("overlay").unwrap(), LayoutOverflow::Auto);
1128 let normalised = parse_layout_overflow("overlay").unwrap().print_as_css_value();
1129 assert_eq!(normalised, "auto");
1130 assert_eq!(
1131 parse_layout_overflow(&normalised).unwrap(),
1132 LayoutOverflow::Auto
1133 );
1134 for o in ALL_OVERFLOW {
1135 assert_ne!(o.print_as_css_value(), "overlay");
1136 }
1137 }
1138
1139 #[test]
1140 fn parse_layout_overflow_rejects_empty_and_whitespace_only_input() {
1141 for input in ["", " ", " ", "\t", "\n", "\r\n", "\t \n \r", "\u{00A0}"] {
1142 assert!(
1143 parse_layout_overflow(input).is_err(),
1144 "{input:?} must not parse"
1145 );
1146 }
1147 }
1148
1149 #[test]
1150 fn parse_layout_overflow_error_carries_the_untrimmed_input() {
1151 let err = parse_layout_overflow(" bogus ").unwrap_err();
1153 assert_eq!(err, LayoutOverflowParseError::InvalidValue(" bogus "));
1154 let msg = format!("{err}");
1155 assert!(msg.contains("bogus"), "{msg}");
1156 assert!(msg.contains("scroll"), "error should list the valid keywords: {msg}");
1157 }
1158
1159 #[test]
1160 fn parse_layout_overflow_is_ascii_case_sensitive() {
1161 for input in ["SCROLL", "Scroll", "sCrOlL", "AUTO", "Hidden", "VISIBLE", "Clip"] {
1166 assert!(
1167 parse_layout_overflow(input).is_err(),
1168 "{input:?} unexpectedly parsed"
1169 );
1170 }
1171 assert_eq!(parse_layout_overflow("scroll").unwrap(), LayoutOverflow::Scroll);
1172 }
1173
1174 #[test]
1175 fn parse_layout_overflow_trims_unicode_whitespace_but_not_zero_width_chars() {
1176 assert_eq!(
1179 parse_layout_overflow("\u{00A0}scroll\u{00A0}").unwrap(),
1180 LayoutOverflow::Scroll
1181 );
1182 assert_eq!(
1183 parse_layout_overflow("\u{3000}auto").unwrap(),
1184 LayoutOverflow::Auto
1185 );
1186 assert!(parse_layout_overflow("\u{200B}scroll").is_err());
1188 assert!(parse_layout_overflow("scroll\u{FEFF}").is_err());
1189 }
1190
1191 #[test]
1192 fn parse_layout_overflow_rejects_garbage_unicode_and_boundary_numbers() {
1193 for input in [
1194 "none",
1195 "hidden-x",
1196 "auto scroll",
1197 "scroll;",
1198 "scroll garbage",
1199 "visible !important",
1200 "\0",
1201 "scroll\0",
1202 "!@#$%^&*()",
1203 "\u{1F600}",
1204 "scroll\u{1F600}",
1205 "e\u{0301}",
1206 "scroll",
1207 "скролл",
1208 "0",
1209 "-0",
1210 "0.0",
1211 "NaN",
1212 "nan",
1213 "inf",
1214 "-inf",
1215 "infinity",
1216 "9223372036854775807",
1217 "-9223372036854775808",
1218 "1e400",
1219 "1e-400",
1220 ] {
1221 assert!(
1222 parse_layout_overflow(input).is_err(),
1223 "{input:?} unexpectedly parsed"
1224 );
1225 }
1226 }
1227
1228 #[test]
1229 fn parse_layout_overflow_survives_extremely_long_and_deeply_nested_input() {
1230 let long = "scroll".repeat(200_000);
1231 assert!(parse_layout_overflow(&long).is_err());
1232
1233 let junk = "a".repeat(1_000_000);
1234 assert!(parse_layout_overflow(&junk).is_err());
1235
1236 let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
1237 assert!(parse_layout_overflow(&nested).is_err());
1238
1239 let padded = format!("{}scroll{}", " ".repeat(500_000), " ".repeat(500_000));
1241 assert_eq!(parse_layout_overflow(&padded).unwrap(), LayoutOverflow::Scroll);
1242 }
1243
1244 #[test]
1249 fn scrollbar_gutter_round_trips_through_print_as_css_value() {
1250 for g in ALL_GUTTER {
1251 let printed = g.print_as_css_value();
1252 assert_eq!(
1253 parse_style_scrollbar_gutter(&printed).unwrap(),
1254 g,
1255 "{g:?} printed as {printed:?} did not round-trip"
1256 );
1257 }
1258 assert_eq!(
1259 StyleScrollbarGutter::StableBothEdges.print_as_css_value(),
1260 "stable both-edges"
1261 );
1262 assert_eq!(StyleScrollbarGutter::default(), StyleScrollbarGutter::Auto);
1263 }
1264
1265 #[test]
1266 fn parse_style_scrollbar_gutter_matches_the_keyword_string_verbatim() {
1267 assert_eq!(
1272 parse_style_scrollbar_gutter("stable both-edges").unwrap(),
1273 StyleScrollbarGutter::StableBothEdges
1274 );
1275 for rejected in [
1276 "stable both-edges", "stable\tboth-edges",
1278 "stable\nboth-edges",
1279 "both-edges stable", "both-edges",
1281 "STABLE",
1282 "Stable Both-Edges",
1283 "stable both-edges stable",
1284 ] {
1285 assert!(
1286 parse_style_scrollbar_gutter(rejected).is_err(),
1287 "{rejected:?} unexpectedly parsed"
1288 );
1289 }
1290 assert_eq!(
1292 parse_style_scrollbar_gutter(" stable both-edges \n").unwrap(),
1293 StyleScrollbarGutter::StableBothEdges
1294 );
1295 }
1296
1297 #[test]
1298 fn parse_style_scrollbar_gutter_rejects_empty_garbage_unicode_and_numbers() {
1299 for input in [
1300 "", " ", "\t\n", "none", "auto stable", "auto;", "stable;", "0", "-0", "NaN", "inf",
1301 "9223372036854775807", "\u{1F600}", "stable", "stable\0",
1302 ] {
1303 assert!(
1304 parse_style_scrollbar_gutter(input).is_err(),
1305 "{input:?} unexpectedly parsed"
1306 );
1307 }
1308 let err = parse_style_scrollbar_gutter(" nope ").unwrap_err();
1309 assert_eq!(
1310 err,
1311 StyleScrollbarGutterParseError::InvalidValue(" nope ")
1312 );
1313 assert!(format!("{err}").contains("scrollbar-gutter"));
1314 }
1315
1316 #[test]
1317 fn parse_style_scrollbar_gutter_survives_long_and_nested_input() {
1318 let long = "stable ".repeat(200_000);
1319 assert!(parse_style_scrollbar_gutter(&long).is_err());
1320 let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
1321 assert!(parse_style_scrollbar_gutter(&nested).is_err());
1322 }
1323
1324 #[test]
1329 fn parse_style_overflow_clip_margin_accepts_either_component_in_either_order() {
1330 let only_box = parse_style_overflow_clip_margin("content-box").unwrap();
1332 assert_eq!(only_box.clip_edge, VisualBox::ContentBox);
1333 assert_eq!(only_box.inner, PixelValue::default());
1334
1335 let only_len = parse_style_overflow_clip_margin("20px").unwrap();
1337 assert_eq!(only_len.clip_edge, VisualBox::PaddingBox);
1338 assert_eq!(only_len.inner, PixelValue::const_px(20));
1339
1340 let a = parse_style_overflow_clip_margin("border-box 10px").unwrap();
1342 let b = parse_style_overflow_clip_margin("10px border-box").unwrap();
1343 assert_eq!(a, b);
1344 assert_eq!(a.clip_edge, VisualBox::BorderBox);
1345 assert_eq!(a.inner, PixelValue::const_px(10));
1346
1347 let c = parse_style_overflow_clip_margin(" border-box \t\n 10px ").unwrap();
1349 assert_eq!(c, a);
1350
1351 assert_eq!(VisualBox::default(), VisualBox::PaddingBox);
1352 }
1353
1354 #[test]
1355 fn parse_style_overflow_clip_margin_rejects_empty_duplicates_and_garbage() {
1356 for input in [
1357 "",
1358 " ",
1359 "\t\n",
1360 "content-box content-box", "10px 20px", "content-box 10px 20px",
1363 "content-box padding-box",
1364 "content-box 10px border-box",
1365 "none",
1366 "auto",
1367 "margin-box",
1368 "10px;",
1369 "10 px extra",
1370 "px",
1371 "\u{1F600}",
1372 "10\u{1F600}",
1373 "content-box",
1374 "content_box",
1375 "CONTENT-BOX",
1376 ] {
1377 assert!(
1378 parse_style_overflow_clip_margin(input).is_err(),
1379 "{input:?} unexpectedly parsed"
1380 );
1381 }
1382 let err = parse_style_overflow_clip_margin(" nope ").unwrap_err();
1383 assert_eq!(
1384 err,
1385 StyleOverflowClipMarginParseError::InvalidValue(" nope ")
1386 );
1387 assert!(format!("{err}").contains("overflow-clip-margin"));
1388 }
1389
1390 #[test]
1391 fn parse_style_overflow_clip_margin_accepts_out_of_range_lengths() {
1392 let neg = parse_style_overflow_clip_margin("-5px").unwrap();
1396 assert!(neg.inner.number.get() < 0.0);
1397
1398 let pct = parse_style_overflow_clip_margin("50%").unwrap();
1399 assert_eq!(pct.inner.metric, SizeMetric::Percent);
1400 assert_eq!(pct.inner.number.get(), 50.0);
1401
1402 let unitless = parse_style_overflow_clip_margin("7").unwrap();
1404 assert_eq!(unitless.inner.metric, SizeMetric::Px);
1405 assert_eq!(unitless.inner.number.get(), 7.0);
1406 }
1407
1408 #[test]
1409 fn parse_style_overflow_clip_margin_saturates_nan_and_infinity() {
1410 let nan = parse_style_overflow_clip_margin("NaN").unwrap();
1415 assert!(!nan.inner.number.get().is_nan());
1416 assert_eq!(nan.inner.number.get(), 0.0);
1417
1418 let pos_inf = parse_style_overflow_clip_margin("inf").unwrap();
1419 assert!(pos_inf.inner.number.get().is_finite());
1420 assert!(pos_inf.inner.number.get() > 0.0);
1421
1422 let neg_inf = parse_style_overflow_clip_margin("-inf").unwrap();
1423 assert!(neg_inf.inner.number.get().is_finite());
1424 assert!(neg_inf.inner.number.get() < 0.0);
1425
1426 let huge = format!("{}px", "9".repeat(4096));
1429 let huge = parse_style_overflow_clip_margin(&huge).unwrap();
1430 assert!(huge.inner.number.get().is_finite());
1431
1432 let tiny = parse_style_overflow_clip_margin("0.0001px").unwrap();
1434 assert_eq!(tiny.inner.number.get(), 0.0);
1435 }
1436
1437 #[test]
1438 fn parse_style_overflow_clip_margin_survives_long_and_nested_input() {
1439 let long_token = format!("{}px", "a".repeat(1_000_000));
1440 assert!(parse_style_overflow_clip_margin(&long_token).is_err());
1441
1442 let many_tokens = "content-box ".repeat(100_000);
1443 assert!(parse_style_overflow_clip_margin(&many_tokens).is_err());
1444
1445 let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
1446 assert!(parse_style_overflow_clip_margin(&nested).is_err());
1447 }
1448
1449 #[test]
1450 fn overflow_clip_margin_round_trips_through_print_as_css_value() {
1451 let lengths = [
1452 PixelValue::const_px(12),
1453 PixelValue::px(1.5),
1454 PixelValue::const_em(2),
1455 PixelValue::const_percent(50),
1456 PixelValue::px(-3.25),
1457 ];
1458 for edge in ALL_VISUAL_BOX {
1459 for inner in lengths {
1460 let original = StyleOverflowClipMargin {
1461 clip_edge: edge,
1462 inner,
1463 };
1464 let printed = original.print_as_css_value();
1465 let reparsed = parse_style_overflow_clip_margin(&printed).unwrap_or_else(|e| {
1466 panic!("{original:?} printed as {printed:?} but failed to reparse: {e}")
1467 });
1468 assert_eq!(reparsed, original, "round-trip broke via {printed:?}");
1469 }
1470 }
1471 }
1472
1473 #[test]
1474 fn overflow_clip_margin_zero_length_prints_only_the_box_and_forgets_the_unit() {
1475 let zero_em = StyleOverflowClipMargin {
1480 clip_edge: VisualBox::ContentBox,
1481 inner: PixelValue::const_em(0),
1482 };
1483 assert_eq!(zero_em.print_as_css_value(), "content-box");
1484 let back = parse_style_overflow_clip_margin(&zero_em.print_as_css_value()).unwrap();
1485 assert_eq!(back.clip_edge, VisualBox::ContentBox);
1486 assert_eq!(back.inner.number.get(), 0.0);
1487 assert_eq!(back.inner.metric, SizeMetric::Px);
1488 assert_ne!(back, zero_em);
1489
1490 let default = StyleOverflowClipMargin::default();
1492 assert_eq!(default.print_as_css_value(), "padding-box");
1493 assert_eq!(
1494 parse_style_overflow_clip_margin(&default.print_as_css_value()).unwrap(),
1495 default
1496 );
1497
1498 let padding_len = StyleOverflowClipMargin {
1500 clip_edge: VisualBox::PaddingBox,
1501 inner: PixelValue::const_px(4),
1502 };
1503 assert_eq!(padding_len.print_as_css_value(), "4px");
1504 }
1505
1506 #[test]
1507 fn visual_box_round_trips_through_the_clip_margin_parser() {
1508 for v in ALL_VISUAL_BOX {
1509 let printed = v.print_as_css_value();
1510 let parsed = parse_style_overflow_clip_margin(&printed).unwrap();
1511 assert_eq!(parsed.clip_edge, v, "{printed:?} did not round-trip");
1512 }
1513 }
1514
1515 #[test]
1520 fn parse_clip_edge_auto_is_ascii_case_insensitive_and_trimmed() {
1521 for input in ["auto", "AUTO", "Auto", "aUtO", " auto ", "\tauto\n"] {
1522 assert_eq!(
1523 parse_clip_edge(input).unwrap(),
1524 OptionF32::None,
1525 "{input:?} should be auto"
1526 );
1527 }
1528 assert!(parse_clip_edge("auto5").is_err());
1530 assert!(parse_clip_edge("autopx").is_err());
1531 assert!(parse_clip_edge("auto auto").is_err());
1532 }
1533
1534 #[test]
1535 fn parse_clip_edge_silently_discards_the_unit() {
1536 for input in ["5px", "5em", "5rem", "5pt", "5in", "5cm", "5mm", "5vw", "5vh", "5%"] {
1540 assert_eq!(
1541 parse_clip_edge(input).unwrap(),
1542 OptionF32::Some(5.0),
1543 "{input:?} did not collapse to a bare 5.0"
1544 );
1545 }
1546 assert_eq!(parse_clip_edge("5").unwrap(), OptionF32::Some(5.0));
1548 assert_eq!(parse_clip_edge("5 px").unwrap(), OptionF32::Some(5.0));
1550 }
1551
1552 #[test]
1553 fn parse_clip_edge_quantises_to_thousandths_and_normalises_negative_zero() {
1554 assert_eq!(parse_clip_edge("0.001px").unwrap(), OptionF32::Some(0.001));
1557 assert_eq!(parse_clip_edge("0.0001px").unwrap(), OptionF32::Some(0.0));
1558 assert_eq!(parse_clip_edge("-0.0009px").unwrap(), OptionF32::Some(0.0));
1559 assert_eq!(parse_clip_edge("1.9999px").unwrap(), OptionF32::Some(1.999));
1560
1561 let minus_zero = parse_clip_edge("-0px").unwrap().into_option().unwrap();
1563 assert_eq!(minus_zero, 0.0);
1564 assert!(minus_zero.is_sign_positive());
1565
1566 assert_eq!(parse_clip_edge("-10px").unwrap(), OptionF32::Some(-10.0));
1568 }
1569
1570 #[test]
1571 fn parse_clip_edge_saturates_nan_and_infinity_to_finite_values() {
1572 let nan = parse_clip_edge("NaN").unwrap().into_option().unwrap();
1573 assert!(!nan.is_nan(), "NaN must not survive into a clip edge");
1574 assert_eq!(nan, 0.0);
1575
1576 let pos_inf = parse_clip_edge("inf").unwrap().into_option().unwrap();
1577 assert!(pos_inf.is_finite());
1578 assert!(pos_inf > 0.0);
1579
1580 let neg_inf = parse_clip_edge("-infinity").unwrap().into_option().unwrap();
1581 assert!(neg_inf.is_finite());
1582 assert!(neg_inf < 0.0);
1583
1584 let huge = format!("{}px", "9".repeat(4096));
1585 let huge = parse_clip_edge(&huge).unwrap().into_option().unwrap();
1586 assert!(huge.is_finite());
1587 }
1588
1589 #[test]
1590 fn parse_clip_edge_rejects_empty_bare_units_and_garbage() {
1591 for input in [
1592 "",
1593 " ",
1594 "\t\n",
1595 "px",
1596 "em",
1597 "%",
1598 "abc",
1599 "10px;",
1600 "10px 20px",
1601 "(10px)",
1602 "\0",
1603 "\u{1F600}",
1604 "1px",
1605 "1px\u{0301}",
1606 "0x10",
1607 ] {
1608 assert!(parse_clip_edge(input).is_err(), "{input:?} unexpectedly parsed");
1609 }
1610 assert_eq!(
1612 parse_clip_edge(" abc ").unwrap_err(),
1613 StyleClipRectParseError::InvalidValue("abc")
1614 );
1615 }
1616
1617 #[test]
1622 fn clip_rect_round_trips_through_print_as_css_value() {
1623 let rects = [
1624 StyleClipRect::default(),
1625 StyleClipRect {
1626 top: OptionF32::Some(0.0),
1627 right: OptionF32::Some(-2.25),
1628 bottom: OptionF32::Some(1.5),
1629 left: OptionF32::None,
1630 },
1631 StyleClipRect {
1632 top: OptionF32::Some(10.0),
1633 right: OptionF32::Some(20.0),
1634 bottom: OptionF32::Some(30.0),
1635 left: OptionF32::Some(40.0),
1636 },
1637 StyleClipRect {
1638 top: OptionF32::None,
1639 right: OptionF32::Some(-1.0),
1640 bottom: OptionF32::None,
1641 left: OptionF32::Some(-1.0),
1642 },
1643 ];
1644 for original in rects {
1645 let printed = original.print_as_css_value();
1646 let reparsed = parse_clip_rect(&printed).unwrap_or_else(|e| {
1647 panic!("{original:?} printed as {printed:?} but failed to reparse: {e}")
1648 });
1649 assert_eq!(reparsed, original, "round-trip broke via {printed:?}");
1650 }
1651 assert_eq!(
1652 StyleClipRect::default().print_as_css_value(),
1653 "rect(auto, auto, auto, auto)"
1654 );
1655 }
1656
1657 #[test]
1658 fn parse_clip_rect_accepts_the_auto_comma_and_legacy_space_forms() {
1659 let all_auto = StyleClipRect::default();
1660 for input in [
1661 "auto",
1662 "AUTO",
1663 " auto ",
1664 "\u{00A0}auto", "rect(auto, auto, auto, auto)",
1666 "rect(auto auto auto auto)",
1667 "RECT(auto, auto, auto, auto)",
1668 " rect( auto , auto , auto , auto ) ",
1669 ] {
1670 assert_eq!(
1671 parse_clip_rect(input).unwrap(),
1672 all_auto,
1673 "{input:?} should be all-auto"
1674 );
1675 }
1676
1677 let mixed = parse_clip_rect("rect(1px, auto, -3px, 4px)").unwrap();
1678 assert_eq!(mixed.top, OptionF32::Some(1.0));
1679 assert_eq!(mixed.right, OptionF32::None);
1680 assert_eq!(mixed.bottom, OptionF32::Some(-3.0));
1681 assert_eq!(mixed.left, OptionF32::Some(4.0));
1682
1683 assert_eq!(
1685 parse_clip_rect("rect(1px,2px,3px,4px)").unwrap(),
1686 StyleClipRect {
1687 top: OptionF32::Some(1.0),
1688 right: OptionF32::Some(2.0),
1689 bottom: OptionF32::Some(3.0),
1690 left: OptionF32::Some(4.0),
1691 }
1692 );
1693 }
1694
1695 #[test]
1696 fn parse_clip_rect_rejects_wrong_arity_mixed_separators_and_trailing_junk() {
1697 for input in [
1698 "rect()",
1699 "rect(,,,)",
1700 "rect(1px)",
1701 "rect(1px, 2px, 3px)",
1702 "rect(1px, 2px, 3px, 4px, 5px)",
1703 "rect(1px, 2px, 3px, 4px,)",
1704 "rect(1px 2px, 3px 4px)", "rect(1px 2px 3px)",
1706 "rect(1px 2px 3px 4px 5px)",
1707 "rect(1px, 2px, 3px, 4px", "rect 1px, 2px, 3px, 4px)", "rect (1px, 2px, 3px, 4px)", "rect(1px, 2px, 3px, 4px) trailing",
1711 "rect(1px, 2px, 3px, 4px);",
1712 "junk rect(1px, 2px, 3px, 4px)",
1713 "rect(auto, auto, auto, abc)",
1714 "",
1715 " ",
1716 "none",
1717 "inherit",
1718 "0",
1719 ] {
1720 assert!(parse_clip_rect(input).is_err(), "{input:?} unexpectedly parsed");
1721 }
1722 }
1723
1724 #[test]
1725 fn parse_clip_rect_function_name_accepts_only_all_lower_or_all_upper_case() {
1726 assert!(parse_clip_rect("rect(auto, auto, auto, auto)").is_ok());
1729 assert!(parse_clip_rect("RECT(auto, auto, auto, auto)").is_ok());
1730 for input in [
1731 "Rect(auto, auto, auto, auto)",
1732 "rECT(auto, auto, auto, auto)",
1733 "ReCt(auto, auto, auto, auto)",
1734 ] {
1735 assert!(parse_clip_rect(input).is_err(), "{input:?} unexpectedly parsed");
1736 }
1737 }
1738
1739 #[test]
1740 fn parse_clip_rect_errors_point_at_the_offending_token() {
1741 let err = parse_clip_rect("rect(1px, abc, 3px, 4px)").unwrap_err();
1743 assert_eq!(err, StyleClipRectParseError::InvalidValue("abc"));
1744 let msg = format!("{err}");
1745 assert!(msg.contains("abc"), "{msg}");
1746 assert!(!msg.contains("1px"), "message leaked the whole input: {msg}");
1749
1750 let err = parse_clip_rect(" rect(1px) ").unwrap_err();
1752 assert_eq!(err, StyleClipRectParseError::InvalidValue(" rect(1px) "));
1753 }
1754
1755 #[test]
1756 fn parse_clip_rect_survives_deep_nesting_and_huge_input() {
1757 let nested = format!("{}{}", "rect(".repeat(10_000), ")".repeat(10_000));
1759 assert!(parse_clip_rect(&nested).is_err());
1760
1761 let parens = format!("{}{}", "(".repeat(100_000), ")".repeat(100_000));
1762 assert!(parse_clip_rect(&parens).is_err());
1763
1764 let wide = format!("rect({})", "1px,".repeat(50_000));
1766 assert!(parse_clip_rect(&wide).is_err());
1767
1768 let long_token = format!("rect({}, auto, auto, auto)", "a".repeat(1_000_000));
1769 assert!(parse_clip_rect(&long_token).is_err());
1770
1771 let huge = format!("rect({}px, auto, auto, auto)", "9".repeat(4096));
1773 let huge = parse_clip_rect(&huge).unwrap();
1774 let top = huge.top.into_option().unwrap();
1775 assert!(top.is_finite());
1776 assert!(top > 0.0);
1777 }
1778
1779 #[test]
1780 fn parse_clip_rect_does_not_panic_on_multibyte_input() {
1781 for input in [
1782 "rect(\u{1F600}, \u{1F600}, \u{1F600}, \u{1F600})",
1783 "rect(1px\u{0301}, auto, auto, auto)",
1784 "réct(1px, 2px, 3px, 4px)",
1785 "rect(1px, auto, auto, auto)", "rect(1px, auto, auto, auto\u{200B})",
1787 "\u{1F600}",
1788 "автo",
1789 "rect(٣px, auto, auto, auto)", ] {
1791 assert!(parse_clip_rect(input).is_err(), "{input:?} unexpectedly parsed");
1792 }
1793 }
1794
1795 #[test]
1800 fn clip_rect_default_is_all_auto() {
1801 let d = StyleClipRect::default();
1802 assert_eq!(d.top, OptionF32::None);
1803 assert_eq!(d.right, OptionF32::None);
1804 assert_eq!(d.bottom, OptionF32::None);
1805 assert_eq!(d.left, OptionF32::None);
1806 }
1807
1808 #[test]
1809 fn clip_rect_resolve_expands_auto_edges_to_the_border_box() {
1810 let (top, right, bottom, left) = StyleClipRect::default().resolve(
1812 100.0, 50.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, );
1816 assert_eq!(top, 0.0);
1817 assert_eq!(left, 0.0);
1818 assert_eq!(right, 100.0 + 1.0 + 2.0 + 5.0 + 6.0);
1819 assert_eq!(bottom, 50.0 + 3.0 + 4.0 + 7.0 + 8.0);
1820 }
1821
1822 #[test]
1823 fn clip_rect_resolve_at_zero_and_with_negative_geometry() {
1824 let all_zero = StyleClipRect::default().resolve(
1825 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
1826 );
1827 assert_eq!(all_zero, (0.0, 0.0, 0.0, 0.0));
1828
1829 let (top, right, bottom, left) = StyleClipRect::default().resolve(
1831 -10.0, -20.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0,
1832 );
1833 assert_eq!(top, 0.0);
1834 assert_eq!(left, 0.0);
1835 assert_eq!(right, -14.0);
1836 assert_eq!(bottom, -24.0);
1837 }
1838
1839 #[test]
1840 fn clip_rect_resolve_ignores_the_geometry_for_explicit_edges() {
1841 let explicit = StyleClipRect {
1842 top: OptionF32::Some(1.0),
1843 right: OptionF32::Some(2.0),
1844 bottom: OptionF32::Some(3.0),
1845 left: OptionF32::Some(4.0),
1846 };
1847 for geometry in [
1849 f32::NAN,
1850 f32::INFINITY,
1851 f32::NEG_INFINITY,
1852 f32::MAX,
1853 f32::MIN,
1854 f32::MIN_POSITIVE,
1855 ] {
1856 let resolved = explicit.resolve(
1857 geometry, geometry, geometry, geometry, geometry, geometry, geometry, geometry,
1858 geometry, geometry,
1859 );
1860 assert_eq!(
1861 resolved,
1862 (1.0, 2.0, 3.0, 4.0),
1863 "explicit edges were perturbed by geometry {geometry:?}"
1864 );
1865 }
1866 }
1867
1868 #[test]
1869 fn clip_rect_resolve_saturates_at_f32_max_and_keeps_nan_contained() {
1870 let (top, right, bottom, left) = StyleClipRect::default().resolve(
1872 f32::MAX,
1873 f32::MAX,
1874 f32::MAX,
1875 f32::MAX,
1876 f32::MAX,
1877 f32::MAX,
1878 f32::MAX,
1879 f32::MAX,
1880 f32::MAX,
1881 f32::MAX,
1882 );
1883 assert_eq!(top, 0.0);
1884 assert_eq!(left, 0.0);
1885 assert!(right.is_infinite() && right.is_sign_positive());
1886 assert!(bottom.is_infinite() && bottom.is_sign_positive());
1887
1888 let (top, right, bottom, left) = StyleClipRect::default().resolve(
1891 f32::NAN,
1892 f32::NAN,
1893 0.0,
1894 0.0,
1895 0.0,
1896 0.0,
1897 0.0,
1898 0.0,
1899 0.0,
1900 0.0,
1901 );
1902 assert_eq!(top, 0.0);
1903 assert_eq!(left, 0.0);
1904 assert!(right.is_nan());
1905 assert!(bottom.is_nan());
1906
1907 let (_, right, bottom, _) = StyleClipRect::default().resolve(
1909 f32::INFINITY,
1910 f32::INFINITY,
1911 f32::NEG_INFINITY,
1912 0.0,
1913 f32::NEG_INFINITY,
1914 0.0,
1915 0.0,
1916 0.0,
1917 0.0,
1918 0.0,
1919 );
1920 assert!(right.is_nan());
1921 assert!(bottom.is_nan());
1922 }
1923
1924 fn error_payloads() -> Vec<String> {
1931 vec![
1932 String::new(),
1933 String::from(" "),
1934 String::from("bogus"),
1935 String::from("\u{1F600}\u{0301}"),
1936 String::from("a\0b"),
1937 String::from("rect(1px, 2px, 3px, 4px)"),
1938 "x".repeat(100_000),
1939 ]
1940 }
1941
1942 macro_rules! assert_error_round_trips {
1943 ($borrowed:ident) => {{
1944 for payload in error_payloads() {
1945 let borrowed = $borrowed::InvalidValue(payload.as_str());
1946 let owned = borrowed.to_contained();
1947 let back = owned.to_shared();
1948 assert_eq!(
1949 back, borrowed,
1950 "{}::InvalidValue({payload:?}) lost data on to_contained/to_shared",
1951 stringify!($borrowed)
1952 );
1953 assert_eq!(owned.to_shared().to_contained(), owned);
1955 }
1956 }};
1957 }
1958
1959 #[test]
1960 fn parse_errors_round_trip_between_borrowed_and_owned_forms() {
1961 assert_error_round_trips!(LayoutOverflowParseError);
1962 assert_error_round_trips!(StyleScrollbarGutterParseError);
1963 assert_error_round_trips!(StyleOverflowClipMarginParseError);
1964 assert_error_round_trips!(StyleClipRectParseError);
1965 }
1966
1967 #[test]
1968 fn parse_errors_produced_by_the_parsers_round_trip_too() {
1969 let e = parse_layout_overflow("nope").unwrap_err();
1970 assert_eq!(e.to_contained().to_shared(), e);
1971
1972 let e = parse_style_scrollbar_gutter("nope").unwrap_err();
1973 assert_eq!(e.to_contained().to_shared(), e);
1974
1975 let e = parse_style_overflow_clip_margin("nope nope").unwrap_err();
1976 assert_eq!(e.to_contained().to_shared(), e);
1977
1978 let e = parse_clip_rect("rect(nope)").unwrap_err();
1979 assert_eq!(e.to_contained().to_shared(), e);
1980 }
1981
1982 #[test]
1983 fn parse_error_messages_name_the_property_and_quote_the_value() {
1984 let msg = format!("{}", LayoutOverflowParseError::InvalidValue("zzz"));
1985 assert!(msg.contains("overflow") && msg.contains("zzz"), "{msg}");
1986
1987 let msg = format!("{}", StyleScrollbarGutterParseError::InvalidValue("zzz"));
1988 assert!(msg.contains("scrollbar-gutter") && msg.contains("zzz"), "{msg}");
1989
1990 let msg = format!("{}", StyleOverflowClipMarginParseError::InvalidValue("zzz"));
1991 assert!(
1992 msg.contains("overflow-clip-margin") && msg.contains("zzz"),
1993 "{msg}"
1994 );
1995
1996 let msg = format!("{}", StyleClipRectParseError::InvalidValue("zzz"));
1997 assert!(msg.contains("clip") && msg.contains("zzz"), "{msg}");
1998
1999 let weird = StyleClipRectParseError::InvalidValue("\u{1F600}\0\u{0301}");
2001 assert!(!format!("{weird:?}").is_empty());
2002 }
2003}