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
119impl Default for ScrollPhysics {
120 fn default() -> Self {
121 Self {
122 smooth_scroll_duration_ms: 300,
123 deceleration_rate: 0.95,
124 min_velocity_threshold: 50.0,
125 max_velocity: 8000.0,
126 wheel_multiplier: 1.0,
127 invert_direction: false,
128 overscroll_elasticity: 0.0, max_overscroll_distance: 100.0,
130 bounce_back_duration_ms: 400,
131 timer_interval_ms: 16,
132 }
133 }
134}
135
136impl ScrollPhysics {
137 #[must_use] pub const fn ios() -> Self {
139 Self {
140 smooth_scroll_duration_ms: 300,
141 deceleration_rate: 0.998,
142 min_velocity_threshold: 20.0,
143 max_velocity: 8000.0,
144 wheel_multiplier: 1.0,
145 invert_direction: true, overscroll_elasticity: 0.5,
147 max_overscroll_distance: 120.0,
148 bounce_back_duration_ms: 500,
149 timer_interval_ms: 16,
150 }
151 }
152
153 #[must_use] pub const fn macos() -> Self {
155 Self {
156 smooth_scroll_duration_ms: 250,
157 deceleration_rate: 0.997,
158 min_velocity_threshold: 30.0,
159 max_velocity: 6000.0,
160 wheel_multiplier: 1.0,
161 invert_direction: true, overscroll_elasticity: 0.3,
163 max_overscroll_distance: 80.0,
164 bounce_back_duration_ms: 400,
165 timer_interval_ms: 16,
166 }
167 }
168
169 #[must_use] pub const fn windows() -> Self {
171 Self {
172 smooth_scroll_duration_ms: 200,
173 deceleration_rate: 0.9,
174 min_velocity_threshold: 100.0,
175 max_velocity: 4000.0,
176 wheel_multiplier: 1.0,
177 invert_direction: false,
178 overscroll_elasticity: 0.0,
179 max_overscroll_distance: 0.0,
180 bounce_back_duration_ms: 200,
181 timer_interval_ms: 16,
182 }
183 }
184
185 #[must_use] pub const fn android() -> Self {
187 Self {
188 smooth_scroll_duration_ms: 250,
189 deceleration_rate: 0.996,
190 min_velocity_threshold: 40.0,
191 max_velocity: 8000.0,
192 wheel_multiplier: 1.0,
193 invert_direction: false,
194 overscroll_elasticity: 0.2, max_overscroll_distance: 60.0,
196 bounce_back_duration_ms: 300,
197 timer_interval_ms: 16,
198 }
199 }
200}
201
202#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
220#[repr(C)]
221pub enum ScrollbarVisibilityMode {
222 #[default]
225 Always,
226 WhenScrolling,
229 Auto,
231}
232
233impl PrintAsCssValue for ScrollbarVisibilityMode {
234 fn print_as_css_value(&self) -> String {
235 match self {
236 Self::Always => "always".to_string(),
237 Self::WhenScrolling => "when-scrolling".to_string(),
238 Self::Auto => "auto".to_string(),
239 }
240 }
241}
242
243#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
254#[repr(C)]
255pub struct ScrollbarFadeDelay {
256 pub ms: u32,
258}
259
260impl ScrollbarFadeDelay {
261 #[must_use] pub const fn new(ms: u32) -> Self { Self { ms } }
262 pub const ZERO: Self = Self { ms: 0 };
263}
264
265impl PrintAsCssValue for ScrollbarFadeDelay {
266 fn print_as_css_value(&self) -> String {
267 if self.ms == 0 { "0".to_string() } else { format!("{}ms", self.ms) }
268 }
269}
270
271#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
282#[repr(C)]
283pub struct ScrollbarFadeDuration {
284 pub ms: u32,
286}
287
288impl ScrollbarFadeDuration {
289 #[must_use] pub const fn new(ms: u32) -> Self { Self { ms } }
290 pub const ZERO: Self = Self { ms: 0 };
291}
292
293impl PrintAsCssValue for ScrollbarFadeDuration {
294 fn print_as_css_value(&self) -> String {
295 if self.ms == 0 { "0".to_string() } else { format!("{}ms", self.ms) }
296 }
297}
298
299#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
313#[repr(C)]
314pub enum OverflowScrolling {
315 #[default]
317 Auto,
318 Touch,
320}
321
322impl PrintAsCssValue for OverflowScrolling {
323 fn print_as_css_value(&self) -> String {
324 match self {
325 Self::Auto => "auto".to_string(),
326 Self::Touch => "touch".to_string(),
327 }
328 }
329}
330
331#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
337#[repr(C)]
338#[derive(Default)]
339pub enum LayoutScrollbarWidth {
340 #[default]
341 Auto,
342 Thin,
343 None,
344}
345
346
347impl PrintAsCssValue for LayoutScrollbarWidth {
348 fn print_as_css_value(&self) -> String {
349 match self {
350 Self::Auto => "auto".to_string(),
351 Self::Thin => "thin".to_string(),
352 Self::None => "none".to_string(),
353 }
354 }
355}
356
357#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
359#[repr(C)]
360pub struct ScrollbarColorCustom {
361 pub thumb: ColorU,
362 pub track: ColorU,
363}
364
365#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
367#[repr(C, u8)]
368#[derive(Default)]
369pub enum StyleScrollbarColor {
370 #[default]
371 Auto,
372 Custom(ScrollbarColorCustom),
373}
374
375
376impl PrintAsCssValue for StyleScrollbarColor {
377 fn print_as_css_value(&self) -> String {
378 match self {
379 Self::Auto => "auto".to_string(),
380 Self::Custom(c) => format!("{} {}", c.thumb.to_hash(), c.track.to_hash()),
381 }
382 }
383}
384
385#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
389#[repr(C)]
390pub struct ScrollbarInfo {
391 pub width: LayoutWidth,
393 pub padding_left: LayoutPaddingLeft,
396 pub padding_right: LayoutPaddingRight,
398 pub track: StyleBackgroundContent,
402 pub thumb: StyleBackgroundContent,
404 pub button: StyleBackgroundContent,
406 pub corner: StyleBackgroundContent,
409 pub resizer: StyleBackgroundContent,
412 pub clip_to_container_border: bool,
417 pub scroll_behavior: ScrollBehavior,
419 pub overscroll_behavior_x: OverscrollBehavior,
421 pub overscroll_behavior_y: OverscrollBehavior,
423 pub overflow_scrolling: OverflowScrolling,
426}
427
428impl Default for ScrollbarInfo {
429 fn default() -> Self {
430 SCROLLBAR_CLASSIC_LIGHT
431 }
432}
433
434impl PrintAsCssValue for ScrollbarInfo {
435 fn print_as_css_value(&self) -> String {
436 format!(
438 "width: {}; padding-left: {}; padding-right: {}; track: {}; thumb: {}; button: {}; \
439 corner: {}; resizer: {}",
440 self.width.print_as_css_value(),
441 self.padding_left.print_as_css_value(),
442 self.padding_right.print_as_css_value(),
443 self.track.print_as_css_value(),
444 self.thumb.print_as_css_value(),
445 self.button.print_as_css_value(),
446 self.corner.print_as_css_value(),
447 self.resizer.print_as_css_value(),
448 )
449 }
450}
451
452#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
454#[repr(C)]
455pub struct ScrollbarStyle {
456 pub horizontal: ScrollbarInfo,
458 pub vertical: ScrollbarInfo,
460}
461
462impl PrintAsCssValue for ScrollbarStyle {
463 fn print_as_css_value(&self) -> String {
464 format!(
466 "horz({}), vert({})",
467 self.horizontal.print_as_css_value(),
468 self.vertical.print_as_css_value()
469 )
470 }
471}
472
473impl crate::codegen::format::FormatAsRustCode for ScrollbarStyle {
475 fn format_as_rust_code(&self, tabs: usize) -> String {
476 let t = String::from(" ").repeat(tabs);
477 let t1 = String::from(" ").repeat(tabs + 1);
478 format!(
479 "ScrollbarStyle {{\r\n{}horizontal: {},\r\n{}vertical: {},\r\n{}}}",
480 t1,
481 crate::codegen::format::format_scrollbar_info(&self.horizontal, tabs + 1),
482 t1,
483 crate::codegen::format::format_scrollbar_info(&self.vertical, tabs + 1),
484 t,
485 )
486 }
487}
488
489impl crate::codegen::format::FormatAsRustCode for LayoutScrollbarWidth {
490 fn format_as_rust_code(&self, _tabs: usize) -> String {
491 match self {
492 Self::Auto => String::from("LayoutScrollbarWidth::Auto"),
493 Self::Thin => String::from("LayoutScrollbarWidth::Thin"),
494 Self::None => String::from("LayoutScrollbarWidth::None"),
495 }
496 }
497}
498
499impl crate::codegen::format::FormatAsRustCode for StyleScrollbarColor {
500 fn format_as_rust_code(&self, _tabs: usize) -> String {
501 match self {
502 Self::Auto => String::from("StyleScrollbarColor::Auto"),
503 Self::Custom(c) => format!(
504 "StyleScrollbarColor::Custom(ScrollbarColorCustom {{ thumb: {}, track: {} }})",
505 crate::codegen::format::format_color_value(&c.thumb),
506 crate::codegen::format::format_color_value(&c.track)
507 ),
508 }
509 }
510}
511
512impl crate::codegen::format::FormatAsRustCode for ScrollbarVisibilityMode {
513 fn format_as_rust_code(&self, _tabs: usize) -> String {
514 match self {
515 Self::Always => String::from("ScrollbarVisibilityMode::Always"),
516 Self::WhenScrolling => String::from("ScrollbarVisibilityMode::WhenScrolling"),
517 Self::Auto => String::from("ScrollbarVisibilityMode::Auto"),
518 }
519 }
520}
521
522impl crate::codegen::format::FormatAsRustCode for ScrollbarFadeDelay {
523 fn format_as_rust_code(&self, _tabs: usize) -> String {
524 format!("ScrollbarFadeDelay::new({})", self.ms)
525 }
526}
527
528impl crate::codegen::format::FormatAsRustCode for ScrollbarFadeDuration {
529 fn format_as_rust_code(&self, _tabs: usize) -> String {
530 format!("ScrollbarFadeDuration::new({})", self.ms)
531 }
532}
533
534#[derive(Debug, Clone, PartialEq, Eq)]
539pub struct ComputedScrollbarStyle {
540 pub width: Option<LayoutWidth>,
542 pub thumb_color: Option<ColorU>,
544 pub track_color: Option<ColorU>,
546}
547
548impl Default for ComputedScrollbarStyle {
549 fn default() -> Self {
550 let default_info = ScrollbarInfo::default();
551 Self {
552 width: Some(default_info.width), thumb_color: match default_info.thumb {
554 StyleBackgroundContent::Color(c) => Some(c),
555 _ => None,
556 },
557 track_color: match default_info.track {
558 StyleBackgroundContent::Color(c) => Some(c),
559 _ => None,
560 },
561 }
562 }
563}
564
565pub const SCROLLBAR_CLASSIC_LIGHT: ScrollbarInfo = ScrollbarInfo {
569 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(17)),
570 padding_left: LayoutPaddingLeft {
571 inner: crate::props::basic::pixel::PixelValue::const_px(2),
572 },
573 padding_right: LayoutPaddingRight {
574 inner: crate::props::basic::pixel::PixelValue::const_px(2),
575 },
576 track: StyleBackgroundContent::Color(ColorU {
577 r: 241,
578 g: 241,
579 b: 241,
580 a: 255,
581 }),
582 thumb: StyleBackgroundContent::Color(ColorU {
583 r: 193,
584 g: 193,
585 b: 193,
586 a: 255,
587 }),
588 button: StyleBackgroundContent::Color(ColorU {
589 r: 163,
590 g: 163,
591 b: 163,
592 a: 255,
593 }),
594 corner: StyleBackgroundContent::Color(ColorU {
595 r: 241,
596 g: 241,
597 b: 241,
598 a: 255,
599 }),
600 resizer: StyleBackgroundContent::Color(ColorU {
601 r: 241,
602 g: 241,
603 b: 241,
604 a: 255,
605 }),
606 clip_to_container_border: false,
607 scroll_behavior: ScrollBehavior::Auto,
608 overscroll_behavior_x: OverscrollBehavior::Auto,
609 overscroll_behavior_y: OverscrollBehavior::Auto,
610 overflow_scrolling: OverflowScrolling::Auto,
611};
612
613pub const SCROLLBAR_CLASSIC_DARK: ScrollbarInfo = ScrollbarInfo {
615 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(17)),
616 padding_left: LayoutPaddingLeft {
617 inner: crate::props::basic::pixel::PixelValue::const_px(2),
618 },
619 padding_right: LayoutPaddingRight {
620 inner: crate::props::basic::pixel::PixelValue::const_px(2),
621 },
622 track: StyleBackgroundContent::Color(ColorU {
623 r: 45,
624 g: 45,
625 b: 45,
626 a: 255,
627 }),
628 thumb: StyleBackgroundContent::Color(ColorU {
629 r: 100,
630 g: 100,
631 b: 100,
632 a: 255,
633 }),
634 button: StyleBackgroundContent::Color(ColorU {
635 r: 120,
636 g: 120,
637 b: 120,
638 a: 255,
639 }),
640 corner: StyleBackgroundContent::Color(ColorU {
641 r: 45,
642 g: 45,
643 b: 45,
644 a: 255,
645 }),
646 resizer: StyleBackgroundContent::Color(ColorU {
647 r: 45,
648 g: 45,
649 b: 45,
650 a: 255,
651 }),
652 clip_to_container_border: false,
653 scroll_behavior: ScrollBehavior::Auto,
654 overscroll_behavior_x: OverscrollBehavior::Auto,
655 overscroll_behavior_y: OverscrollBehavior::Auto,
656 overflow_scrolling: OverflowScrolling::Auto,
657};
658
659pub const SCROLLBAR_MACOS_LIGHT: ScrollbarInfo = ScrollbarInfo {
661 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(8)),
662 padding_left: LayoutPaddingLeft {
663 inner: crate::props::basic::pixel::PixelValue::const_px(0),
664 },
665 padding_right: LayoutPaddingRight {
666 inner: crate::props::basic::pixel::PixelValue::const_px(0),
667 },
668 track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
669 thumb: StyleBackgroundContent::Color(ColorU {
670 r: 0,
671 g: 0,
672 b: 0,
673 a: 100,
674 }), button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
676 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
677 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
678 clip_to_container_border: true, scroll_behavior: ScrollBehavior::Smooth,
680 overscroll_behavior_x: OverscrollBehavior::Auto,
681 overscroll_behavior_y: OverscrollBehavior::Auto,
682 overflow_scrolling: OverflowScrolling::Auto,
683};
684
685pub const SCROLLBAR_MACOS_DARK: ScrollbarInfo = ScrollbarInfo {
687 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(8)),
688 padding_left: LayoutPaddingLeft {
689 inner: crate::props::basic::pixel::PixelValue::const_px(0),
690 },
691 padding_right: LayoutPaddingRight {
692 inner: crate::props::basic::pixel::PixelValue::const_px(0),
693 },
694 track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
695 thumb: StyleBackgroundContent::Color(ColorU {
696 r: 255,
697 g: 255,
698 b: 255,
699 a: 100,
700 }), button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
702 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
703 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
704 clip_to_container_border: true, scroll_behavior: ScrollBehavior::Smooth,
706 overscroll_behavior_x: OverscrollBehavior::Auto,
707 overscroll_behavior_y: OverscrollBehavior::Auto,
708 overflow_scrolling: OverflowScrolling::Auto,
709};
710
711pub const SCROLLBAR_WINDOWS_LIGHT: ScrollbarInfo = ScrollbarInfo {
713 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(12)),
714 padding_left: LayoutPaddingLeft {
715 inner: crate::props::basic::pixel::PixelValue::const_px(0),
716 },
717 padding_right: LayoutPaddingRight {
718 inner: crate::props::basic::pixel::PixelValue::const_px(0),
719 },
720 track: StyleBackgroundContent::Color(ColorU {
721 r: 241,
722 g: 241,
723 b: 241,
724 a: 255,
725 }),
726 thumb: StyleBackgroundContent::Color(ColorU {
727 r: 130,
728 g: 130,
729 b: 130,
730 a: 255,
731 }),
732 button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
733 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
734 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
735 clip_to_container_border: false,
736 scroll_behavior: ScrollBehavior::Auto,
737 overscroll_behavior_x: OverscrollBehavior::None,
738 overscroll_behavior_y: OverscrollBehavior::None,
739 overflow_scrolling: OverflowScrolling::Auto,
740};
741
742pub const SCROLLBAR_WINDOWS_DARK: ScrollbarInfo = ScrollbarInfo {
744 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(12)),
745 padding_left: LayoutPaddingLeft {
746 inner: crate::props::basic::pixel::PixelValue::const_px(0),
747 },
748 padding_right: LayoutPaddingRight {
749 inner: crate::props::basic::pixel::PixelValue::const_px(0),
750 },
751 track: StyleBackgroundContent::Color(ColorU {
752 r: 32,
753 g: 32,
754 b: 32,
755 a: 255,
756 }),
757 thumb: StyleBackgroundContent::Color(ColorU {
758 r: 110,
759 g: 110,
760 b: 110,
761 a: 255,
762 }),
763 button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
764 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
765 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
766 clip_to_container_border: false,
767 scroll_behavior: ScrollBehavior::Auto,
768 overscroll_behavior_x: OverscrollBehavior::None,
769 overscroll_behavior_y: OverscrollBehavior::None,
770 overflow_scrolling: OverflowScrolling::Auto,
771};
772
773pub const SCROLLBAR_IOS_LIGHT: ScrollbarInfo = ScrollbarInfo {
775 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(7)),
776 padding_left: LayoutPaddingLeft {
777 inner: crate::props::basic::pixel::PixelValue::const_px(0),
778 },
779 padding_right: LayoutPaddingRight {
780 inner: crate::props::basic::pixel::PixelValue::const_px(0),
781 },
782 track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
783 thumb: StyleBackgroundContent::Color(ColorU {
784 r: 0,
785 g: 0,
786 b: 0,
787 a: 102,
788 }), button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
790 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
791 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
792 clip_to_container_border: true, scroll_behavior: ScrollBehavior::Smooth,
794 overscroll_behavior_x: OverscrollBehavior::Auto,
795 overscroll_behavior_y: OverscrollBehavior::Auto,
796 overflow_scrolling: OverflowScrolling::Auto,
797};
798
799pub const SCROLLBAR_IOS_DARK: ScrollbarInfo = ScrollbarInfo {
801 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(7)),
802 padding_left: LayoutPaddingLeft {
803 inner: crate::props::basic::pixel::PixelValue::const_px(0),
804 },
805 padding_right: LayoutPaddingRight {
806 inner: crate::props::basic::pixel::PixelValue::const_px(0),
807 },
808 track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
809 thumb: StyleBackgroundContent::Color(ColorU {
810 r: 255,
811 g: 255,
812 b: 255,
813 a: 102,
814 }), button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
816 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
817 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
818 clip_to_container_border: true, scroll_behavior: ScrollBehavior::Smooth,
820 overscroll_behavior_x: OverscrollBehavior::Auto,
821 overscroll_behavior_y: OverscrollBehavior::Auto,
822 overflow_scrolling: OverflowScrolling::Auto,
823};
824
825pub const SCROLLBAR_ANDROID_LIGHT: ScrollbarInfo = ScrollbarInfo {
827 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(6)),
828 padding_left: LayoutPaddingLeft {
829 inner: crate::props::basic::pixel::PixelValue::const_px(0),
830 },
831 padding_right: LayoutPaddingRight {
832 inner: crate::props::basic::pixel::PixelValue::const_px(0),
833 },
834 track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
835 thumb: StyleBackgroundContent::Color(ColorU {
836 r: 0,
837 g: 0,
838 b: 0,
839 a: 102,
840 }), button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
842 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
843 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
844 clip_to_container_border: true, scroll_behavior: ScrollBehavior::Smooth,
846 overscroll_behavior_x: OverscrollBehavior::Contain,
847 overscroll_behavior_y: OverscrollBehavior::Auto,
848 overflow_scrolling: OverflowScrolling::Auto,
849};
850
851pub const SCROLLBAR_ANDROID_DARK: ScrollbarInfo = ScrollbarInfo {
853 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(6)),
854 padding_left: LayoutPaddingLeft {
855 inner: crate::props::basic::pixel::PixelValue::const_px(0),
856 },
857 padding_right: LayoutPaddingRight {
858 inner: crate::props::basic::pixel::PixelValue::const_px(0),
859 },
860 track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
861 thumb: StyleBackgroundContent::Color(ColorU {
862 r: 255,
863 g: 255,
864 b: 255,
865 a: 102,
866 }), button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
868 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
869 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
870 clip_to_container_border: true, scroll_behavior: ScrollBehavior::Smooth,
872 overscroll_behavior_x: OverscrollBehavior::Contain,
873 overscroll_behavior_y: OverscrollBehavior::Auto,
874 overflow_scrolling: OverflowScrolling::Auto,
875};
876
877#[derive(Clone, PartialEq, Eq)]
880pub enum LayoutScrollbarWidthParseError<'a> {
881 InvalidValue(&'a str),
882}
883impl_debug_as_display!(LayoutScrollbarWidthParseError<'a>);
884impl_display! { LayoutScrollbarWidthParseError<'a>, {
885 InvalidValue(v) => format!("Invalid scrollbar-width value: \"{}\"", v),
886}}
887
888#[derive(Debug, Clone, PartialEq, Eq)]
889#[repr(C, u8)]
890pub enum LayoutScrollbarWidthParseErrorOwned {
891 InvalidValue(AzString),
892}
893impl LayoutScrollbarWidthParseError<'_> {
894 #[must_use] pub fn to_contained(&self) -> LayoutScrollbarWidthParseErrorOwned {
895 match self {
896 Self::InvalidValue(s) => {
897 LayoutScrollbarWidthParseErrorOwned::InvalidValue((*s).to_string().into())
898 }
899 }
900 }
901}
902impl LayoutScrollbarWidthParseErrorOwned {
903 #[must_use] pub fn to_shared(&self) -> LayoutScrollbarWidthParseError<'_> {
904 match self {
905 Self::InvalidValue(s) => LayoutScrollbarWidthParseError::InvalidValue(s.as_str()),
906 }
907 }
908}
909
910#[cfg(feature = "parser")]
911pub fn parse_layout_scrollbar_width(
915 input: &str,
916) -> Result<LayoutScrollbarWidth, LayoutScrollbarWidthParseError<'_>> {
917 match input.trim() {
918 "auto" => Ok(LayoutScrollbarWidth::Auto),
919 "thin" => Ok(LayoutScrollbarWidth::Thin),
920 "none" => Ok(LayoutScrollbarWidth::None),
921 _ => Err(LayoutScrollbarWidthParseError::InvalidValue(input)),
922 }
923}
924
925#[derive(Clone, PartialEq)]
926pub enum StyleScrollbarColorParseError<'a> {
927 InvalidValue(&'a str),
928 Color(CssColorParseError<'a>),
929}
930impl_debug_as_display!(StyleScrollbarColorParseError<'a>);
931impl_display! { StyleScrollbarColorParseError<'a>, {
932 InvalidValue(v) => format!("Invalid scrollbar-color value: \"{}\"", v),
933 Color(e) => format!("Invalid color in scrollbar-color: {}", e),
934}}
935impl_from!(CssColorParseError<'a>, StyleScrollbarColorParseError::Color);
936
937#[derive(Debug, Clone, PartialEq)]
938#[repr(C, u8)]
939pub enum StyleScrollbarColorParseErrorOwned {
940 InvalidValue(AzString),
941 Color(CssColorParseErrorOwned),
942}
943impl StyleScrollbarColorParseError<'_> {
944 #[must_use] pub fn to_contained(&self) -> StyleScrollbarColorParseErrorOwned {
945 match self {
946 Self::InvalidValue(s) => {
947 StyleScrollbarColorParseErrorOwned::InvalidValue((*s).to_string().into())
948 }
949 Self::Color(e) => StyleScrollbarColorParseErrorOwned::Color(e.to_contained()),
950 }
951 }
952}
953impl StyleScrollbarColorParseErrorOwned {
954 #[must_use] pub fn to_shared(&self) -> StyleScrollbarColorParseError<'_> {
955 match self {
956 Self::InvalidValue(s) => StyleScrollbarColorParseError::InvalidValue(s.as_str()),
957 Self::Color(e) => StyleScrollbarColorParseError::Color(e.to_shared()),
958 }
959 }
960}
961
962#[cfg(feature = "parser")]
963pub fn parse_style_scrollbar_color(
967 input: &str,
968) -> Result<StyleScrollbarColor, StyleScrollbarColorParseError<'_>> {
969 let input = input.trim();
970 if input == "auto" {
971 return Ok(StyleScrollbarColor::Auto);
972 }
973
974 let mut parts = input.split_whitespace();
975 let thumb_str = parts
976 .next()
977 .ok_or(StyleScrollbarColorParseError::InvalidValue(input))?;
978 let track_str = parts
979 .next()
980 .ok_or(StyleScrollbarColorParseError::InvalidValue(input))?;
981
982 if parts.next().is_some() {
983 return Err(StyleScrollbarColorParseError::InvalidValue(input));
984 }
985
986 let thumb = parse_css_color(thumb_str)?;
987 let track = parse_css_color(track_str)?;
988
989 Ok(StyleScrollbarColor::Custom(ScrollbarColorCustom {
990 thumb,
991 track,
992 }))
993}
994
995#[derive(Clone, PartialEq, Eq)]
998pub enum ScrollbarVisibilityModeParseError<'a> {
999 InvalidValue(&'a str),
1000}
1001impl_debug_as_display!(ScrollbarVisibilityModeParseError<'a>);
1002impl_display! { ScrollbarVisibilityModeParseError<'a>, {
1003 InvalidValue(v) => format!("Invalid scrollbar-visibility value: \"{}\"", v),
1004}}
1005
1006#[derive(Debug, Clone, PartialEq, Eq)]
1007#[repr(C, u8)]
1008pub enum ScrollbarVisibilityModeParseErrorOwned {
1009 InvalidValue(AzString),
1010}
1011impl ScrollbarVisibilityModeParseError<'_> {
1012 #[must_use] pub fn to_contained(&self) -> ScrollbarVisibilityModeParseErrorOwned {
1013 match self {
1014 Self::InvalidValue(s) => ScrollbarVisibilityModeParseErrorOwned::InvalidValue((*s).to_string().into()),
1015 }
1016 }
1017}
1018impl ScrollbarVisibilityModeParseErrorOwned {
1019 #[must_use] pub fn to_shared(&self) -> ScrollbarVisibilityModeParseError<'_> {
1020 match self {
1021 Self::InvalidValue(s) => ScrollbarVisibilityModeParseError::InvalidValue(s.as_str()),
1022 }
1023 }
1024}
1025
1026#[cfg(feature = "parser")]
1027pub fn parse_scrollbar_visibility_mode(
1031 input: &str,
1032) -> Result<ScrollbarVisibilityMode, ScrollbarVisibilityModeParseError<'_>> {
1033 match input.trim() {
1034 "always" => Ok(ScrollbarVisibilityMode::Always),
1035 "when-scrolling" => Ok(ScrollbarVisibilityMode::WhenScrolling),
1036 "auto" => Ok(ScrollbarVisibilityMode::Auto),
1037 _ => Err(ScrollbarVisibilityModeParseError::InvalidValue(input)),
1038 }
1039}
1040
1041#[derive(Clone, PartialEq, Eq)]
1044pub enum ScrollbarFadeDelayParseError<'a> {
1045 InvalidValue(&'a str),
1046}
1047impl_debug_as_display!(ScrollbarFadeDelayParseError<'a>);
1048impl_display! { ScrollbarFadeDelayParseError<'a>, {
1049 InvalidValue(v) => format!("Invalid scrollbar-fade-delay value: \"{}\"", v),
1050}}
1051
1052#[derive(Debug, Clone, PartialEq, Eq)]
1053#[repr(C, u8)]
1054pub enum ScrollbarFadeDelayParseErrorOwned {
1055 InvalidValue(AzString),
1056}
1057impl ScrollbarFadeDelayParseError<'_> {
1058 #[must_use] pub fn to_contained(&self) -> ScrollbarFadeDelayParseErrorOwned {
1059 match self {
1060 Self::InvalidValue(s) => ScrollbarFadeDelayParseErrorOwned::InvalidValue((*s).to_string().into()),
1061 }
1062 }
1063}
1064impl ScrollbarFadeDelayParseErrorOwned {
1065 #[must_use] pub fn to_shared(&self) -> ScrollbarFadeDelayParseError<'_> {
1066 match self {
1067 Self::InvalidValue(s) => ScrollbarFadeDelayParseError::InvalidValue(s.as_str()),
1068 }
1069 }
1070}
1071
1072#[cfg(feature = "parser")]
1078fn parse_time_ms(input: &str) -> Option<u32> {
1079 crate::props::basic::time::parse_duration(input).ok().map(|d| d.millis())
1080}
1081
1082#[cfg(feature = "parser")]
1083pub fn parse_scrollbar_fade_delay(
1087 input: &str,
1088) -> Result<ScrollbarFadeDelay, ScrollbarFadeDelayParseError<'_>> {
1089 parse_time_ms(input)
1090 .map(ScrollbarFadeDelay::new)
1091 .ok_or(ScrollbarFadeDelayParseError::InvalidValue(input))
1092}
1093
1094#[derive(Clone, PartialEq, Eq)]
1097pub enum ScrollbarFadeDurationParseError<'a> {
1098 InvalidValue(&'a str),
1099}
1100impl_debug_as_display!(ScrollbarFadeDurationParseError<'a>);
1101impl_display! { ScrollbarFadeDurationParseError<'a>, {
1102 InvalidValue(v) => format!("Invalid scrollbar-fade-duration value: \"{}\"", v),
1103}}
1104
1105#[derive(Debug, Clone, PartialEq, Eq)]
1106#[repr(C, u8)]
1107pub enum ScrollbarFadeDurationParseErrorOwned {
1108 InvalidValue(AzString),
1109}
1110impl ScrollbarFadeDurationParseError<'_> {
1111 #[must_use] pub fn to_contained(&self) -> ScrollbarFadeDurationParseErrorOwned {
1112 match self {
1113 Self::InvalidValue(s) => ScrollbarFadeDurationParseErrorOwned::InvalidValue((*s).to_string().into()),
1114 }
1115 }
1116}
1117impl ScrollbarFadeDurationParseErrorOwned {
1118 #[must_use] pub fn to_shared(&self) -> ScrollbarFadeDurationParseError<'_> {
1119 match self {
1120 Self::InvalidValue(s) => ScrollbarFadeDurationParseError::InvalidValue(s.as_str()),
1121 }
1122 }
1123}
1124
1125#[cfg(feature = "parser")]
1126pub fn parse_scrollbar_fade_duration(
1130 input: &str,
1131) -> Result<ScrollbarFadeDuration, ScrollbarFadeDurationParseError<'_>> {
1132 parse_time_ms(input)
1133 .map(ScrollbarFadeDuration::new)
1134 .ok_or(ScrollbarFadeDurationParseError::InvalidValue(input))
1135}
1136
1137#[cfg(all(test, feature = "parser"))]
1138mod tests {
1139 use super::*;
1140 use crate::props::basic::color::ColorU;
1141
1142 #[test]
1143 fn test_parse_scrollbar_width() {
1144 assert_eq!(
1145 parse_layout_scrollbar_width("auto").unwrap(),
1146 LayoutScrollbarWidth::Auto
1147 );
1148 assert_eq!(
1149 parse_layout_scrollbar_width("thin").unwrap(),
1150 LayoutScrollbarWidth::Thin
1151 );
1152 assert_eq!(
1153 parse_layout_scrollbar_width("none").unwrap(),
1154 LayoutScrollbarWidth::None
1155 );
1156 assert!(parse_layout_scrollbar_width("thick").is_err());
1157 }
1158
1159 #[test]
1160 fn test_parse_scrollbar_color() {
1161 assert_eq!(
1162 parse_style_scrollbar_color("auto").unwrap(),
1163 StyleScrollbarColor::Auto
1164 );
1165
1166 let custom = parse_style_scrollbar_color("red blue").unwrap();
1167 assert_eq!(
1168 custom,
1169 StyleScrollbarColor::Custom(ScrollbarColorCustom {
1170 thumb: ColorU::RED,
1171 track: ColorU::BLUE
1172 })
1173 );
1174
1175 let custom_hex = parse_style_scrollbar_color("#ff0000 #0000ff").unwrap();
1176 assert_eq!(
1177 custom_hex,
1178 StyleScrollbarColor::Custom(ScrollbarColorCustom {
1179 thumb: ColorU::RED,
1180 track: ColorU::BLUE
1181 })
1182 );
1183
1184 assert!(parse_style_scrollbar_color("red").is_err());
1185 assert!(parse_style_scrollbar_color("red blue green").is_err());
1186 }
1187}
1188
1189#[cfg(test)]
1190#[allow(clippy::unreadable_literal, clippy::float_cmp)]
1191mod autotest_generated {
1192 use super::*;
1193 use crate::codegen::format::FormatAsRustCode;
1194
1195 #[cfg(feature = "parser")]
1199 const TWO_POW_24: u32 = 16_777_216;
1200
1201 #[cfg(feature = "parser")]
1203 const GARBAGE: &[&str] = &[
1204 "",
1205 " ",
1206 " ",
1207 "\t\n",
1208 "\u{a0}", "\0",
1210 "\u{1F600}", "e\u{0301}", "\u{202e}auto", "аuto", "AUTO",
1215 "auto;",
1216 "auto garbage",
1217 "-1",
1218 "NaN",
1219 "inf",
1220 "0x10",
1221 "9223372036854775807", "1e400",
1223 "{[(<",
1224 ];
1225
1226 fn assert_physics_invariants(p: ScrollPhysics, name: &str) {
1235 for (field, v) in [
1236 ("deceleration_rate", p.deceleration_rate),
1237 ("min_velocity_threshold", p.min_velocity_threshold),
1238 ("max_velocity", p.max_velocity),
1239 ("wheel_multiplier", p.wheel_multiplier),
1240 ("overscroll_elasticity", p.overscroll_elasticity),
1241 ("max_overscroll_distance", p.max_overscroll_distance),
1242 ] {
1243 assert!(v.is_finite(), "{name}.{field} is not finite: {v}");
1244 assert!(!v.is_nan(), "{name}.{field} is NaN");
1245 assert!(v >= 0.0, "{name}.{field} is negative: {v}");
1246 }
1247
1248 assert!(
1249 p.deceleration_rate > 0.0 && p.deceleration_rate < 1.0,
1250 "{name}.deceleration_rate must stay inside (0.0, 1.0) or momentum never stops: {}",
1251 p.deceleration_rate
1252 );
1253 assert!(
1254 (0.0..=1.0).contains(&p.overscroll_elasticity),
1255 "{name}.overscroll_elasticity out of [0.0, 1.0]: {}",
1256 p.overscroll_elasticity
1257 );
1258 assert!(
1259 p.max_velocity > p.min_velocity_threshold,
1260 "{name}: max_velocity ({}) must exceed min_velocity_threshold ({})",
1261 p.max_velocity,
1262 p.min_velocity_threshold
1263 );
1264 assert!(
1265 p.wheel_multiplier > 0.0,
1266 "{name}.wheel_multiplier must be > 0 or the wheel does nothing"
1267 );
1268 assert!(
1269 p.timer_interval_ms > 0,
1270 "{name}.timer_interval_ms == 0 would spin the physics timer"
1271 );
1272 assert!(
1273 p.smooth_scroll_duration_ms > 0,
1274 "{name}.smooth_scroll_duration_ms == 0 makes `scroll-behavior: smooth` a no-op"
1275 );
1276 }
1277
1278 #[test]
1279 fn scroll_physics_presets_hold_their_invariants() {
1280 assert_physics_invariants(ScrollPhysics::default(), "default");
1281 assert_physics_invariants(ScrollPhysics::ios(), "ios");
1282 assert_physics_invariants(ScrollPhysics::macos(), "macos");
1283 assert_physics_invariants(ScrollPhysics::windows(), "windows");
1284 assert_physics_invariants(ScrollPhysics::android(), "android");
1285 }
1286
1287 #[test]
1288 fn scroll_physics_presets_are_pure_and_distinct() {
1289 assert_eq!(ScrollPhysics::ios(), ScrollPhysics::ios());
1291 assert_eq!(ScrollPhysics::windows(), ScrollPhysics::windows());
1292
1293 assert_ne!(ScrollPhysics::ios(), ScrollPhysics::macos());
1295 assert_ne!(ScrollPhysics::ios(), ScrollPhysics::android());
1296 assert_ne!(ScrollPhysics::macos(), ScrollPhysics::windows());
1297 assert_ne!(ScrollPhysics::android(), ScrollPhysics::windows());
1298 assert_ne!(ScrollPhysics::default(), ScrollPhysics::ios());
1299 }
1300
1301 #[test]
1304 fn scroll_physics_presets_match_their_documented_platform_behavior() {
1305 let win = ScrollPhysics::windows();
1306 assert_eq!(win.overscroll_elasticity, 0.0);
1307 assert_eq!(win.max_overscroll_distance, 0.0);
1308 assert!(!win.invert_direction);
1309
1310 assert!(ScrollPhysics::ios().invert_direction);
1311 assert!(ScrollPhysics::macos().invert_direction);
1312 assert!(!ScrollPhysics::android().invert_direction);
1313
1314 assert!(ScrollPhysics::ios().deceleration_rate > ScrollPhysics::windows().deceleration_rate);
1316
1317 assert_eq!(ScrollPhysics::default().overscroll_elasticity, 0.0);
1319 }
1320
1321 #[test]
1326 fn fade_delay_and_duration_constructors_store_their_argument_verbatim() {
1327 for ms in [0u32, 1, 16, 500, u32::MAX / 2, u32::MAX - 1, u32::MAX] {
1328 assert_eq!(ScrollbarFadeDelay::new(ms).ms, ms);
1329 assert_eq!(ScrollbarFadeDuration::new(ms).ms, ms);
1330 }
1331 }
1332
1333 #[test]
1334 fn fade_zero_constants_agree_with_new_and_default() {
1335 assert_eq!(ScrollbarFadeDelay::ZERO, ScrollbarFadeDelay::new(0));
1336 assert_eq!(ScrollbarFadeDelay::ZERO, ScrollbarFadeDelay::default());
1337 assert_eq!(ScrollbarFadeDuration::ZERO, ScrollbarFadeDuration::new(0));
1338 assert_eq!(ScrollbarFadeDuration::ZERO, ScrollbarFadeDuration::default());
1339 assert_eq!(ScrollbarFadeDelay::ZERO.ms, 0);
1340 assert_eq!(ScrollbarFadeDuration::ZERO.ms, 0);
1341 }
1342
1343 #[test]
1346 fn fade_delay_orders_by_millisecond_count() {
1347 assert!(ScrollbarFadeDelay::new(0) < ScrollbarFadeDelay::new(1));
1348 assert!(ScrollbarFadeDelay::new(499) < ScrollbarFadeDelay::new(500));
1349 assert!(ScrollbarFadeDelay::new(u32::MAX) > ScrollbarFadeDelay::new(u32::MAX - 1));
1350 assert!(ScrollbarFadeDuration::new(0) < ScrollbarFadeDuration::new(u32::MAX));
1351 }
1352
1353 #[test]
1358 fn enum_printers_emit_the_css_keywords() {
1359 assert_eq!(ScrollBehavior::Auto.print_as_css_value(), "auto");
1360 assert_eq!(ScrollBehavior::Smooth.print_as_css_value(), "smooth");
1361 assert_eq!(ScrollBehavior::default(), ScrollBehavior::Auto);
1362
1363 assert_eq!(OverscrollBehavior::Auto.print_as_css_value(), "auto");
1364 assert_eq!(OverscrollBehavior::Contain.print_as_css_value(), "contain");
1365 assert_eq!(OverscrollBehavior::None.print_as_css_value(), "none");
1366 assert_eq!(OverscrollBehavior::default(), OverscrollBehavior::Auto);
1367
1368 assert_eq!(OverflowScrolling::Auto.print_as_css_value(), "auto");
1369 assert_eq!(OverflowScrolling::Touch.print_as_css_value(), "touch");
1370 assert_eq!(OverflowScrolling::default(), OverflowScrolling::Auto);
1371
1372 assert_eq!(LayoutScrollbarWidth::Auto.print_as_css_value(), "auto");
1373 assert_eq!(LayoutScrollbarWidth::Thin.print_as_css_value(), "thin");
1374 assert_eq!(LayoutScrollbarWidth::None.print_as_css_value(), "none");
1375 assert_eq!(LayoutScrollbarWidth::default(), LayoutScrollbarWidth::Auto);
1376
1377 assert_eq!(ScrollbarVisibilityMode::Always.print_as_css_value(), "always");
1378 assert_eq!(
1379 ScrollbarVisibilityMode::WhenScrolling.print_as_css_value(),
1380 "when-scrolling"
1381 );
1382 assert_eq!(ScrollbarVisibilityMode::Auto.print_as_css_value(), "auto");
1383 assert_eq!(
1384 ScrollbarVisibilityMode::default(),
1385 ScrollbarVisibilityMode::Always
1386 );
1387 }
1388
1389 #[test]
1393 fn fade_printers_special_case_zero_and_keep_the_unit_otherwise() {
1394 assert_eq!(ScrollbarFadeDelay::new(0).print_as_css_value(), "0");
1395 assert_eq!(ScrollbarFadeDelay::new(1).print_as_css_value(), "1ms");
1396 assert_eq!(ScrollbarFadeDelay::new(500).print_as_css_value(), "500ms");
1397 assert_eq!(
1398 ScrollbarFadeDelay::new(u32::MAX).print_as_css_value(),
1399 "4294967295ms"
1400 );
1401 assert_eq!(ScrollbarFadeDuration::new(0).print_as_css_value(), "0");
1402 assert_eq!(ScrollbarFadeDuration::new(200).print_as_css_value(), "200ms");
1403 assert_eq!(
1404 ScrollbarFadeDuration::new(u32::MAX).print_as_css_value(),
1405 "4294967295ms"
1406 );
1407 }
1408
1409 #[test]
1410 fn scrollbar_color_printer_emits_two_eight_digit_hashes() {
1411 assert_eq!(StyleScrollbarColor::Auto.print_as_css_value(), "auto");
1412 assert_eq!(StyleScrollbarColor::default(), StyleScrollbarColor::Auto);
1413
1414 let custom = StyleScrollbarColor::Custom(ScrollbarColorCustom {
1415 thumb: ColorU::RED,
1416 track: ColorU::TRANSPARENT,
1417 });
1418 assert_eq!(custom.print_as_css_value(), "#ff0000ff #00000000");
1419 }
1420
1421 #[test]
1424 fn aggregate_printers_do_not_panic_and_mention_both_axes() {
1425 let printed = ScrollbarStyle::default().print_as_css_value();
1426 assert!(printed.contains("horz("), "{printed}");
1427 assert!(printed.contains("vert("), "{printed}");
1428
1429 let info = ScrollbarInfo::default().print_as_css_value();
1430 assert!(info.contains("width:"), "{info}");
1431 assert!(info.contains("thumb:"), "{info}");
1432 assert!(info.contains("resizer:"), "{info}");
1433 }
1434
1435 #[test]
1440 fn format_as_rust_code_emits_constructible_expressions() {
1441 assert_eq!(
1442 LayoutScrollbarWidth::Thin.format_as_rust_code(0),
1443 "LayoutScrollbarWidth::Thin"
1444 );
1445 assert_eq!(
1446 LayoutScrollbarWidth::None.format_as_rust_code(7),
1447 "LayoutScrollbarWidth::None",
1448 "indent depth must not leak into a unit-variant literal"
1449 );
1450 assert_eq!(
1451 ScrollbarVisibilityMode::WhenScrolling.format_as_rust_code(0),
1452 "ScrollbarVisibilityMode::WhenScrolling"
1453 );
1454 assert_eq!(
1455 StyleScrollbarColor::Auto.format_as_rust_code(0),
1456 "StyleScrollbarColor::Auto"
1457 );
1458
1459 assert_eq!(
1461 ScrollbarFadeDelay::new(0).format_as_rust_code(0),
1462 "ScrollbarFadeDelay::new(0)"
1463 );
1464 assert_eq!(
1465 ScrollbarFadeDelay::new(u32::MAX).format_as_rust_code(3),
1466 "ScrollbarFadeDelay::new(4294967295)"
1467 );
1468 assert_eq!(
1469 ScrollbarFadeDuration::new(u32::MAX).format_as_rust_code(0),
1470 "ScrollbarFadeDuration::new(4294967295)"
1471 );
1472 }
1473
1474 #[test]
1475 fn format_as_rust_code_of_aggregates_does_not_panic() {
1476 let custom = StyleScrollbarColor::Custom(ScrollbarColorCustom {
1477 thumb: ColorU::TRANSPARENT,
1478 track: ColorU::WHITE,
1479 })
1480 .format_as_rust_code(0);
1481 assert!(
1482 custom.starts_with("StyleScrollbarColor::Custom(ScrollbarColorCustom {"),
1483 "{custom}"
1484 );
1485 assert!(custom.contains("thumb:") && custom.contains("track:"), "{custom}");
1486
1487 for tabs in [0usize, 1, 4] {
1488 let code = ScrollbarStyle::default().format_as_rust_code(tabs);
1489 assert!(code.starts_with("ScrollbarStyle {"), "{code}");
1490 assert!(code.contains("horizontal:"), "{code}");
1491 assert!(code.contains("vertical:"), "{code}");
1492 }
1493 }
1494
1495 #[test]
1500 fn scrollbar_info_default_is_the_classic_light_constant() {
1501 assert_eq!(ScrollbarInfo::default(), SCROLLBAR_CLASSIC_LIGHT);
1502
1503 let style = ScrollbarStyle::default();
1504 assert_eq!(style.horizontal, SCROLLBAR_CLASSIC_LIGHT);
1505 assert_eq!(style.vertical, SCROLLBAR_CLASSIC_LIGHT);
1506 }
1507
1508 #[test]
1512 fn computed_default_mirrors_the_default_scrollbar_info() {
1513 let computed = ComputedScrollbarStyle::default();
1514 let info = ScrollbarInfo::default();
1515
1516 assert_eq!(computed.width, Some(info.width));
1517 assert_eq!(
1518 computed.thumb_color,
1519 Some(ColorU { r: 193, g: 193, b: 193, a: 255 })
1520 );
1521 assert_eq!(
1522 computed.track_color,
1523 Some(ColorU { r: 241, g: 241, b: 241, a: 255 })
1524 );
1525 assert!(
1526 computed.thumb_color.is_some() && computed.track_color.is_some(),
1527 "the classic-light default must resolve to solid colors, not None"
1528 );
1529 }
1530
1531 #[test]
1535 fn preset_constants_agree_on_the_overlay_clipping_flag() {
1536 for info in [SCROLLBAR_CLASSIC_LIGHT, SCROLLBAR_CLASSIC_DARK] {
1537 assert!(!info.clip_to_container_border);
1538 assert_eq!(info.scroll_behavior, ScrollBehavior::Auto);
1539 }
1540 for info in [
1541 SCROLLBAR_MACOS_LIGHT,
1542 SCROLLBAR_MACOS_DARK,
1543 SCROLLBAR_IOS_LIGHT,
1544 SCROLLBAR_IOS_DARK,
1545 SCROLLBAR_ANDROID_LIGHT,
1546 SCROLLBAR_ANDROID_DARK,
1547 ] {
1548 assert!(info.clip_to_container_border);
1549 assert_eq!(info.scroll_behavior, ScrollBehavior::Smooth);
1550 }
1551 for info in [SCROLLBAR_WINDOWS_LIGHT, SCROLLBAR_WINDOWS_DARK] {
1552 assert!(!info.clip_to_container_border);
1553 assert_eq!(info.overscroll_behavior_x, OverscrollBehavior::None);
1554 assert_eq!(info.overscroll_behavior_y, OverscrollBehavior::None);
1555 }
1556 }
1557
1558 #[test]
1561 fn light_and_dark_presets_share_their_geometry() {
1562 for (light, dark) in [
1563 (SCROLLBAR_CLASSIC_LIGHT, SCROLLBAR_CLASSIC_DARK),
1564 (SCROLLBAR_MACOS_LIGHT, SCROLLBAR_MACOS_DARK),
1565 (SCROLLBAR_WINDOWS_LIGHT, SCROLLBAR_WINDOWS_DARK),
1566 (SCROLLBAR_IOS_LIGHT, SCROLLBAR_IOS_DARK),
1567 (SCROLLBAR_ANDROID_LIGHT, SCROLLBAR_ANDROID_DARK),
1568 ] {
1569 assert_eq!(light.width, dark.width);
1570 assert_eq!(light.padding_left, dark.padding_left);
1571 assert_eq!(light.padding_right, dark.padding_right);
1572 assert_eq!(light.clip_to_container_border, dark.clip_to_container_border);
1573 assert_ne!(light.thumb, dark.thumb, "light/dark thumbs must differ");
1574 }
1575 }
1576
1577 #[cfg(feature = "parser")]
1582 #[test]
1583 fn scrollbar_width_parses_the_three_legal_keywords() {
1584 assert_eq!(
1585 parse_layout_scrollbar_width("auto"),
1586 Ok(LayoutScrollbarWidth::Auto)
1587 );
1588 assert_eq!(
1589 parse_layout_scrollbar_width("thin"),
1590 Ok(LayoutScrollbarWidth::Thin)
1591 );
1592 assert_eq!(
1593 parse_layout_scrollbar_width("none"),
1594 Ok(LayoutScrollbarWidth::None)
1595 );
1596 }
1597
1598 #[cfg(feature = "parser")]
1599 #[test]
1600 fn scrollbar_width_trims_surrounding_whitespace_but_rejects_inner_junk() {
1601 assert_eq!(
1602 parse_layout_scrollbar_width(" \t thin \n "),
1603 Ok(LayoutScrollbarWidth::Thin)
1604 );
1605 assert!(parse_layout_scrollbar_width("thin;").is_err());
1606 assert!(parse_layout_scrollbar_width("thin thin").is_err());
1607 assert!(parse_layout_scrollbar_width("th in").is_err());
1608 }
1609
1610 #[cfg(feature = "parser")]
1614 #[test]
1615 fn scrollbar_width_keyword_matching_is_case_sensitive() {
1616 assert!(parse_layout_scrollbar_width("AUTO").is_err());
1617 assert!(parse_layout_scrollbar_width("Thin").is_err());
1618 assert!(parse_layout_scrollbar_width("NONE").is_err());
1619 }
1620
1621 #[cfg(feature = "parser")]
1622 #[test]
1623 fn scrollbar_width_rejects_every_garbage_input_without_panicking() {
1624 for input in GARBAGE {
1625 assert!(
1626 parse_layout_scrollbar_width(input).is_err(),
1627 "expected {input:?} to be rejected"
1628 );
1629 }
1630 }
1631
1632 #[cfg(feature = "parser")]
1635 #[test]
1636 fn scrollbar_width_error_keeps_the_raw_untrimmed_input() {
1637 let raw = " thick ";
1638 assert_eq!(
1639 parse_layout_scrollbar_width(raw),
1640 Err(LayoutScrollbarWidthParseError::InvalidValue(raw))
1641 );
1642 let msg = format!("{}", parse_layout_scrollbar_width(raw).unwrap_err());
1643 assert!(msg.contains(raw), "{msg}");
1644 }
1645
1646 #[cfg(feature = "parser")]
1647 #[test]
1648 fn scrollbar_width_survives_a_megabyte_of_input_and_deep_nesting() {
1649 let huge = "a".repeat(1_000_000);
1650 assert!(parse_layout_scrollbar_width(&huge).is_err());
1651
1652 let repeated_token = "auto".repeat(250_000);
1653 assert!(parse_layout_scrollbar_width(&repeated_token).is_err());
1654
1655 let nested = "(".repeat(10_000);
1656 assert!(parse_layout_scrollbar_width(&nested).is_err());
1657 }
1658
1659 #[cfg(feature = "parser")]
1660 #[test]
1661 fn scrollbar_width_round_trips_through_its_printer() {
1662 for value in [
1663 LayoutScrollbarWidth::Auto,
1664 LayoutScrollbarWidth::Thin,
1665 LayoutScrollbarWidth::None,
1666 ] {
1667 let encoded = value.print_as_css_value();
1668 assert_eq!(
1669 parse_layout_scrollbar_width(&encoded),
1670 Ok(value),
1671 "{encoded} did not decode back to {value:?}"
1672 );
1673 }
1674 }
1675
1676 #[cfg(feature = "parser")]
1681 #[test]
1682 fn scrollbar_color_needs_exactly_two_colors_or_the_auto_keyword() {
1683 assert_eq!(parse_style_scrollbar_color("auto"), Ok(StyleScrollbarColor::Auto));
1684 assert_eq!(
1685 parse_style_scrollbar_color(" auto "),
1686 Ok(StyleScrollbarColor::Auto)
1687 );
1688 assert_eq!(
1689 parse_style_scrollbar_color("red blue"),
1690 Ok(StyleScrollbarColor::Custom(ScrollbarColorCustom {
1691 thumb: ColorU::RED,
1692 track: ColorU::BLUE,
1693 }))
1694 );
1695
1696 for input in ["red", "#fff", "red blue green", "a b c d"] {
1698 assert!(
1699 matches!(
1700 parse_style_scrollbar_color(input),
1701 Err(StyleScrollbarColorParseError::InvalidValue(_))
1702 ),
1703 "expected {input:?} to be an InvalidValue error"
1704 );
1705 }
1706 }
1707
1708 #[cfg(feature = "parser")]
1711 #[test]
1712 fn scrollbar_color_accepts_any_whitespace_run_as_the_separator() {
1713 let expected = StyleScrollbarColor::Custom(ScrollbarColorCustom {
1714 thumb: ColorU::RED,
1715 track: ColorU::BLUE,
1716 });
1717 assert_eq!(parse_style_scrollbar_color("red\tblue"), Ok(expected));
1718 assert_eq!(parse_style_scrollbar_color("red\n blue"), Ok(expected));
1719 assert_eq!(parse_style_scrollbar_color(" red blue "), Ok(expected));
1720 }
1721
1722 #[cfg(feature = "parser")]
1725 #[test]
1726 fn scrollbar_color_names_are_case_insensitive_but_the_auto_keyword_is_not() {
1727 assert_eq!(
1728 parse_style_scrollbar_color("RED BLUE"),
1729 Ok(StyleScrollbarColor::Custom(ScrollbarColorCustom {
1730 thumb: ColorU::RED,
1731 track: ColorU::BLUE,
1732 }))
1733 );
1734 assert!(matches!(
1736 parse_style_scrollbar_color("AUTO"),
1737 Err(StyleScrollbarColorParseError::InvalidValue(_))
1738 ));
1739 assert!(matches!(
1741 parse_style_scrollbar_color("auto auto"),
1742 Err(StyleScrollbarColorParseError::Color(_))
1743 ));
1744 }
1745
1746 #[cfg(feature = "parser")]
1751 #[test]
1752 fn scrollbar_color_rejects_functional_colors_containing_spaces() {
1753 assert_eq!(
1754 parse_style_scrollbar_color("rgb(255,0,0) blue"),
1755 Ok(StyleScrollbarColor::Custom(ScrollbarColorCustom {
1756 thumb: ColorU::RED,
1757 track: ColorU::BLUE,
1758 }))
1759 );
1760 assert!(matches!(
1761 parse_style_scrollbar_color("rgb(255, 0, 0) blue"),
1762 Err(StyleScrollbarColorParseError::InvalidValue(_))
1763 ));
1764 }
1765
1766 #[cfg(feature = "parser")]
1767 #[test]
1768 fn scrollbar_color_reports_which_component_failed() {
1769 assert!(matches!(
1771 parse_style_scrollbar_color("notacolor blue"),
1772 Err(StyleScrollbarColorParseError::Color(_))
1773 ));
1774 assert!(matches!(
1775 parse_style_scrollbar_color("red notacolor"),
1776 Err(StyleScrollbarColorParseError::Color(_))
1777 ));
1778 assert!(matches!(
1779 parse_style_scrollbar_color("#gggggg #000000"),
1780 Err(StyleScrollbarColorParseError::Color(_))
1781 ));
1782 }
1783
1784 #[cfg(feature = "parser")]
1785 #[test]
1786 fn scrollbar_color_rejects_garbage_without_panicking() {
1787 for input in GARBAGE {
1788 assert!(
1789 parse_style_scrollbar_color(input).is_err(),
1790 "expected {input:?} to be rejected"
1791 );
1792 }
1793 for input in [
1795 "0 0",
1796 "-0 -0",
1797 "NaN NaN",
1798 "inf inf",
1799 "9223372036854775807 1",
1800 "1e400 1e400",
1801 "-1 -1",
1802 ] {
1803 assert!(
1804 parse_style_scrollbar_color(input).is_err(),
1805 "expected {input:?} to be rejected"
1806 );
1807 }
1808 }
1809
1810 #[cfg(feature = "parser")]
1811 #[test]
1812 fn scrollbar_color_survives_huge_and_deeply_nested_input() {
1813 let huge = "z".repeat(500_000);
1814 let two_huge = format!("{huge} {huge}");
1815 assert!(parse_style_scrollbar_color(&two_huge).is_err());
1816
1817 let nested = "(".repeat(10_000);
1818 assert!(parse_style_scrollbar_color(&format!("{nested} {nested}")).is_err());
1819
1820 let many = "red ".repeat(100_000);
1822 assert!(matches!(
1823 parse_style_scrollbar_color(&many),
1824 Err(StyleScrollbarColorParseError::InvalidValue(_))
1825 ));
1826 }
1827
1828 #[cfg(feature = "parser")]
1832 #[test]
1833 fn scrollbar_color_error_carries_the_trimmed_input() {
1834 assert_eq!(
1835 parse_style_scrollbar_color(" red "),
1836 Err(StyleScrollbarColorParseError::InvalidValue("red"))
1837 );
1838 }
1839
1840 #[cfg(feature = "parser")]
1841 #[test]
1842 fn scrollbar_color_round_trips_through_its_printer() {
1843 let samples = [
1844 StyleScrollbarColor::Auto,
1845 StyleScrollbarColor::Custom(ScrollbarColorCustom {
1846 thumb: ColorU::RED,
1847 track: ColorU::BLUE,
1848 }),
1849 StyleScrollbarColor::Custom(ScrollbarColorCustom {
1850 thumb: ColorU::TRANSPARENT,
1851 track: ColorU::TRANSPARENT,
1852 }),
1853 StyleScrollbarColor::Custom(ScrollbarColorCustom {
1854 thumb: ColorU { r: 0, g: 0, b: 0, a: 100 },
1855 track: ColorU { r: 1, g: 2, b: 3, a: 4 },
1856 }),
1857 StyleScrollbarColor::Custom(ScrollbarColorCustom {
1858 thumb: ColorU::WHITE,
1859 track: ColorU::BLACK,
1860 }),
1861 ];
1862 for value in samples {
1863 let encoded = value.print_as_css_value();
1864 assert_eq!(
1865 parse_style_scrollbar_color(&encoded),
1866 Ok(value),
1867 "{encoded} did not decode back to {value:?}"
1868 );
1869 }
1870 }
1871
1872 #[cfg(feature = "parser")]
1877 #[test]
1878 fn visibility_mode_parses_its_three_keywords_and_trims() {
1879 assert_eq!(
1880 parse_scrollbar_visibility_mode("always"),
1881 Ok(ScrollbarVisibilityMode::Always)
1882 );
1883 assert_eq!(
1884 parse_scrollbar_visibility_mode(" when-scrolling\t"),
1885 Ok(ScrollbarVisibilityMode::WhenScrolling)
1886 );
1887 assert_eq!(
1888 parse_scrollbar_visibility_mode("auto"),
1889 Ok(ScrollbarVisibilityMode::Auto)
1890 );
1891 }
1892
1893 #[cfg(feature = "parser")]
1894 #[test]
1895 fn visibility_mode_rejects_near_misses_and_garbage() {
1896 for input in [
1897 "when scrolling", "whenscrolling",
1899 "when-scrolling-",
1900 "-when-scrolling",
1901 "ALWAYS",
1902 "always;",
1903 "always auto",
1904 ] {
1905 assert!(
1906 parse_scrollbar_visibility_mode(input).is_err(),
1907 "expected {input:?} to be rejected"
1908 );
1909 }
1910 for input in GARBAGE {
1911 assert!(
1912 parse_scrollbar_visibility_mode(input).is_err(),
1913 "expected {input:?} to be rejected"
1914 );
1915 }
1916 }
1917
1918 #[cfg(feature = "parser")]
1919 #[test]
1920 fn visibility_mode_survives_huge_and_nested_input() {
1921 assert!(parse_scrollbar_visibility_mode(&"a".repeat(1_000_000)).is_err());
1922 assert!(parse_scrollbar_visibility_mode(&"always".repeat(200_000)).is_err());
1923 assert!(parse_scrollbar_visibility_mode(&"[".repeat(10_000)).is_err());
1924 }
1925
1926 #[cfg(feature = "parser")]
1927 #[test]
1928 fn visibility_mode_round_trips_through_its_printer() {
1929 for value in [
1930 ScrollbarVisibilityMode::Always,
1931 ScrollbarVisibilityMode::WhenScrolling,
1932 ScrollbarVisibilityMode::Auto,
1933 ] {
1934 let encoded = value.print_as_css_value();
1935 assert_eq!(
1936 parse_scrollbar_visibility_mode(&encoded),
1937 Ok(value),
1938 "{encoded} did not decode back to {value:?}"
1939 );
1940 }
1941 }
1942
1943 #[cfg(feature = "parser")]
1948 #[test]
1949 fn parse_time_ms_accepts_bare_zero_and_both_units() {
1950 assert_eq!(parse_time_ms("0"), Some(0));
1951 assert_eq!(parse_time_ms("0ms"), Some(0));
1952 assert_eq!(parse_time_ms("0s"), Some(0));
1953 assert_eq!(parse_time_ms("500ms"), Some(500));
1954 assert_eq!(parse_time_ms("1s"), Some(1000));
1955 assert_eq!(parse_time_ms("1.5s"), Some(1500));
1956 assert_eq!(parse_time_ms(" 200ms "), Some(200));
1957 assert_eq!(parse_time_ms("200MS"), Some(200), "units are case-insensitive");
1958 }
1959
1960 #[cfg(feature = "parser")]
1965 #[test]
1966 fn parse_time_ms_converts_the_tick_unit_to_milliseconds() {
1967 assert_eq!(parse_time_ms("60t"), Some(1000));
1968 assert_eq!(parse_time_ms("30t"), Some(500));
1969 assert_eq!(parse_time_ms("1t"), Some(16));
1970 assert_eq!(parse_time_ms("0t"), Some(0));
1971 assert_ne!(parse_time_ms("60t"), Some(60), "ticks passed through as ms");
1972 }
1973
1974 #[cfg(feature = "parser")]
1977 #[test]
1978 fn parse_time_ms_requires_an_attached_unit() {
1979 assert_eq!(parse_time_ms("500"), None);
1980 assert_eq!(parse_time_ms("1 s"), None);
1981 assert_eq!(parse_time_ms("500 ms"), None);
1982 assert_eq!(parse_time_ms("ms"), None);
1983 assert_eq!(parse_time_ms("s"), None);
1984 assert_eq!(parse_time_ms("500px"), None);
1985 assert_eq!(parse_time_ms("500msms"), None);
1986 }
1987
1988 #[cfg(feature = "parser")]
1989 #[test]
1990 fn parse_time_ms_rejects_empty_blank_unicode_and_garbage() {
1991 for input in ["", " ", " ", "\t\n", "\u{1F600}", "e\u{0301}", "٥ms", "500ms"] {
1992 assert_eq!(parse_time_ms(input), None, "expected {input:?} to be rejected");
1993 }
1994 }
1995
1996 #[cfg(feature = "parser")]
1997 #[test]
1998 fn parse_time_ms_rejects_negative_durations() {
1999 assert_eq!(parse_time_ms("-1ms"), None);
2000 assert_eq!(parse_time_ms("-0.5s"), None);
2001 assert_eq!(parse_time_ms("-inf ms"), None);
2002 }
2003
2004 #[cfg(feature = "parser")]
2007 #[test]
2008 fn parse_time_ms_accepts_negative_zero_as_zero() {
2009 assert_eq!(parse_time_ms("-0ms"), Some(0));
2010 assert_eq!(parse_time_ms("-0.0s"), Some(0));
2011 }
2012
2013 #[cfg(feature = "parser")]
2018 #[test]
2019 fn parse_time_ms_saturates_on_non_finite_and_huge_values() {
2020 assert_eq!(parse_time_ms("infms"), Some(u32::MAX));
2021 assert_eq!(parse_time_ms("infinityms"), Some(u32::MAX));
2022 assert_eq!(parse_time_ms("infs"), Some(u32::MAX));
2023 assert_eq!(parse_time_ms("nanms"), Some(0));
2024 assert_eq!(parse_time_ms("NaNms"), Some(0));
2025
2026 assert_eq!(parse_time_ms("1e30ms"), Some(u32::MAX));
2027 assert_eq!(parse_time_ms("1e400ms"), Some(u32::MAX), "overflows f32 to inf");
2028 assert_eq!(parse_time_ms("4294967296ms"), Some(u32::MAX), "2^32 clamps");
2029 assert_eq!(parse_time_ms("1e-30ms"), Some(0), "underflows to zero");
2030
2031 let long_number = format!("{}ms", "9".repeat(100_000));
2033 assert_eq!(parse_time_ms(&long_number), Some(u32::MAX));
2034 }
2035
2036 #[cfg(feature = "parser")]
2039 #[test]
2040 fn parse_time_ms_saturates_when_seconds_overflow_milliseconds() {
2041 assert_eq!(parse_time_ms("1000s"), Some(1_000_000));
2043 assert_eq!(parse_time_ms("16777s"), Some(16_777_000));
2044
2045 assert_eq!(parse_time_ms("4294968s"), Some(u32::MAX));
2047 assert_eq!(parse_time_ms("5000000s"), Some(u32::MAX));
2048
2049 let ms = parse_time_ms("4294967s").expect("4294967s must parse");
2052 assert!(
2053 ms.abs_diff(4_294_967_000) <= 512,
2054 "4294967s decoded to {ms}, which is nowhere near 4294967000ms"
2055 );
2056
2057 assert_eq!(parse_time_ms("0.0005s"), Some(0), "sub-ms truncates toward zero");
2058 }
2059
2060 #[cfg(feature = "parser")]
2065 #[test]
2066 fn fade_parsers_accept_the_documented_syntax() {
2067 assert_eq!(
2068 parse_scrollbar_fade_delay("500ms"),
2069 Ok(ScrollbarFadeDelay::new(500))
2070 );
2071 assert_eq!(parse_scrollbar_fade_delay("0"), Ok(ScrollbarFadeDelay::ZERO));
2072 assert_eq!(
2073 parse_scrollbar_fade_delay(" 1s "),
2074 Ok(ScrollbarFadeDelay::new(1000))
2075 );
2076 assert_eq!(
2077 parse_scrollbar_fade_duration("200ms"),
2078 Ok(ScrollbarFadeDuration::new(200))
2079 );
2080 assert_eq!(
2081 parse_scrollbar_fade_duration("0"),
2082 Ok(ScrollbarFadeDuration::ZERO)
2083 );
2084 }
2085
2086 #[cfg(feature = "parser")]
2087 #[test]
2088 fn fade_parsers_reject_garbage_and_keep_the_raw_input_in_the_error() {
2089 for input in GARBAGE {
2090 assert!(
2091 parse_scrollbar_fade_delay(input).is_err(),
2092 "delay: expected {input:?} to be rejected"
2093 );
2094 assert!(
2095 parse_scrollbar_fade_duration(input).is_err(),
2096 "duration: expected {input:?} to be rejected"
2097 );
2098 }
2099
2100 let raw = " bogus ";
2101 assert_eq!(
2102 parse_scrollbar_fade_delay(raw),
2103 Err(ScrollbarFadeDelayParseError::InvalidValue(raw))
2104 );
2105 assert_eq!(
2106 parse_scrollbar_fade_duration(raw),
2107 Err(ScrollbarFadeDurationParseError::InvalidValue(raw))
2108 );
2109 }
2110
2111 #[cfg(feature = "parser")]
2112 #[test]
2113 fn fade_parsers_reject_negative_delays() {
2114 assert!(parse_scrollbar_fade_delay("-1ms").is_err());
2115 assert!(parse_scrollbar_fade_delay("-500ms").is_err());
2116 assert!(parse_scrollbar_fade_duration("-0.5s").is_err());
2117 }
2118
2119 #[cfg(feature = "parser")]
2120 #[test]
2121 fn fade_parsers_saturate_instead_of_overflowing() {
2122 assert_eq!(
2123 parse_scrollbar_fade_delay("1e30ms"),
2124 Ok(ScrollbarFadeDelay::new(u32::MAX))
2125 );
2126 assert_eq!(
2127 parse_scrollbar_fade_duration("99999999999999s"),
2128 Ok(ScrollbarFadeDuration::new(u32::MAX))
2129 );
2130 }
2131
2132 #[cfg(feature = "parser")]
2133 #[test]
2134 fn fade_parsers_survive_huge_and_nested_input() {
2135 assert!(parse_scrollbar_fade_delay(&"a".repeat(1_000_000)).is_err());
2136 assert!(parse_scrollbar_fade_duration(&"0ms".repeat(300_000)).is_err());
2137 assert!(parse_scrollbar_fade_delay(&"(".repeat(10_000)).is_err());
2138 assert!(parse_scrollbar_fade_duration(&"[".repeat(10_000)).is_err());
2139 }
2140
2141 #[cfg(feature = "parser")]
2146 #[test]
2147 fn fade_delay_round_trips_exactly_up_to_two_pow_24() {
2148 for ms in [
2149 0u32,
2150 1,
2151 8,
2152 16,
2153 200,
2154 500,
2155 65_535,
2156 1_000_000,
2157 TWO_POW_24 - 1,
2158 TWO_POW_24,
2159 u32::MAX,
2160 ] {
2161 let value = ScrollbarFadeDelay::new(ms);
2162 let encoded = value.print_as_css_value();
2163 assert_eq!(
2164 parse_scrollbar_fade_delay(&encoded),
2165 Ok(value),
2166 "{ms}ms encoded as {encoded:?} did not decode back"
2167 );
2168
2169 let value = ScrollbarFadeDuration::new(ms);
2170 let encoded = value.print_as_css_value();
2171 assert_eq!(
2172 parse_scrollbar_fade_duration(&encoded),
2173 Ok(value),
2174 "{ms}ms encoded as {encoded:?} did not decode back"
2175 );
2176 }
2177 }
2178
2179 #[cfg(feature = "parser")]
2183 #[test]
2184 fn fade_delay_round_trip_is_lossy_above_two_pow_24() {
2185 let value = ScrollbarFadeDelay::new(TWO_POW_24 + 1);
2186 let decoded = parse_scrollbar_fade_delay(&value.print_as_css_value()).unwrap();
2187 assert_ne!(decoded, value, "expected precision loss above 2^24");
2188 assert_eq!(decoded.ms, TWO_POW_24, "must snap down to the nearest f32");
2189 }
2190
2191 fn error_payloads() -> [String; 6] {
2198 [
2199 String::new(),
2200 String::from(" "),
2201 String::from("thick"),
2202 String::from("\u{1F600}\u{0301}"),
2203 String::from("nul\0inside"),
2204 "x".repeat(100_000),
2205 ]
2206 }
2207
2208 #[test]
2209 fn layout_scrollbar_width_error_round_trips_through_owned_and_back() {
2210 for payload in error_payloads() {
2211 let shared = LayoutScrollbarWidthParseError::InvalidValue(&payload);
2212 let owned = shared.to_contained();
2213 assert_eq!(
2214 owned,
2215 LayoutScrollbarWidthParseErrorOwned::InvalidValue(payload.clone().into())
2216 );
2217 assert_eq!(owned.to_shared(), shared, "owned -> shared lost information");
2218 assert_eq!(
2219 owned.to_shared().to_contained(),
2220 owned,
2221 "conversion is not idempotent"
2222 );
2223 }
2224 }
2225
2226 #[test]
2227 fn visibility_mode_error_round_trips_through_owned_and_back() {
2228 for payload in error_payloads() {
2229 let shared = ScrollbarVisibilityModeParseError::InvalidValue(&payload);
2230 let owned = shared.to_contained();
2231 assert_eq!(owned.to_shared(), shared);
2232 assert_eq!(owned.to_shared().to_contained(), owned);
2233 }
2234 }
2235
2236 #[test]
2237 fn fade_delay_and_duration_errors_round_trip_through_owned_and_back() {
2238 for payload in error_payloads() {
2239 let delay = ScrollbarFadeDelayParseError::InvalidValue(&payload);
2240 let owned_delay = delay.to_contained();
2241 assert_eq!(owned_delay.to_shared(), delay);
2242 assert_eq!(owned_delay.to_shared().to_contained(), owned_delay);
2243
2244 let duration = ScrollbarFadeDurationParseError::InvalidValue(&payload);
2245 let owned_duration = duration.to_contained();
2246 assert_eq!(owned_duration.to_shared(), duration);
2247 assert_eq!(owned_duration.to_shared().to_contained(), owned_duration);
2248 }
2249 }
2250
2251 #[test]
2252 fn scrollbar_color_invalid_value_error_round_trips_through_owned_and_back() {
2253 for payload in error_payloads() {
2254 let shared = StyleScrollbarColorParseError::InvalidValue(&payload);
2255 let owned = shared.to_contained();
2256 assert_eq!(
2257 owned,
2258 StyleScrollbarColorParseErrorOwned::InvalidValue(payload.clone().into())
2259 );
2260 assert_eq!(owned.to_shared(), shared);
2261 assert_eq!(owned.to_shared().to_contained(), owned);
2262 }
2263 }
2264
2265 #[cfg(feature = "parser")]
2268 #[test]
2269 fn scrollbar_color_nested_color_error_round_trips_through_owned_and_back() {
2270 let shared = parse_style_scrollbar_color("notacolor blue").unwrap_err();
2271 assert!(matches!(shared, StyleScrollbarColorParseError::Color(_)));
2272
2273 let owned = shared.to_contained();
2274 assert!(matches!(owned, StyleScrollbarColorParseErrorOwned::Color(_)));
2275 assert_eq!(owned.to_shared(), shared, "nested color error lost information");
2276 assert_eq!(owned.to_shared().to_contained(), owned);
2277 }
2278
2279 #[cfg(feature = "parser")]
2282 #[test]
2283 fn error_display_mentions_the_offending_input() {
2284 let width = parse_layout_scrollbar_width("thick").unwrap_err();
2285 assert!(format!("{width}").contains("thick"), "{width}");
2286 assert!(format!("{width:?}").contains("thick"), "{width:?}");
2287
2288 let color = parse_style_scrollbar_color("red").unwrap_err();
2289 assert!(format!("{color}").contains("red"), "{color}");
2290
2291 let vis = parse_scrollbar_visibility_mode("sometimes").unwrap_err();
2292 assert!(format!("{vis}").contains("sometimes"), "{vis}");
2293
2294 let delay = parse_scrollbar_fade_delay("soon").unwrap_err();
2295 assert!(format!("{delay}").contains("soon"), "{delay}");
2296
2297 let duration = parse_scrollbar_fade_duration("briefly").unwrap_err();
2298 assert!(format!("{duration}").contains("briefly"), "{duration}");
2299 }
2300
2301 #[test]
2304 fn error_display_does_not_panic_on_exotic_payloads() {
2305 for payload in error_payloads() {
2306 let err = LayoutScrollbarWidthParseError::InvalidValue(&payload);
2307 assert!(!format!("{err}").is_empty());
2308 let owned = err.to_contained();
2309 assert!(!format!("{}", owned.to_shared()).is_empty());
2310 }
2311 }
2312}