1use alloc::string::{String, ToString};
4use crate::corety::AzString;
5
6use crate::props::{
7 basic::{
8 color::{parse_css_color, ColorU, CssColorParseError, CssColorParseErrorOwned},
9 },
10 formatter::PrintAsCssValue,
11 layout::{
12 dimensions::LayoutWidth,
13 spacing::{LayoutPaddingLeft, LayoutPaddingRight},
14 },
15 style::background::StyleBackgroundContent,
16};
17
18#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
25#[repr(C)]
26pub enum ScrollBehavior {
27 #[default]
29 Auto,
30 Smooth,
32}
33
34impl PrintAsCssValue for ScrollBehavior {
35 fn print_as_css_value(&self) -> String {
36 match self {
37 Self::Auto => "auto".to_string(),
38 Self::Smooth => "smooth".to_string(),
39 }
40 }
41}
42
43#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
46#[repr(C)]
47pub enum OverscrollBehavior {
48 #[default]
50 Auto,
51 Contain,
53 None,
55}
56
57impl PrintAsCssValue for OverscrollBehavior {
58 fn print_as_css_value(&self) -> String {
59 match self {
60 Self::Auto => "auto".to_string(),
61 Self::Contain => "contain".to_string(),
62 Self::None => "none".to_string(),
63 }
64 }
65}
66
67#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
76#[repr(C)]
77pub struct ScrollPhysics {
78 pub smooth_scroll_duration_ms: u32,
81
82 pub deceleration_rate: f32,
86
87 pub min_velocity_threshold: f32,
90
91 pub max_velocity: f32,
93
94 pub wheel_multiplier: f32,
97
98 pub invert_direction: bool,
100
101 pub overscroll_elasticity: f32,
104
105 pub max_overscroll_distance: f32,
108
109 pub bounce_back_duration_ms: u32,
112
113 pub timer_interval_ms: u32,
117
118 pub wheel_animate_bounce_ms: u32,
124}
125
126impl Default for ScrollPhysics {
127 fn default() -> Self {
128 Self {
129 smooth_scroll_duration_ms: 300,
130 deceleration_rate: 0.95,
131 min_velocity_threshold: 50.0,
132 max_velocity: 8000.0,
133 wheel_multiplier: 1.0,
134 invert_direction: false,
135 overscroll_elasticity: 0.0, max_overscroll_distance: 100.0,
137 bounce_back_duration_ms: 400,
138 timer_interval_ms: 16,
139 wheel_animate_bounce_ms: 120,
140 }
141 }
142}
143
144impl ScrollPhysics {
145 #[must_use] pub const fn ios() -> Self {
147 Self {
148 smooth_scroll_duration_ms: 300,
149 deceleration_rate: 0.998,
150 min_velocity_threshold: 20.0,
151 max_velocity: 8000.0,
152 wheel_multiplier: 1.0,
153 invert_direction: true, overscroll_elasticity: 0.5,
155 max_overscroll_distance: 120.0,
156 bounce_back_duration_ms: 500,
157 timer_interval_ms: 16,
158 wheel_animate_bounce_ms: 120,
159 }
160 }
161
162 #[must_use] pub const fn macos() -> Self {
164 Self {
165 smooth_scroll_duration_ms: 250,
166 deceleration_rate: 0.997,
167 min_velocity_threshold: 30.0,
168 max_velocity: 6000.0,
169 wheel_multiplier: 1.0,
170 invert_direction: true, overscroll_elasticity: 0.3,
172 max_overscroll_distance: 80.0,
173 bounce_back_duration_ms: 400,
174 timer_interval_ms: 16,
175 wheel_animate_bounce_ms: 120,
176 }
177 }
178
179 #[must_use] pub const fn windows() -> Self {
181 Self {
182 smooth_scroll_duration_ms: 200,
183 deceleration_rate: 0.9,
184 min_velocity_threshold: 100.0,
185 max_velocity: 4000.0,
186 wheel_multiplier: 1.0,
187 invert_direction: false,
188 overscroll_elasticity: 0.0,
189 max_overscroll_distance: 0.0,
190 bounce_back_duration_ms: 200,
191 timer_interval_ms: 16,
192 wheel_animate_bounce_ms: 120,
193 }
194 }
195
196 #[must_use] pub const fn android() -> Self {
198 Self {
199 smooth_scroll_duration_ms: 250,
200 deceleration_rate: 0.996,
201 min_velocity_threshold: 40.0,
202 max_velocity: 8000.0,
203 wheel_multiplier: 1.0,
204 invert_direction: false,
205 overscroll_elasticity: 0.2, max_overscroll_distance: 60.0,
207 bounce_back_duration_ms: 300,
208 timer_interval_ms: 16,
209 wheel_animate_bounce_ms: 120,
210 }
211 }
212}
213
214impl_option!(
215 ScrollPhysics,
216 OptionScrollPhysics,
217 [Debug, Copy, Clone, PartialEq, PartialOrd]
218);
219
220#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
238#[repr(C)]
239pub enum ScrollbarVisibilityMode {
240 #[default]
243 Always,
244 WhenScrolling,
247 Auto,
249}
250
251impl PrintAsCssValue for ScrollbarVisibilityMode {
252 fn print_as_css_value(&self) -> String {
253 match self {
254 Self::Always => "always".to_string(),
255 Self::WhenScrolling => "when-scrolling".to_string(),
256 Self::Auto => "auto".to_string(),
257 }
258 }
259}
260
261#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
272#[repr(C)]
273pub struct ScrollbarFadeDelay {
274 pub ms: u32,
276}
277
278impl ScrollbarFadeDelay {
279 #[must_use] pub const fn new(ms: u32) -> Self { Self { ms } }
280 pub const ZERO: Self = Self { ms: 0 };
281}
282
283impl PrintAsCssValue for ScrollbarFadeDelay {
284 fn print_as_css_value(&self) -> String {
285 if self.ms == 0 { "0".to_string() } else { format!("{}ms", self.ms) }
286 }
287}
288
289#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
300#[repr(C)]
301pub struct ScrollbarFadeDuration {
302 pub ms: u32,
304}
305
306impl ScrollbarFadeDuration {
307 #[must_use] pub const fn new(ms: u32) -> Self { Self { ms } }
308 pub const ZERO: Self = Self { ms: 0 };
309}
310
311impl PrintAsCssValue for ScrollbarFadeDuration {
312 fn print_as_css_value(&self) -> String {
313 if self.ms == 0 { "0".to_string() } else { format!("{}ms", self.ms) }
314 }
315}
316
317#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
331#[repr(C)]
332pub enum OverflowScrolling {
333 #[default]
335 Auto,
336 Touch,
338}
339
340impl PrintAsCssValue for OverflowScrolling {
341 fn print_as_css_value(&self) -> String {
342 match self {
343 Self::Auto => "auto".to_string(),
344 Self::Touch => "touch".to_string(),
345 }
346 }
347}
348
349#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
355#[repr(C)]
356#[derive(Default)]
357pub enum LayoutScrollbarWidth {
358 #[default]
359 Auto,
360 Thin,
361 None,
362}
363
364
365impl PrintAsCssValue for LayoutScrollbarWidth {
366 fn print_as_css_value(&self) -> String {
367 match self {
368 Self::Auto => "auto".to_string(),
369 Self::Thin => "thin".to_string(),
370 Self::None => "none".to_string(),
371 }
372 }
373}
374
375#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
377#[repr(C)]
378pub struct ScrollbarColorCustom {
379 pub thumb: ColorU,
380 pub track: ColorU,
381}
382
383#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
385#[repr(C, u8)]
386#[derive(Default)]
387pub enum StyleScrollbarColor {
388 #[default]
389 Auto,
390 Custom(ScrollbarColorCustom),
391}
392
393
394impl PrintAsCssValue for StyleScrollbarColor {
395 fn print_as_css_value(&self) -> String {
396 match self {
397 Self::Auto => "auto".to_string(),
398 Self::Custom(c) => format!("{} {}", c.thumb.to_hash(), c.track.to_hash()),
399 }
400 }
401}
402
403#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
407#[repr(C)]
408pub struct ScrollbarInfo {
409 pub width: LayoutWidth,
411 pub padding_left: LayoutPaddingLeft,
414 pub padding_right: LayoutPaddingRight,
416 pub track: StyleBackgroundContent,
420 pub thumb: StyleBackgroundContent,
422 pub button: StyleBackgroundContent,
424 pub corner: StyleBackgroundContent,
427 pub resizer: StyleBackgroundContent,
430 pub clip_to_container_border: bool,
435 pub scroll_behavior: ScrollBehavior,
437 pub overscroll_behavior_x: OverscrollBehavior,
439 pub overscroll_behavior_y: OverscrollBehavior,
441 pub overflow_scrolling: OverflowScrolling,
444}
445
446impl Default for ScrollbarInfo {
447 fn default() -> Self {
448 SCROLLBAR_CLASSIC_LIGHT
449 }
450}
451
452impl PrintAsCssValue for ScrollbarInfo {
453 fn print_as_css_value(&self) -> String {
454 format!(
456 "width: {}; padding-left: {}; padding-right: {}; track: {}; thumb: {}; button: {}; \
457 corner: {}; resizer: {}",
458 self.width.print_as_css_value(),
459 self.padding_left.print_as_css_value(),
460 self.padding_right.print_as_css_value(),
461 self.track.print_as_css_value(),
462 self.thumb.print_as_css_value(),
463 self.button.print_as_css_value(),
464 self.corner.print_as_css_value(),
465 self.resizer.print_as_css_value(),
466 )
467 }
468}
469
470#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
472#[repr(C)]
473pub struct ScrollbarStyle {
474 pub horizontal: ScrollbarInfo,
476 pub vertical: ScrollbarInfo,
478}
479
480impl PrintAsCssValue for ScrollbarStyle {
481 fn print_as_css_value(&self) -> String {
482 format!(
484 "horz({}), vert({})",
485 self.horizontal.print_as_css_value(),
486 self.vertical.print_as_css_value()
487 )
488 }
489}
490
491impl crate::codegen::format::FormatAsRustCode for ScrollbarStyle {
493 fn format_as_rust_code(&self, tabs: usize) -> String {
494 let t = String::from(" ").repeat(tabs);
495 let t1 = String::from(" ").repeat(tabs + 1);
496 format!(
497 "ScrollbarStyle {{\r\n{}horizontal: {},\r\n{}vertical: {},\r\n{}}}",
498 t1,
499 crate::codegen::format::format_scrollbar_info(&self.horizontal, tabs + 1),
500 t1,
501 crate::codegen::format::format_scrollbar_info(&self.vertical, tabs + 1),
502 t,
503 )
504 }
505}
506
507impl crate::codegen::format::FormatAsRustCode for LayoutScrollbarWidth {
508 fn format_as_rust_code(&self, _tabs: usize) -> String {
509 match self {
510 Self::Auto => String::from("LayoutScrollbarWidth::Auto"),
511 Self::Thin => String::from("LayoutScrollbarWidth::Thin"),
512 Self::None => String::from("LayoutScrollbarWidth::None"),
513 }
514 }
515}
516
517impl crate::codegen::format::FormatAsRustCode for StyleScrollbarColor {
518 fn format_as_rust_code(&self, _tabs: usize) -> String {
519 match self {
520 Self::Auto => String::from("StyleScrollbarColor::Auto"),
521 Self::Custom(c) => format!(
522 "StyleScrollbarColor::Custom(ScrollbarColorCustom {{ thumb: {}, track: {} }})",
523 crate::codegen::format::format_color_value(&c.thumb),
524 crate::codegen::format::format_color_value(&c.track)
525 ),
526 }
527 }
528}
529
530impl crate::codegen::format::FormatAsRustCode for ScrollbarVisibilityMode {
531 fn format_as_rust_code(&self, _tabs: usize) -> String {
532 match self {
533 Self::Always => String::from("ScrollbarVisibilityMode::Always"),
534 Self::WhenScrolling => String::from("ScrollbarVisibilityMode::WhenScrolling"),
535 Self::Auto => String::from("ScrollbarVisibilityMode::Auto"),
536 }
537 }
538}
539
540impl crate::codegen::format::FormatAsRustCode for ScrollbarFadeDelay {
541 fn format_as_rust_code(&self, _tabs: usize) -> String {
542 format!("ScrollbarFadeDelay::new({})", self.ms)
543 }
544}
545
546impl crate::codegen::format::FormatAsRustCode for ScrollbarFadeDuration {
547 fn format_as_rust_code(&self, _tabs: usize) -> String {
548 format!("ScrollbarFadeDuration::new({})", self.ms)
549 }
550}
551
552#[derive(Debug, Clone, PartialEq, Eq)]
557pub struct ComputedScrollbarStyle {
558 pub width: Option<LayoutWidth>,
560 pub thumb_color: Option<ColorU>,
562 pub track_color: Option<ColorU>,
564}
565
566impl Default for ComputedScrollbarStyle {
567 fn default() -> Self {
568 let default_info = ScrollbarInfo::default();
569 Self {
570 width: Some(default_info.width), thumb_color: match default_info.thumb {
572 StyleBackgroundContent::Color(c) => Some(c),
573 _ => None,
574 },
575 track_color: match default_info.track {
576 StyleBackgroundContent::Color(c) => Some(c),
577 _ => None,
578 },
579 }
580 }
581}
582
583pub const SCROLLBAR_CLASSIC_LIGHT: ScrollbarInfo = ScrollbarInfo {
587 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(17)),
588 padding_left: LayoutPaddingLeft {
589 inner: crate::props::basic::pixel::PixelValue::const_px(2),
590 },
591 padding_right: LayoutPaddingRight {
592 inner: crate::props::basic::pixel::PixelValue::const_px(2),
593 },
594 track: StyleBackgroundContent::Color(ColorU {
595 r: 241,
596 g: 241,
597 b: 241,
598 a: 255,
599 }),
600 thumb: StyleBackgroundContent::Color(ColorU {
601 r: 193,
602 g: 193,
603 b: 193,
604 a: 255,
605 }),
606 button: StyleBackgroundContent::Color(ColorU {
607 r: 163,
608 g: 163,
609 b: 163,
610 a: 255,
611 }),
612 corner: StyleBackgroundContent::Color(ColorU {
613 r: 241,
614 g: 241,
615 b: 241,
616 a: 255,
617 }),
618 resizer: StyleBackgroundContent::Color(ColorU {
619 r: 241,
620 g: 241,
621 b: 241,
622 a: 255,
623 }),
624 clip_to_container_border: false,
625 scroll_behavior: ScrollBehavior::Auto,
626 overscroll_behavior_x: OverscrollBehavior::Auto,
627 overscroll_behavior_y: OverscrollBehavior::Auto,
628 overflow_scrolling: OverflowScrolling::Auto,
629};
630
631pub const SCROLLBAR_CLASSIC_DARK: ScrollbarInfo = ScrollbarInfo {
633 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(17)),
634 padding_left: LayoutPaddingLeft {
635 inner: crate::props::basic::pixel::PixelValue::const_px(2),
636 },
637 padding_right: LayoutPaddingRight {
638 inner: crate::props::basic::pixel::PixelValue::const_px(2),
639 },
640 track: StyleBackgroundContent::Color(ColorU {
641 r: 45,
642 g: 45,
643 b: 45,
644 a: 255,
645 }),
646 thumb: StyleBackgroundContent::Color(ColorU {
647 r: 100,
648 g: 100,
649 b: 100,
650 a: 255,
651 }),
652 button: StyleBackgroundContent::Color(ColorU {
653 r: 120,
654 g: 120,
655 b: 120,
656 a: 255,
657 }),
658 corner: StyleBackgroundContent::Color(ColorU {
659 r: 45,
660 g: 45,
661 b: 45,
662 a: 255,
663 }),
664 resizer: StyleBackgroundContent::Color(ColorU {
665 r: 45,
666 g: 45,
667 b: 45,
668 a: 255,
669 }),
670 clip_to_container_border: false,
671 scroll_behavior: ScrollBehavior::Auto,
672 overscroll_behavior_x: OverscrollBehavior::Auto,
673 overscroll_behavior_y: OverscrollBehavior::Auto,
674 overflow_scrolling: OverflowScrolling::Auto,
675};
676
677pub const SCROLLBAR_MACOS_LIGHT: ScrollbarInfo = ScrollbarInfo {
679 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(8)),
680 padding_left: LayoutPaddingLeft {
681 inner: crate::props::basic::pixel::PixelValue::const_px(0),
682 },
683 padding_right: LayoutPaddingRight {
684 inner: crate::props::basic::pixel::PixelValue::const_px(0),
685 },
686 track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
687 thumb: StyleBackgroundContent::Color(ColorU {
688 r: 0,
689 g: 0,
690 b: 0,
691 a: 100,
692 }), button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
694 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
695 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
696 clip_to_container_border: true, scroll_behavior: ScrollBehavior::Smooth,
698 overscroll_behavior_x: OverscrollBehavior::Auto,
699 overscroll_behavior_y: OverscrollBehavior::Auto,
700 overflow_scrolling: OverflowScrolling::Auto,
701};
702
703pub const SCROLLBAR_MACOS_DARK: ScrollbarInfo = ScrollbarInfo {
705 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(8)),
706 padding_left: LayoutPaddingLeft {
707 inner: crate::props::basic::pixel::PixelValue::const_px(0),
708 },
709 padding_right: LayoutPaddingRight {
710 inner: crate::props::basic::pixel::PixelValue::const_px(0),
711 },
712 track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
713 thumb: StyleBackgroundContent::Color(ColorU {
714 r: 255,
715 g: 255,
716 b: 255,
717 a: 100,
718 }), button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
720 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
721 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
722 clip_to_container_border: true, scroll_behavior: ScrollBehavior::Smooth,
724 overscroll_behavior_x: OverscrollBehavior::Auto,
725 overscroll_behavior_y: OverscrollBehavior::Auto,
726 overflow_scrolling: OverflowScrolling::Auto,
727};
728
729pub const SCROLLBAR_WINDOWS_LIGHT: ScrollbarInfo = ScrollbarInfo {
731 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(12)),
732 padding_left: LayoutPaddingLeft {
733 inner: crate::props::basic::pixel::PixelValue::const_px(0),
734 },
735 padding_right: LayoutPaddingRight {
736 inner: crate::props::basic::pixel::PixelValue::const_px(0),
737 },
738 track: StyleBackgroundContent::Color(ColorU {
739 r: 241,
740 g: 241,
741 b: 241,
742 a: 255,
743 }),
744 thumb: StyleBackgroundContent::Color(ColorU {
745 r: 130,
746 g: 130,
747 b: 130,
748 a: 255,
749 }),
750 button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
751 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
752 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
753 clip_to_container_border: false,
754 scroll_behavior: ScrollBehavior::Auto,
755 overscroll_behavior_x: OverscrollBehavior::None,
756 overscroll_behavior_y: OverscrollBehavior::None,
757 overflow_scrolling: OverflowScrolling::Auto,
758};
759
760pub const SCROLLBAR_WINDOWS_DARK: ScrollbarInfo = ScrollbarInfo {
762 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(12)),
763 padding_left: LayoutPaddingLeft {
764 inner: crate::props::basic::pixel::PixelValue::const_px(0),
765 },
766 padding_right: LayoutPaddingRight {
767 inner: crate::props::basic::pixel::PixelValue::const_px(0),
768 },
769 track: StyleBackgroundContent::Color(ColorU {
770 r: 32,
771 g: 32,
772 b: 32,
773 a: 255,
774 }),
775 thumb: StyleBackgroundContent::Color(ColorU {
776 r: 110,
777 g: 110,
778 b: 110,
779 a: 255,
780 }),
781 button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
782 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
783 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
784 clip_to_container_border: false,
785 scroll_behavior: ScrollBehavior::Auto,
786 overscroll_behavior_x: OverscrollBehavior::None,
787 overscroll_behavior_y: OverscrollBehavior::None,
788 overflow_scrolling: OverflowScrolling::Auto,
789};
790
791pub const SCROLLBAR_IOS_LIGHT: ScrollbarInfo = ScrollbarInfo {
793 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(7)),
794 padding_left: LayoutPaddingLeft {
795 inner: crate::props::basic::pixel::PixelValue::const_px(0),
796 },
797 padding_right: LayoutPaddingRight {
798 inner: crate::props::basic::pixel::PixelValue::const_px(0),
799 },
800 track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
801 thumb: StyleBackgroundContent::Color(ColorU {
802 r: 0,
803 g: 0,
804 b: 0,
805 a: 102,
806 }), button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
808 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
809 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
810 clip_to_container_border: true, scroll_behavior: ScrollBehavior::Smooth,
812 overscroll_behavior_x: OverscrollBehavior::Auto,
813 overscroll_behavior_y: OverscrollBehavior::Auto,
814 overflow_scrolling: OverflowScrolling::Auto,
815};
816
817pub const SCROLLBAR_IOS_DARK: ScrollbarInfo = ScrollbarInfo {
819 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(7)),
820 padding_left: LayoutPaddingLeft {
821 inner: crate::props::basic::pixel::PixelValue::const_px(0),
822 },
823 padding_right: LayoutPaddingRight {
824 inner: crate::props::basic::pixel::PixelValue::const_px(0),
825 },
826 track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
827 thumb: StyleBackgroundContent::Color(ColorU {
828 r: 255,
829 g: 255,
830 b: 255,
831 a: 102,
832 }), button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
834 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
835 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
836 clip_to_container_border: true, scroll_behavior: ScrollBehavior::Smooth,
838 overscroll_behavior_x: OverscrollBehavior::Auto,
839 overscroll_behavior_y: OverscrollBehavior::Auto,
840 overflow_scrolling: OverflowScrolling::Auto,
841};
842
843pub const SCROLLBAR_ANDROID_LIGHT: ScrollbarInfo = ScrollbarInfo {
845 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(6)),
846 padding_left: LayoutPaddingLeft {
847 inner: crate::props::basic::pixel::PixelValue::const_px(0),
848 },
849 padding_right: LayoutPaddingRight {
850 inner: crate::props::basic::pixel::PixelValue::const_px(0),
851 },
852 track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
853 thumb: StyleBackgroundContent::Color(ColorU {
854 r: 0,
855 g: 0,
856 b: 0,
857 a: 102,
858 }), button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
860 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
861 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
862 clip_to_container_border: true, scroll_behavior: ScrollBehavior::Smooth,
864 overscroll_behavior_x: OverscrollBehavior::Contain,
865 overscroll_behavior_y: OverscrollBehavior::Auto,
866 overflow_scrolling: OverflowScrolling::Auto,
867};
868
869pub const SCROLLBAR_ANDROID_DARK: ScrollbarInfo = ScrollbarInfo {
871 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(6)),
872 padding_left: LayoutPaddingLeft {
873 inner: crate::props::basic::pixel::PixelValue::const_px(0),
874 },
875 padding_right: LayoutPaddingRight {
876 inner: crate::props::basic::pixel::PixelValue::const_px(0),
877 },
878 track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
879 thumb: StyleBackgroundContent::Color(ColorU {
880 r: 255,
881 g: 255,
882 b: 255,
883 a: 102,
884 }), button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
886 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
887 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
888 clip_to_container_border: true, scroll_behavior: ScrollBehavior::Smooth,
890 overscroll_behavior_x: OverscrollBehavior::Contain,
891 overscroll_behavior_y: OverscrollBehavior::Auto,
892 overflow_scrolling: OverflowScrolling::Auto,
893};
894
895#[derive(Clone, PartialEq, Eq)]
898pub enum LayoutScrollbarWidthParseError<'a> {
899 InvalidValue(&'a str),
900}
901impl_debug_as_display!(LayoutScrollbarWidthParseError<'a>);
902impl_display! { LayoutScrollbarWidthParseError<'a>, {
903 InvalidValue(v) => format!("Invalid scrollbar-width value: \"{}\"", v),
904}}
905
906#[derive(Debug, Clone, PartialEq, Eq)]
907#[repr(C, u8)]
908pub enum LayoutScrollbarWidthParseErrorOwned {
909 InvalidValue(AzString),
910}
911impl LayoutScrollbarWidthParseError<'_> {
912 #[must_use] pub fn to_contained(&self) -> LayoutScrollbarWidthParseErrorOwned {
913 match self {
914 Self::InvalidValue(s) => {
915 LayoutScrollbarWidthParseErrorOwned::InvalidValue((*s).to_string().into())
916 }
917 }
918 }
919}
920impl LayoutScrollbarWidthParseErrorOwned {
921 #[must_use] pub fn to_shared(&self) -> LayoutScrollbarWidthParseError<'_> {
922 match self {
923 Self::InvalidValue(s) => LayoutScrollbarWidthParseError::InvalidValue(s.as_str()),
924 }
925 }
926}
927
928#[cfg(feature = "parser")]
929pub fn parse_layout_scrollbar_width(
933 input: &str,
934) -> Result<LayoutScrollbarWidth, LayoutScrollbarWidthParseError<'_>> {
935 match input.trim() {
936 "auto" => Ok(LayoutScrollbarWidth::Auto),
937 "thin" => Ok(LayoutScrollbarWidth::Thin),
938 "none" => Ok(LayoutScrollbarWidth::None),
939 _ => Err(LayoutScrollbarWidthParseError::InvalidValue(input)),
940 }
941}
942
943#[derive(Clone, PartialEq)]
944pub enum StyleScrollbarColorParseError<'a> {
945 InvalidValue(&'a str),
946 Color(CssColorParseError<'a>),
947}
948impl_debug_as_display!(StyleScrollbarColorParseError<'a>);
949impl_display! { StyleScrollbarColorParseError<'a>, {
950 InvalidValue(v) => format!("Invalid scrollbar-color value: \"{}\"", v),
951 Color(e) => format!("Invalid color in scrollbar-color: {}", e),
952}}
953impl_from!(CssColorParseError<'a>, StyleScrollbarColorParseError::Color);
954
955#[derive(Debug, Clone, PartialEq)]
956#[repr(C, u8)]
957pub enum StyleScrollbarColorParseErrorOwned {
958 InvalidValue(AzString),
959 Color(CssColorParseErrorOwned),
960}
961impl StyleScrollbarColorParseError<'_> {
962 #[must_use] pub fn to_contained(&self) -> StyleScrollbarColorParseErrorOwned {
963 match self {
964 Self::InvalidValue(s) => {
965 StyleScrollbarColorParseErrorOwned::InvalidValue((*s).to_string().into())
966 }
967 Self::Color(e) => StyleScrollbarColorParseErrorOwned::Color(e.to_contained()),
968 }
969 }
970}
971impl StyleScrollbarColorParseErrorOwned {
972 #[must_use] pub fn to_shared(&self) -> StyleScrollbarColorParseError<'_> {
973 match self {
974 Self::InvalidValue(s) => StyleScrollbarColorParseError::InvalidValue(s.as_str()),
975 Self::Color(e) => StyleScrollbarColorParseError::Color(e.to_shared()),
976 }
977 }
978}
979
980#[cfg(feature = "parser")]
981pub fn parse_style_scrollbar_color(
985 input: &str,
986) -> Result<StyleScrollbarColor, StyleScrollbarColorParseError<'_>> {
987 let input = input.trim();
988 if input == "auto" {
989 return Ok(StyleScrollbarColor::Auto);
990 }
991
992 let mut parts = input.split_whitespace();
993 let thumb_str = parts
994 .next()
995 .ok_or(StyleScrollbarColorParseError::InvalidValue(input))?;
996 let track_str = parts
997 .next()
998 .ok_or(StyleScrollbarColorParseError::InvalidValue(input))?;
999
1000 if parts.next().is_some() {
1001 return Err(StyleScrollbarColorParseError::InvalidValue(input));
1002 }
1003
1004 let thumb = parse_css_color(thumb_str)?;
1005 let track = parse_css_color(track_str)?;
1006
1007 Ok(StyleScrollbarColor::Custom(ScrollbarColorCustom {
1008 thumb,
1009 track,
1010 }))
1011}
1012
1013#[derive(Clone, PartialEq, Eq)]
1016pub enum ScrollbarVisibilityModeParseError<'a> {
1017 InvalidValue(&'a str),
1018}
1019impl_debug_as_display!(ScrollbarVisibilityModeParseError<'a>);
1020impl_display! { ScrollbarVisibilityModeParseError<'a>, {
1021 InvalidValue(v) => format!("Invalid scrollbar-visibility value: \"{}\"", v),
1022}}
1023
1024#[derive(Debug, Clone, PartialEq, Eq)]
1025#[repr(C, u8)]
1026pub enum ScrollbarVisibilityModeParseErrorOwned {
1027 InvalidValue(AzString),
1028}
1029impl ScrollbarVisibilityModeParseError<'_> {
1030 #[must_use] pub fn to_contained(&self) -> ScrollbarVisibilityModeParseErrorOwned {
1031 match self {
1032 Self::InvalidValue(s) => ScrollbarVisibilityModeParseErrorOwned::InvalidValue((*s).to_string().into()),
1033 }
1034 }
1035}
1036impl ScrollbarVisibilityModeParseErrorOwned {
1037 #[must_use] pub fn to_shared(&self) -> ScrollbarVisibilityModeParseError<'_> {
1038 match self {
1039 Self::InvalidValue(s) => ScrollbarVisibilityModeParseError::InvalidValue(s.as_str()),
1040 }
1041 }
1042}
1043
1044#[cfg(feature = "parser")]
1045pub fn parse_scrollbar_visibility_mode(
1049 input: &str,
1050) -> Result<ScrollbarVisibilityMode, ScrollbarVisibilityModeParseError<'_>> {
1051 match input.trim() {
1052 "always" => Ok(ScrollbarVisibilityMode::Always),
1053 "when-scrolling" => Ok(ScrollbarVisibilityMode::WhenScrolling),
1054 "auto" => Ok(ScrollbarVisibilityMode::Auto),
1055 _ => Err(ScrollbarVisibilityModeParseError::InvalidValue(input)),
1056 }
1057}
1058
1059#[derive(Clone, PartialEq, Eq)]
1062pub enum ScrollbarFadeDelayParseError<'a> {
1063 InvalidValue(&'a str),
1064}
1065impl_debug_as_display!(ScrollbarFadeDelayParseError<'a>);
1066impl_display! { ScrollbarFadeDelayParseError<'a>, {
1067 InvalidValue(v) => format!("Invalid scrollbar-fade-delay value: \"{}\"", v),
1068}}
1069
1070#[derive(Debug, Clone, PartialEq, Eq)]
1071#[repr(C, u8)]
1072pub enum ScrollbarFadeDelayParseErrorOwned {
1073 InvalidValue(AzString),
1074}
1075impl ScrollbarFadeDelayParseError<'_> {
1076 #[must_use] pub fn to_contained(&self) -> ScrollbarFadeDelayParseErrorOwned {
1077 match self {
1078 Self::InvalidValue(s) => ScrollbarFadeDelayParseErrorOwned::InvalidValue((*s).to_string().into()),
1079 }
1080 }
1081}
1082impl ScrollbarFadeDelayParseErrorOwned {
1083 #[must_use] pub fn to_shared(&self) -> ScrollbarFadeDelayParseError<'_> {
1084 match self {
1085 Self::InvalidValue(s) => ScrollbarFadeDelayParseError::InvalidValue(s.as_str()),
1086 }
1087 }
1088}
1089
1090#[cfg(feature = "parser")]
1096fn parse_time_ms(input: &str) -> Option<u32> {
1097 crate::props::basic::time::parse_duration(input).ok().map(|d| d.millis())
1098}
1099
1100#[cfg(feature = "parser")]
1101pub fn parse_scrollbar_fade_delay(
1105 input: &str,
1106) -> Result<ScrollbarFadeDelay, ScrollbarFadeDelayParseError<'_>> {
1107 parse_time_ms(input)
1108 .map(ScrollbarFadeDelay::new)
1109 .ok_or(ScrollbarFadeDelayParseError::InvalidValue(input))
1110}
1111
1112#[derive(Clone, PartialEq, Eq)]
1115pub enum ScrollbarFadeDurationParseError<'a> {
1116 InvalidValue(&'a str),
1117}
1118impl_debug_as_display!(ScrollbarFadeDurationParseError<'a>);
1119impl_display! { ScrollbarFadeDurationParseError<'a>, {
1120 InvalidValue(v) => format!("Invalid scrollbar-fade-duration value: \"{}\"", v),
1121}}
1122
1123#[derive(Debug, Clone, PartialEq, Eq)]
1124#[repr(C, u8)]
1125pub enum ScrollbarFadeDurationParseErrorOwned {
1126 InvalidValue(AzString),
1127}
1128impl ScrollbarFadeDurationParseError<'_> {
1129 #[must_use] pub fn to_contained(&self) -> ScrollbarFadeDurationParseErrorOwned {
1130 match self {
1131 Self::InvalidValue(s) => ScrollbarFadeDurationParseErrorOwned::InvalidValue((*s).to_string().into()),
1132 }
1133 }
1134}
1135impl ScrollbarFadeDurationParseErrorOwned {
1136 #[must_use] pub fn to_shared(&self) -> ScrollbarFadeDurationParseError<'_> {
1137 match self {
1138 Self::InvalidValue(s) => ScrollbarFadeDurationParseError::InvalidValue(s.as_str()),
1139 }
1140 }
1141}
1142
1143#[cfg(feature = "parser")]
1144pub fn parse_scrollbar_fade_duration(
1148 input: &str,
1149) -> Result<ScrollbarFadeDuration, ScrollbarFadeDurationParseError<'_>> {
1150 parse_time_ms(input)
1151 .map(ScrollbarFadeDuration::new)
1152 .ok_or(ScrollbarFadeDurationParseError::InvalidValue(input))
1153}
1154
1155#[cfg(all(test, feature = "parser"))]
1156mod tests {
1157 use super::*;
1158 use crate::props::basic::color::ColorU;
1159
1160 #[test]
1161 fn test_parse_scrollbar_width() {
1162 assert_eq!(
1163 parse_layout_scrollbar_width("auto").unwrap(),
1164 LayoutScrollbarWidth::Auto
1165 );
1166 assert_eq!(
1167 parse_layout_scrollbar_width("thin").unwrap(),
1168 LayoutScrollbarWidth::Thin
1169 );
1170 assert_eq!(
1171 parse_layout_scrollbar_width("none").unwrap(),
1172 LayoutScrollbarWidth::None
1173 );
1174 assert!(parse_layout_scrollbar_width("thick").is_err());
1175 }
1176
1177 #[test]
1178 fn test_parse_scrollbar_color() {
1179 assert_eq!(
1180 parse_style_scrollbar_color("auto").unwrap(),
1181 StyleScrollbarColor::Auto
1182 );
1183
1184 let custom = parse_style_scrollbar_color("red blue").unwrap();
1185 assert_eq!(
1186 custom,
1187 StyleScrollbarColor::Custom(ScrollbarColorCustom {
1188 thumb: ColorU::RED,
1189 track: ColorU::BLUE
1190 })
1191 );
1192
1193 let custom_hex = parse_style_scrollbar_color("#ff0000 #0000ff").unwrap();
1194 assert_eq!(
1195 custom_hex,
1196 StyleScrollbarColor::Custom(ScrollbarColorCustom {
1197 thumb: ColorU::RED,
1198 track: ColorU::BLUE
1199 })
1200 );
1201
1202 assert!(parse_style_scrollbar_color("red").is_err());
1203 assert!(parse_style_scrollbar_color("red blue green").is_err());
1204 }
1205}
1206
1207#[cfg(test)]
1208#[allow(clippy::unreadable_literal, clippy::float_cmp)]
1209mod autotest_generated {
1210 use super::*;
1211 use crate::codegen::format::FormatAsRustCode;
1212
1213 #[cfg(feature = "parser")]
1217 const TWO_POW_24: u32 = 16_777_216;
1218
1219 #[cfg(feature = "parser")]
1221 const GARBAGE: &[&str] = &[
1222 "",
1223 " ",
1224 " ",
1225 "\t\n",
1226 "\u{a0}", "\0",
1228 "\u{1F600}", "e\u{0301}", "\u{202e}auto", "аuto", "AUTO",
1233 "auto;",
1234 "auto garbage",
1235 "-1",
1236 "NaN",
1237 "inf",
1238 "0x10",
1239 "9223372036854775807", "1e400",
1241 "{[(<",
1242 ];
1243
1244 fn assert_physics_invariants(p: ScrollPhysics, name: &str) {
1253 for (field, v) in [
1254 ("deceleration_rate", p.deceleration_rate),
1255 ("min_velocity_threshold", p.min_velocity_threshold),
1256 ("max_velocity", p.max_velocity),
1257 ("wheel_multiplier", p.wheel_multiplier),
1258 ("overscroll_elasticity", p.overscroll_elasticity),
1259 ("max_overscroll_distance", p.max_overscroll_distance),
1260 ] {
1261 assert!(v.is_finite(), "{name}.{field} is not finite: {v}");
1262 assert!(!v.is_nan(), "{name}.{field} is NaN");
1263 assert!(v >= 0.0, "{name}.{field} is negative: {v}");
1264 }
1265
1266 assert!(
1267 p.deceleration_rate > 0.0 && p.deceleration_rate < 1.0,
1268 "{name}.deceleration_rate must stay inside (0.0, 1.0) or momentum never stops: {}",
1269 p.deceleration_rate
1270 );
1271 assert!(
1272 (0.0..=1.0).contains(&p.overscroll_elasticity),
1273 "{name}.overscroll_elasticity out of [0.0, 1.0]: {}",
1274 p.overscroll_elasticity
1275 );
1276 assert!(
1277 p.max_velocity > p.min_velocity_threshold,
1278 "{name}: max_velocity ({}) must exceed min_velocity_threshold ({})",
1279 p.max_velocity,
1280 p.min_velocity_threshold
1281 );
1282 assert!(
1283 p.wheel_multiplier > 0.0,
1284 "{name}.wheel_multiplier must be > 0 or the wheel does nothing"
1285 );
1286 assert!(
1287 p.timer_interval_ms > 0,
1288 "{name}.timer_interval_ms == 0 would spin the physics timer"
1289 );
1290 assert!(
1291 p.smooth_scroll_duration_ms > 0,
1292 "{name}.smooth_scroll_duration_ms == 0 makes `scroll-behavior: smooth` a no-op"
1293 );
1294 }
1295
1296 #[test]
1297 fn scroll_physics_presets_hold_their_invariants() {
1298 assert_physics_invariants(ScrollPhysics::default(), "default");
1299 assert_physics_invariants(ScrollPhysics::ios(), "ios");
1300 assert_physics_invariants(ScrollPhysics::macos(), "macos");
1301 assert_physics_invariants(ScrollPhysics::windows(), "windows");
1302 assert_physics_invariants(ScrollPhysics::android(), "android");
1303 }
1304
1305 #[test]
1306 fn scroll_physics_presets_are_pure_and_distinct() {
1307 assert_eq!(ScrollPhysics::ios(), ScrollPhysics::ios());
1309 assert_eq!(ScrollPhysics::windows(), ScrollPhysics::windows());
1310
1311 assert_ne!(ScrollPhysics::ios(), ScrollPhysics::macos());
1313 assert_ne!(ScrollPhysics::ios(), ScrollPhysics::android());
1314 assert_ne!(ScrollPhysics::macos(), ScrollPhysics::windows());
1315 assert_ne!(ScrollPhysics::android(), ScrollPhysics::windows());
1316 assert_ne!(ScrollPhysics::default(), ScrollPhysics::ios());
1317 }
1318
1319 #[test]
1322 fn scroll_physics_presets_match_their_documented_platform_behavior() {
1323 let win = ScrollPhysics::windows();
1324 assert_eq!(win.overscroll_elasticity, 0.0);
1325 assert_eq!(win.max_overscroll_distance, 0.0);
1326 assert!(!win.invert_direction);
1327
1328 assert!(ScrollPhysics::ios().invert_direction);
1329 assert!(ScrollPhysics::macos().invert_direction);
1330 assert!(!ScrollPhysics::android().invert_direction);
1331
1332 assert!(ScrollPhysics::ios().deceleration_rate > ScrollPhysics::windows().deceleration_rate);
1334
1335 assert_eq!(ScrollPhysics::default().overscroll_elasticity, 0.0);
1337 }
1338
1339 #[test]
1344 fn fade_delay_and_duration_constructors_store_their_argument_verbatim() {
1345 for ms in [0u32, 1, 16, 500, u32::MAX / 2, u32::MAX - 1, u32::MAX] {
1346 assert_eq!(ScrollbarFadeDelay::new(ms).ms, ms);
1347 assert_eq!(ScrollbarFadeDuration::new(ms).ms, ms);
1348 }
1349 }
1350
1351 #[test]
1352 fn fade_zero_constants_agree_with_new_and_default() {
1353 assert_eq!(ScrollbarFadeDelay::ZERO, ScrollbarFadeDelay::new(0));
1354 assert_eq!(ScrollbarFadeDelay::ZERO, ScrollbarFadeDelay::default());
1355 assert_eq!(ScrollbarFadeDuration::ZERO, ScrollbarFadeDuration::new(0));
1356 assert_eq!(ScrollbarFadeDuration::ZERO, ScrollbarFadeDuration::default());
1357 assert_eq!(ScrollbarFadeDelay::ZERO.ms, 0);
1358 assert_eq!(ScrollbarFadeDuration::ZERO.ms, 0);
1359 }
1360
1361 #[test]
1364 fn fade_delay_orders_by_millisecond_count() {
1365 assert!(ScrollbarFadeDelay::new(0) < ScrollbarFadeDelay::new(1));
1366 assert!(ScrollbarFadeDelay::new(499) < ScrollbarFadeDelay::new(500));
1367 assert!(ScrollbarFadeDelay::new(u32::MAX) > ScrollbarFadeDelay::new(u32::MAX - 1));
1368 assert!(ScrollbarFadeDuration::new(0) < ScrollbarFadeDuration::new(u32::MAX));
1369 }
1370
1371 #[test]
1376 fn enum_printers_emit_the_css_keywords() {
1377 assert_eq!(ScrollBehavior::Auto.print_as_css_value(), "auto");
1378 assert_eq!(ScrollBehavior::Smooth.print_as_css_value(), "smooth");
1379 assert_eq!(ScrollBehavior::default(), ScrollBehavior::Auto);
1380
1381 assert_eq!(OverscrollBehavior::Auto.print_as_css_value(), "auto");
1382 assert_eq!(OverscrollBehavior::Contain.print_as_css_value(), "contain");
1383 assert_eq!(OverscrollBehavior::None.print_as_css_value(), "none");
1384 assert_eq!(OverscrollBehavior::default(), OverscrollBehavior::Auto);
1385
1386 assert_eq!(OverflowScrolling::Auto.print_as_css_value(), "auto");
1387 assert_eq!(OverflowScrolling::Touch.print_as_css_value(), "touch");
1388 assert_eq!(OverflowScrolling::default(), OverflowScrolling::Auto);
1389
1390 assert_eq!(LayoutScrollbarWidth::Auto.print_as_css_value(), "auto");
1391 assert_eq!(LayoutScrollbarWidth::Thin.print_as_css_value(), "thin");
1392 assert_eq!(LayoutScrollbarWidth::None.print_as_css_value(), "none");
1393 assert_eq!(LayoutScrollbarWidth::default(), LayoutScrollbarWidth::Auto);
1394
1395 assert_eq!(ScrollbarVisibilityMode::Always.print_as_css_value(), "always");
1396 assert_eq!(
1397 ScrollbarVisibilityMode::WhenScrolling.print_as_css_value(),
1398 "when-scrolling"
1399 );
1400 assert_eq!(ScrollbarVisibilityMode::Auto.print_as_css_value(), "auto");
1401 assert_eq!(
1402 ScrollbarVisibilityMode::default(),
1403 ScrollbarVisibilityMode::Always
1404 );
1405 }
1406
1407 #[test]
1411 fn fade_printers_special_case_zero_and_keep_the_unit_otherwise() {
1412 assert_eq!(ScrollbarFadeDelay::new(0).print_as_css_value(), "0");
1413 assert_eq!(ScrollbarFadeDelay::new(1).print_as_css_value(), "1ms");
1414 assert_eq!(ScrollbarFadeDelay::new(500).print_as_css_value(), "500ms");
1415 assert_eq!(
1416 ScrollbarFadeDelay::new(u32::MAX).print_as_css_value(),
1417 "4294967295ms"
1418 );
1419 assert_eq!(ScrollbarFadeDuration::new(0).print_as_css_value(), "0");
1420 assert_eq!(ScrollbarFadeDuration::new(200).print_as_css_value(), "200ms");
1421 assert_eq!(
1422 ScrollbarFadeDuration::new(u32::MAX).print_as_css_value(),
1423 "4294967295ms"
1424 );
1425 }
1426
1427 #[test]
1428 fn scrollbar_color_printer_emits_two_eight_digit_hashes() {
1429 assert_eq!(StyleScrollbarColor::Auto.print_as_css_value(), "auto");
1430 assert_eq!(StyleScrollbarColor::default(), StyleScrollbarColor::Auto);
1431
1432 let custom = StyleScrollbarColor::Custom(ScrollbarColorCustom {
1433 thumb: ColorU::RED,
1434 track: ColorU::TRANSPARENT,
1435 });
1436 assert_eq!(custom.print_as_css_value(), "#ff0000ff #00000000");
1437 }
1438
1439 #[test]
1442 fn aggregate_printers_do_not_panic_and_mention_both_axes() {
1443 let printed = ScrollbarStyle::default().print_as_css_value();
1444 assert!(printed.contains("horz("), "{printed}");
1445 assert!(printed.contains("vert("), "{printed}");
1446
1447 let info = ScrollbarInfo::default().print_as_css_value();
1448 assert!(info.contains("width:"), "{info}");
1449 assert!(info.contains("thumb:"), "{info}");
1450 assert!(info.contains("resizer:"), "{info}");
1451 }
1452
1453 #[test]
1458 fn format_as_rust_code_emits_constructible_expressions() {
1459 assert_eq!(
1460 LayoutScrollbarWidth::Thin.format_as_rust_code(0),
1461 "LayoutScrollbarWidth::Thin"
1462 );
1463 assert_eq!(
1464 LayoutScrollbarWidth::None.format_as_rust_code(7),
1465 "LayoutScrollbarWidth::None",
1466 "indent depth must not leak into a unit-variant literal"
1467 );
1468 assert_eq!(
1469 ScrollbarVisibilityMode::WhenScrolling.format_as_rust_code(0),
1470 "ScrollbarVisibilityMode::WhenScrolling"
1471 );
1472 assert_eq!(
1473 StyleScrollbarColor::Auto.format_as_rust_code(0),
1474 "StyleScrollbarColor::Auto"
1475 );
1476
1477 assert_eq!(
1479 ScrollbarFadeDelay::new(0).format_as_rust_code(0),
1480 "ScrollbarFadeDelay::new(0)"
1481 );
1482 assert_eq!(
1483 ScrollbarFadeDelay::new(u32::MAX).format_as_rust_code(3),
1484 "ScrollbarFadeDelay::new(4294967295)"
1485 );
1486 assert_eq!(
1487 ScrollbarFadeDuration::new(u32::MAX).format_as_rust_code(0),
1488 "ScrollbarFadeDuration::new(4294967295)"
1489 );
1490 }
1491
1492 #[test]
1493 fn format_as_rust_code_of_aggregates_does_not_panic() {
1494 let custom = StyleScrollbarColor::Custom(ScrollbarColorCustom {
1495 thumb: ColorU::TRANSPARENT,
1496 track: ColorU::WHITE,
1497 })
1498 .format_as_rust_code(0);
1499 assert!(
1500 custom.starts_with("StyleScrollbarColor::Custom(ScrollbarColorCustom {"),
1501 "{custom}"
1502 );
1503 assert!(custom.contains("thumb:") && custom.contains("track:"), "{custom}");
1504
1505 for tabs in [0usize, 1, 4] {
1506 let code = ScrollbarStyle::default().format_as_rust_code(tabs);
1507 assert!(code.starts_with("ScrollbarStyle {"), "{code}");
1508 assert!(code.contains("horizontal:"), "{code}");
1509 assert!(code.contains("vertical:"), "{code}");
1510 }
1511 }
1512
1513 #[test]
1518 fn scrollbar_info_default_is_the_classic_light_constant() {
1519 assert_eq!(ScrollbarInfo::default(), SCROLLBAR_CLASSIC_LIGHT);
1520
1521 let style = ScrollbarStyle::default();
1522 assert_eq!(style.horizontal, SCROLLBAR_CLASSIC_LIGHT);
1523 assert_eq!(style.vertical, SCROLLBAR_CLASSIC_LIGHT);
1524 }
1525
1526 #[test]
1530 fn computed_default_mirrors_the_default_scrollbar_info() {
1531 let computed = ComputedScrollbarStyle::default();
1532 let info = ScrollbarInfo::default();
1533
1534 assert_eq!(computed.width, Some(info.width));
1535 assert_eq!(
1536 computed.thumb_color,
1537 Some(ColorU { r: 193, g: 193, b: 193, a: 255 })
1538 );
1539 assert_eq!(
1540 computed.track_color,
1541 Some(ColorU { r: 241, g: 241, b: 241, a: 255 })
1542 );
1543 assert!(
1544 computed.thumb_color.is_some() && computed.track_color.is_some(),
1545 "the classic-light default must resolve to solid colors, not None"
1546 );
1547 }
1548
1549 #[test]
1553 fn preset_constants_agree_on_the_overlay_clipping_flag() {
1554 for info in [SCROLLBAR_CLASSIC_LIGHT, SCROLLBAR_CLASSIC_DARK] {
1555 assert!(!info.clip_to_container_border);
1556 assert_eq!(info.scroll_behavior, ScrollBehavior::Auto);
1557 }
1558 for info in [
1559 SCROLLBAR_MACOS_LIGHT,
1560 SCROLLBAR_MACOS_DARK,
1561 SCROLLBAR_IOS_LIGHT,
1562 SCROLLBAR_IOS_DARK,
1563 SCROLLBAR_ANDROID_LIGHT,
1564 SCROLLBAR_ANDROID_DARK,
1565 ] {
1566 assert!(info.clip_to_container_border);
1567 assert_eq!(info.scroll_behavior, ScrollBehavior::Smooth);
1568 }
1569 for info in [SCROLLBAR_WINDOWS_LIGHT, SCROLLBAR_WINDOWS_DARK] {
1570 assert!(!info.clip_to_container_border);
1571 assert_eq!(info.overscroll_behavior_x, OverscrollBehavior::None);
1572 assert_eq!(info.overscroll_behavior_y, OverscrollBehavior::None);
1573 }
1574 }
1575
1576 #[test]
1579 fn light_and_dark_presets_share_their_geometry() {
1580 for (light, dark) in [
1581 (SCROLLBAR_CLASSIC_LIGHT, SCROLLBAR_CLASSIC_DARK),
1582 (SCROLLBAR_MACOS_LIGHT, SCROLLBAR_MACOS_DARK),
1583 (SCROLLBAR_WINDOWS_LIGHT, SCROLLBAR_WINDOWS_DARK),
1584 (SCROLLBAR_IOS_LIGHT, SCROLLBAR_IOS_DARK),
1585 (SCROLLBAR_ANDROID_LIGHT, SCROLLBAR_ANDROID_DARK),
1586 ] {
1587 assert_eq!(light.width, dark.width);
1588 assert_eq!(light.padding_left, dark.padding_left);
1589 assert_eq!(light.padding_right, dark.padding_right);
1590 assert_eq!(light.clip_to_container_border, dark.clip_to_container_border);
1591 assert_ne!(light.thumb, dark.thumb, "light/dark thumbs must differ");
1592 }
1593 }
1594
1595 #[cfg(feature = "parser")]
1600 #[test]
1601 fn scrollbar_width_parses_the_three_legal_keywords() {
1602 assert_eq!(
1603 parse_layout_scrollbar_width("auto"),
1604 Ok(LayoutScrollbarWidth::Auto)
1605 );
1606 assert_eq!(
1607 parse_layout_scrollbar_width("thin"),
1608 Ok(LayoutScrollbarWidth::Thin)
1609 );
1610 assert_eq!(
1611 parse_layout_scrollbar_width("none"),
1612 Ok(LayoutScrollbarWidth::None)
1613 );
1614 }
1615
1616 #[cfg(feature = "parser")]
1617 #[test]
1618 fn scrollbar_width_trims_surrounding_whitespace_but_rejects_inner_junk() {
1619 assert_eq!(
1620 parse_layout_scrollbar_width(" \t thin \n "),
1621 Ok(LayoutScrollbarWidth::Thin)
1622 );
1623 assert!(parse_layout_scrollbar_width("thin;").is_err());
1624 assert!(parse_layout_scrollbar_width("thin thin").is_err());
1625 assert!(parse_layout_scrollbar_width("th in").is_err());
1626 }
1627
1628 #[cfg(feature = "parser")]
1632 #[test]
1633 fn scrollbar_width_keyword_matching_is_case_sensitive() {
1634 assert!(parse_layout_scrollbar_width("AUTO").is_err());
1635 assert!(parse_layout_scrollbar_width("Thin").is_err());
1636 assert!(parse_layout_scrollbar_width("NONE").is_err());
1637 }
1638
1639 #[cfg(feature = "parser")]
1640 #[test]
1641 fn scrollbar_width_rejects_every_garbage_input_without_panicking() {
1642 for input in GARBAGE {
1643 assert!(
1644 parse_layout_scrollbar_width(input).is_err(),
1645 "expected {input:?} to be rejected"
1646 );
1647 }
1648 }
1649
1650 #[cfg(feature = "parser")]
1653 #[test]
1654 fn scrollbar_width_error_keeps_the_raw_untrimmed_input() {
1655 let raw = " thick ";
1656 assert_eq!(
1657 parse_layout_scrollbar_width(raw),
1658 Err(LayoutScrollbarWidthParseError::InvalidValue(raw))
1659 );
1660 let msg = format!("{}", parse_layout_scrollbar_width(raw).unwrap_err());
1661 assert!(msg.contains(raw), "{msg}");
1662 }
1663
1664 #[cfg(feature = "parser")]
1665 #[test]
1666 fn scrollbar_width_survives_a_megabyte_of_input_and_deep_nesting() {
1667 let huge = "a".repeat(1_000_000);
1668 assert!(parse_layout_scrollbar_width(&huge).is_err());
1669
1670 let repeated_token = "auto".repeat(250_000);
1671 assert!(parse_layout_scrollbar_width(&repeated_token).is_err());
1672
1673 let nested = "(".repeat(10_000);
1674 assert!(parse_layout_scrollbar_width(&nested).is_err());
1675 }
1676
1677 #[cfg(feature = "parser")]
1678 #[test]
1679 fn scrollbar_width_round_trips_through_its_printer() {
1680 for value in [
1681 LayoutScrollbarWidth::Auto,
1682 LayoutScrollbarWidth::Thin,
1683 LayoutScrollbarWidth::None,
1684 ] {
1685 let encoded = value.print_as_css_value();
1686 assert_eq!(
1687 parse_layout_scrollbar_width(&encoded),
1688 Ok(value),
1689 "{encoded} did not decode back to {value:?}"
1690 );
1691 }
1692 }
1693
1694 #[cfg(feature = "parser")]
1699 #[test]
1700 fn scrollbar_color_needs_exactly_two_colors_or_the_auto_keyword() {
1701 assert_eq!(parse_style_scrollbar_color("auto"), Ok(StyleScrollbarColor::Auto));
1702 assert_eq!(
1703 parse_style_scrollbar_color(" auto "),
1704 Ok(StyleScrollbarColor::Auto)
1705 );
1706 assert_eq!(
1707 parse_style_scrollbar_color("red blue"),
1708 Ok(StyleScrollbarColor::Custom(ScrollbarColorCustom {
1709 thumb: ColorU::RED,
1710 track: ColorU::BLUE,
1711 }))
1712 );
1713
1714 for input in ["red", "#fff", "red blue green", "a b c d"] {
1716 assert!(
1717 matches!(
1718 parse_style_scrollbar_color(input),
1719 Err(StyleScrollbarColorParseError::InvalidValue(_))
1720 ),
1721 "expected {input:?} to be an InvalidValue error"
1722 );
1723 }
1724 }
1725
1726 #[cfg(feature = "parser")]
1729 #[test]
1730 fn scrollbar_color_accepts_any_whitespace_run_as_the_separator() {
1731 let expected = StyleScrollbarColor::Custom(ScrollbarColorCustom {
1732 thumb: ColorU::RED,
1733 track: ColorU::BLUE,
1734 });
1735 assert_eq!(parse_style_scrollbar_color("red\tblue"), Ok(expected));
1736 assert_eq!(parse_style_scrollbar_color("red\n blue"), Ok(expected));
1737 assert_eq!(parse_style_scrollbar_color(" red blue "), Ok(expected));
1738 }
1739
1740 #[cfg(feature = "parser")]
1743 #[test]
1744 fn scrollbar_color_names_are_case_insensitive_but_the_auto_keyword_is_not() {
1745 assert_eq!(
1746 parse_style_scrollbar_color("RED BLUE"),
1747 Ok(StyleScrollbarColor::Custom(ScrollbarColorCustom {
1748 thumb: ColorU::RED,
1749 track: ColorU::BLUE,
1750 }))
1751 );
1752 assert!(matches!(
1754 parse_style_scrollbar_color("AUTO"),
1755 Err(StyleScrollbarColorParseError::InvalidValue(_))
1756 ));
1757 assert!(matches!(
1759 parse_style_scrollbar_color("auto auto"),
1760 Err(StyleScrollbarColorParseError::Color(_))
1761 ));
1762 }
1763
1764 #[cfg(feature = "parser")]
1769 #[test]
1770 fn scrollbar_color_rejects_functional_colors_containing_spaces() {
1771 assert_eq!(
1772 parse_style_scrollbar_color("rgb(255,0,0) blue"),
1773 Ok(StyleScrollbarColor::Custom(ScrollbarColorCustom {
1774 thumb: ColorU::RED,
1775 track: ColorU::BLUE,
1776 }))
1777 );
1778 assert!(matches!(
1779 parse_style_scrollbar_color("rgb(255, 0, 0) blue"),
1780 Err(StyleScrollbarColorParseError::InvalidValue(_))
1781 ));
1782 }
1783
1784 #[cfg(feature = "parser")]
1785 #[test]
1786 fn scrollbar_color_reports_which_component_failed() {
1787 assert!(matches!(
1789 parse_style_scrollbar_color("notacolor blue"),
1790 Err(StyleScrollbarColorParseError::Color(_))
1791 ));
1792 assert!(matches!(
1793 parse_style_scrollbar_color("red notacolor"),
1794 Err(StyleScrollbarColorParseError::Color(_))
1795 ));
1796 assert!(matches!(
1797 parse_style_scrollbar_color("#gggggg #000000"),
1798 Err(StyleScrollbarColorParseError::Color(_))
1799 ));
1800 }
1801
1802 #[cfg(feature = "parser")]
1803 #[test]
1804 fn scrollbar_color_rejects_garbage_without_panicking() {
1805 for input in GARBAGE {
1806 assert!(
1807 parse_style_scrollbar_color(input).is_err(),
1808 "expected {input:?} to be rejected"
1809 );
1810 }
1811 for input in [
1813 "0 0",
1814 "-0 -0",
1815 "NaN NaN",
1816 "inf inf",
1817 "9223372036854775807 1",
1818 "1e400 1e400",
1819 "-1 -1",
1820 ] {
1821 assert!(
1822 parse_style_scrollbar_color(input).is_err(),
1823 "expected {input:?} to be rejected"
1824 );
1825 }
1826 }
1827
1828 #[cfg(feature = "parser")]
1829 #[test]
1830 fn scrollbar_color_survives_huge_and_deeply_nested_input() {
1831 let huge = "z".repeat(500_000);
1832 let two_huge = format!("{huge} {huge}");
1833 assert!(parse_style_scrollbar_color(&two_huge).is_err());
1834
1835 let nested = "(".repeat(10_000);
1836 assert!(parse_style_scrollbar_color(&format!("{nested} {nested}")).is_err());
1837
1838 let many = "red ".repeat(100_000);
1840 assert!(matches!(
1841 parse_style_scrollbar_color(&many),
1842 Err(StyleScrollbarColorParseError::InvalidValue(_))
1843 ));
1844 }
1845
1846 #[cfg(feature = "parser")]
1850 #[test]
1851 fn scrollbar_color_error_carries_the_trimmed_input() {
1852 assert_eq!(
1853 parse_style_scrollbar_color(" red "),
1854 Err(StyleScrollbarColorParseError::InvalidValue("red"))
1855 );
1856 }
1857
1858 #[cfg(feature = "parser")]
1859 #[test]
1860 fn scrollbar_color_round_trips_through_its_printer() {
1861 let samples = [
1862 StyleScrollbarColor::Auto,
1863 StyleScrollbarColor::Custom(ScrollbarColorCustom {
1864 thumb: ColorU::RED,
1865 track: ColorU::BLUE,
1866 }),
1867 StyleScrollbarColor::Custom(ScrollbarColorCustom {
1868 thumb: ColorU::TRANSPARENT,
1869 track: ColorU::TRANSPARENT,
1870 }),
1871 StyleScrollbarColor::Custom(ScrollbarColorCustom {
1872 thumb: ColorU { r: 0, g: 0, b: 0, a: 100 },
1873 track: ColorU { r: 1, g: 2, b: 3, a: 4 },
1874 }),
1875 StyleScrollbarColor::Custom(ScrollbarColorCustom {
1876 thumb: ColorU::WHITE,
1877 track: ColorU::BLACK,
1878 }),
1879 ];
1880 for value in samples {
1881 let encoded = value.print_as_css_value();
1882 assert_eq!(
1883 parse_style_scrollbar_color(&encoded),
1884 Ok(value),
1885 "{encoded} did not decode back to {value:?}"
1886 );
1887 }
1888 }
1889
1890 #[cfg(feature = "parser")]
1895 #[test]
1896 fn visibility_mode_parses_its_three_keywords_and_trims() {
1897 assert_eq!(
1898 parse_scrollbar_visibility_mode("always"),
1899 Ok(ScrollbarVisibilityMode::Always)
1900 );
1901 assert_eq!(
1902 parse_scrollbar_visibility_mode(" when-scrolling\t"),
1903 Ok(ScrollbarVisibilityMode::WhenScrolling)
1904 );
1905 assert_eq!(
1906 parse_scrollbar_visibility_mode("auto"),
1907 Ok(ScrollbarVisibilityMode::Auto)
1908 );
1909 }
1910
1911 #[cfg(feature = "parser")]
1912 #[test]
1913 fn visibility_mode_rejects_near_misses_and_garbage() {
1914 for input in [
1915 "when scrolling", "whenscrolling",
1917 "when-scrolling-",
1918 "-when-scrolling",
1919 "ALWAYS",
1920 "always;",
1921 "always auto",
1922 ] {
1923 assert!(
1924 parse_scrollbar_visibility_mode(input).is_err(),
1925 "expected {input:?} to be rejected"
1926 );
1927 }
1928 for input in GARBAGE {
1929 assert!(
1930 parse_scrollbar_visibility_mode(input).is_err(),
1931 "expected {input:?} to be rejected"
1932 );
1933 }
1934 }
1935
1936 #[cfg(feature = "parser")]
1937 #[test]
1938 fn visibility_mode_survives_huge_and_nested_input() {
1939 assert!(parse_scrollbar_visibility_mode(&"a".repeat(1_000_000)).is_err());
1940 assert!(parse_scrollbar_visibility_mode(&"always".repeat(200_000)).is_err());
1941 assert!(parse_scrollbar_visibility_mode(&"[".repeat(10_000)).is_err());
1942 }
1943
1944 #[cfg(feature = "parser")]
1945 #[test]
1946 fn visibility_mode_round_trips_through_its_printer() {
1947 for value in [
1948 ScrollbarVisibilityMode::Always,
1949 ScrollbarVisibilityMode::WhenScrolling,
1950 ScrollbarVisibilityMode::Auto,
1951 ] {
1952 let encoded = value.print_as_css_value();
1953 assert_eq!(
1954 parse_scrollbar_visibility_mode(&encoded),
1955 Ok(value),
1956 "{encoded} did not decode back to {value:?}"
1957 );
1958 }
1959 }
1960
1961 #[cfg(feature = "parser")]
1966 #[test]
1967 fn parse_time_ms_accepts_bare_zero_and_both_units() {
1968 assert_eq!(parse_time_ms("0"), Some(0));
1969 assert_eq!(parse_time_ms("0ms"), Some(0));
1970 assert_eq!(parse_time_ms("0s"), Some(0));
1971 assert_eq!(parse_time_ms("500ms"), Some(500));
1972 assert_eq!(parse_time_ms("1s"), Some(1000));
1973 assert_eq!(parse_time_ms("1.5s"), Some(1500));
1974 assert_eq!(parse_time_ms(" 200ms "), Some(200));
1975 assert_eq!(parse_time_ms("200MS"), Some(200), "units are case-insensitive");
1976 }
1977
1978 #[cfg(feature = "parser")]
1983 #[test]
1984 fn parse_time_ms_converts_the_tick_unit_to_milliseconds() {
1985 assert_eq!(parse_time_ms("60t"), Some(1000));
1986 assert_eq!(parse_time_ms("30t"), Some(500));
1987 assert_eq!(parse_time_ms("1t"), Some(16));
1988 assert_eq!(parse_time_ms("0t"), Some(0));
1989 assert_ne!(parse_time_ms("60t"), Some(60), "ticks passed through as ms");
1990 }
1991
1992 #[cfg(feature = "parser")]
1995 #[test]
1996 fn parse_time_ms_requires_an_attached_unit() {
1997 assert_eq!(parse_time_ms("500"), None);
1998 assert_eq!(parse_time_ms("1 s"), None);
1999 assert_eq!(parse_time_ms("500 ms"), None);
2000 assert_eq!(parse_time_ms("ms"), None);
2001 assert_eq!(parse_time_ms("s"), None);
2002 assert_eq!(parse_time_ms("500px"), None);
2003 assert_eq!(parse_time_ms("500msms"), None);
2004 }
2005
2006 #[cfg(feature = "parser")]
2007 #[test]
2008 fn parse_time_ms_rejects_empty_blank_unicode_and_garbage() {
2009 for input in ["", " ", " ", "\t\n", "\u{1F600}", "e\u{0301}", "٥ms", "500ms"] {
2010 assert_eq!(parse_time_ms(input), None, "expected {input:?} to be rejected");
2011 }
2012 }
2013
2014 #[cfg(feature = "parser")]
2015 #[test]
2016 fn parse_time_ms_rejects_negative_durations() {
2017 assert_eq!(parse_time_ms("-1ms"), None);
2018 assert_eq!(parse_time_ms("-0.5s"), None);
2019 assert_eq!(parse_time_ms("-inf ms"), None);
2020 }
2021
2022 #[cfg(feature = "parser")]
2025 #[test]
2026 fn parse_time_ms_accepts_negative_zero_as_zero() {
2027 assert_eq!(parse_time_ms("-0ms"), Some(0));
2028 assert_eq!(parse_time_ms("-0.0s"), Some(0));
2029 }
2030
2031 #[cfg(feature = "parser")]
2036 #[test]
2037 fn parse_time_ms_saturates_on_non_finite_and_huge_values() {
2038 assert_eq!(parse_time_ms("infms"), Some(u32::MAX));
2039 assert_eq!(parse_time_ms("infinityms"), Some(u32::MAX));
2040 assert_eq!(parse_time_ms("infs"), Some(u32::MAX));
2041 assert_eq!(parse_time_ms("nanms"), Some(0));
2042 assert_eq!(parse_time_ms("NaNms"), Some(0));
2043
2044 assert_eq!(parse_time_ms("1e30ms"), Some(u32::MAX));
2045 assert_eq!(parse_time_ms("1e400ms"), Some(u32::MAX), "overflows f32 to inf");
2046 assert_eq!(parse_time_ms("4294967296ms"), Some(u32::MAX), "2^32 clamps");
2047 assert_eq!(parse_time_ms("1e-30ms"), Some(0), "underflows to zero");
2048
2049 let long_number = format!("{}ms", "9".repeat(100_000));
2051 assert_eq!(parse_time_ms(&long_number), Some(u32::MAX));
2052 }
2053
2054 #[cfg(feature = "parser")]
2057 #[test]
2058 fn parse_time_ms_saturates_when_seconds_overflow_milliseconds() {
2059 assert_eq!(parse_time_ms("1000s"), Some(1_000_000));
2061 assert_eq!(parse_time_ms("16777s"), Some(16_777_000));
2062
2063 assert_eq!(parse_time_ms("4294968s"), Some(u32::MAX));
2065 assert_eq!(parse_time_ms("5000000s"), Some(u32::MAX));
2066
2067 let ms = parse_time_ms("4294967s").expect("4294967s must parse");
2070 assert!(
2071 ms.abs_diff(4_294_967_000) <= 512,
2072 "4294967s decoded to {ms}, which is nowhere near 4294967000ms"
2073 );
2074
2075 assert_eq!(parse_time_ms("0.0005s"), Some(0), "sub-ms truncates toward zero");
2076 }
2077
2078 #[cfg(feature = "parser")]
2083 #[test]
2084 fn fade_parsers_accept_the_documented_syntax() {
2085 assert_eq!(
2086 parse_scrollbar_fade_delay("500ms"),
2087 Ok(ScrollbarFadeDelay::new(500))
2088 );
2089 assert_eq!(parse_scrollbar_fade_delay("0"), Ok(ScrollbarFadeDelay::ZERO));
2090 assert_eq!(
2091 parse_scrollbar_fade_delay(" 1s "),
2092 Ok(ScrollbarFadeDelay::new(1000))
2093 );
2094 assert_eq!(
2095 parse_scrollbar_fade_duration("200ms"),
2096 Ok(ScrollbarFadeDuration::new(200))
2097 );
2098 assert_eq!(
2099 parse_scrollbar_fade_duration("0"),
2100 Ok(ScrollbarFadeDuration::ZERO)
2101 );
2102 }
2103
2104 #[cfg(feature = "parser")]
2105 #[test]
2106 fn fade_parsers_reject_garbage_and_keep_the_raw_input_in_the_error() {
2107 for input in GARBAGE {
2108 assert!(
2109 parse_scrollbar_fade_delay(input).is_err(),
2110 "delay: expected {input:?} to be rejected"
2111 );
2112 assert!(
2113 parse_scrollbar_fade_duration(input).is_err(),
2114 "duration: expected {input:?} to be rejected"
2115 );
2116 }
2117
2118 let raw = " bogus ";
2119 assert_eq!(
2120 parse_scrollbar_fade_delay(raw),
2121 Err(ScrollbarFadeDelayParseError::InvalidValue(raw))
2122 );
2123 assert_eq!(
2124 parse_scrollbar_fade_duration(raw),
2125 Err(ScrollbarFadeDurationParseError::InvalidValue(raw))
2126 );
2127 }
2128
2129 #[cfg(feature = "parser")]
2130 #[test]
2131 fn fade_parsers_reject_negative_delays() {
2132 assert!(parse_scrollbar_fade_delay("-1ms").is_err());
2133 assert!(parse_scrollbar_fade_delay("-500ms").is_err());
2134 assert!(parse_scrollbar_fade_duration("-0.5s").is_err());
2135 }
2136
2137 #[cfg(feature = "parser")]
2138 #[test]
2139 fn fade_parsers_saturate_instead_of_overflowing() {
2140 assert_eq!(
2141 parse_scrollbar_fade_delay("1e30ms"),
2142 Ok(ScrollbarFadeDelay::new(u32::MAX))
2143 );
2144 assert_eq!(
2145 parse_scrollbar_fade_duration("99999999999999s"),
2146 Ok(ScrollbarFadeDuration::new(u32::MAX))
2147 );
2148 }
2149
2150 #[cfg(feature = "parser")]
2151 #[test]
2152 fn fade_parsers_survive_huge_and_nested_input() {
2153 assert!(parse_scrollbar_fade_delay(&"a".repeat(1_000_000)).is_err());
2154 assert!(parse_scrollbar_fade_duration(&"0ms".repeat(300_000)).is_err());
2155 assert!(parse_scrollbar_fade_delay(&"(".repeat(10_000)).is_err());
2156 assert!(parse_scrollbar_fade_duration(&"[".repeat(10_000)).is_err());
2157 }
2158
2159 #[cfg(feature = "parser")]
2164 #[test]
2165 fn fade_delay_round_trips_exactly_up_to_two_pow_24() {
2166 for ms in [
2167 0u32,
2168 1,
2169 8,
2170 16,
2171 200,
2172 500,
2173 65_535,
2174 1_000_000,
2175 TWO_POW_24 - 1,
2176 TWO_POW_24,
2177 u32::MAX,
2178 ] {
2179 let value = ScrollbarFadeDelay::new(ms);
2180 let encoded = value.print_as_css_value();
2181 assert_eq!(
2182 parse_scrollbar_fade_delay(&encoded),
2183 Ok(value),
2184 "{ms}ms encoded as {encoded:?} did not decode back"
2185 );
2186
2187 let value = ScrollbarFadeDuration::new(ms);
2188 let encoded = value.print_as_css_value();
2189 assert_eq!(
2190 parse_scrollbar_fade_duration(&encoded),
2191 Ok(value),
2192 "{ms}ms encoded as {encoded:?} did not decode back"
2193 );
2194 }
2195 }
2196
2197 #[cfg(feature = "parser")]
2201 #[test]
2202 fn fade_delay_round_trip_is_lossy_above_two_pow_24() {
2203 let value = ScrollbarFadeDelay::new(TWO_POW_24 + 1);
2204 let decoded = parse_scrollbar_fade_delay(&value.print_as_css_value()).unwrap();
2205 assert_ne!(decoded, value, "expected precision loss above 2^24");
2206 assert_eq!(decoded.ms, TWO_POW_24, "must snap down to the nearest f32");
2207 }
2208
2209 fn error_payloads() -> [String; 6] {
2216 [
2217 String::new(),
2218 String::from(" "),
2219 String::from("thick"),
2220 String::from("\u{1F600}\u{0301}"),
2221 String::from("nul\0inside"),
2222 "x".repeat(100_000),
2223 ]
2224 }
2225
2226 #[test]
2227 fn layout_scrollbar_width_error_round_trips_through_owned_and_back() {
2228 for payload in error_payloads() {
2229 let shared = LayoutScrollbarWidthParseError::InvalidValue(&payload);
2230 let owned = shared.to_contained();
2231 assert_eq!(
2232 owned,
2233 LayoutScrollbarWidthParseErrorOwned::InvalidValue(payload.clone().into())
2234 );
2235 assert_eq!(owned.to_shared(), shared, "owned -> shared lost information");
2236 assert_eq!(
2237 owned.to_shared().to_contained(),
2238 owned,
2239 "conversion is not idempotent"
2240 );
2241 }
2242 }
2243
2244 #[test]
2245 fn visibility_mode_error_round_trips_through_owned_and_back() {
2246 for payload in error_payloads() {
2247 let shared = ScrollbarVisibilityModeParseError::InvalidValue(&payload);
2248 let owned = shared.to_contained();
2249 assert_eq!(owned.to_shared(), shared);
2250 assert_eq!(owned.to_shared().to_contained(), owned);
2251 }
2252 }
2253
2254 #[test]
2255 fn fade_delay_and_duration_errors_round_trip_through_owned_and_back() {
2256 for payload in error_payloads() {
2257 let delay = ScrollbarFadeDelayParseError::InvalidValue(&payload);
2258 let owned_delay = delay.to_contained();
2259 assert_eq!(owned_delay.to_shared(), delay);
2260 assert_eq!(owned_delay.to_shared().to_contained(), owned_delay);
2261
2262 let duration = ScrollbarFadeDurationParseError::InvalidValue(&payload);
2263 let owned_duration = duration.to_contained();
2264 assert_eq!(owned_duration.to_shared(), duration);
2265 assert_eq!(owned_duration.to_shared().to_contained(), owned_duration);
2266 }
2267 }
2268
2269 #[test]
2270 fn scrollbar_color_invalid_value_error_round_trips_through_owned_and_back() {
2271 for payload in error_payloads() {
2272 let shared = StyleScrollbarColorParseError::InvalidValue(&payload);
2273 let owned = shared.to_contained();
2274 assert_eq!(
2275 owned,
2276 StyleScrollbarColorParseErrorOwned::InvalidValue(payload.clone().into())
2277 );
2278 assert_eq!(owned.to_shared(), shared);
2279 assert_eq!(owned.to_shared().to_contained(), owned);
2280 }
2281 }
2282
2283 #[cfg(feature = "parser")]
2286 #[test]
2287 fn scrollbar_color_nested_color_error_round_trips_through_owned_and_back() {
2288 let shared = parse_style_scrollbar_color("notacolor blue").unwrap_err();
2289 assert!(matches!(shared, StyleScrollbarColorParseError::Color(_)));
2290
2291 let owned = shared.to_contained();
2292 assert!(matches!(owned, StyleScrollbarColorParseErrorOwned::Color(_)));
2293 assert_eq!(owned.to_shared(), shared, "nested color error lost information");
2294 assert_eq!(owned.to_shared().to_contained(), owned);
2295 }
2296
2297 #[cfg(feature = "parser")]
2300 #[test]
2301 fn error_display_mentions_the_offending_input() {
2302 let width = parse_layout_scrollbar_width("thick").unwrap_err();
2303 assert!(format!("{width}").contains("thick"), "{width}");
2304 assert!(format!("{width:?}").contains("thick"), "{width:?}");
2305
2306 let color = parse_style_scrollbar_color("red").unwrap_err();
2307 assert!(format!("{color}").contains("red"), "{color}");
2308
2309 let vis = parse_scrollbar_visibility_mode("sometimes").unwrap_err();
2310 assert!(format!("{vis}").contains("sometimes"), "{vis}");
2311
2312 let delay = parse_scrollbar_fade_delay("soon").unwrap_err();
2313 assert!(format!("{delay}").contains("soon"), "{delay}");
2314
2315 let duration = parse_scrollbar_fade_duration("briefly").unwrap_err();
2316 assert!(format!("{duration}").contains("briefly"), "{duration}");
2317 }
2318
2319 #[test]
2322 fn error_display_does_not_panic_on_exotic_payloads() {
2323 for payload in error_payloads() {
2324 let err = LayoutScrollbarWidthParseError::InvalidValue(&payload);
2325 assert!(!format!("{err}").is_empty());
2326 let owned = err.to_contained();
2327 assert!(!format!("{}", owned.to_shared()).is_empty());
2328 }
2329 }
2330}