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")]
1073fn parse_time_ms(input: &str) -> Option<u32> {
1074 crate::props::basic::time::parse_duration(input).ok().map(|d| d.inner)
1075}
1076
1077#[cfg(feature = "parser")]
1078pub fn parse_scrollbar_fade_delay(
1082 input: &str,
1083) -> Result<ScrollbarFadeDelay, ScrollbarFadeDelayParseError<'_>> {
1084 parse_time_ms(input)
1085 .map(ScrollbarFadeDelay::new)
1086 .ok_or(ScrollbarFadeDelayParseError::InvalidValue(input))
1087}
1088
1089#[derive(Clone, PartialEq, Eq)]
1092pub enum ScrollbarFadeDurationParseError<'a> {
1093 InvalidValue(&'a str),
1094}
1095impl_debug_as_display!(ScrollbarFadeDurationParseError<'a>);
1096impl_display! { ScrollbarFadeDurationParseError<'a>, {
1097 InvalidValue(v) => format!("Invalid scrollbar-fade-duration value: \"{}\"", v),
1098}}
1099
1100#[derive(Debug, Clone, PartialEq, Eq)]
1101#[repr(C, u8)]
1102pub enum ScrollbarFadeDurationParseErrorOwned {
1103 InvalidValue(AzString),
1104}
1105impl ScrollbarFadeDurationParseError<'_> {
1106 #[must_use] pub fn to_contained(&self) -> ScrollbarFadeDurationParseErrorOwned {
1107 match self {
1108 Self::InvalidValue(s) => ScrollbarFadeDurationParseErrorOwned::InvalidValue((*s).to_string().into()),
1109 }
1110 }
1111}
1112impl ScrollbarFadeDurationParseErrorOwned {
1113 #[must_use] pub fn to_shared(&self) -> ScrollbarFadeDurationParseError<'_> {
1114 match self {
1115 Self::InvalidValue(s) => ScrollbarFadeDurationParseError::InvalidValue(s.as_str()),
1116 }
1117 }
1118}
1119
1120#[cfg(feature = "parser")]
1121pub fn parse_scrollbar_fade_duration(
1125 input: &str,
1126) -> Result<ScrollbarFadeDuration, ScrollbarFadeDurationParseError<'_>> {
1127 parse_time_ms(input)
1128 .map(ScrollbarFadeDuration::new)
1129 .ok_or(ScrollbarFadeDurationParseError::InvalidValue(input))
1130}
1131
1132#[cfg(all(test, feature = "parser"))]
1133mod tests {
1134 use super::*;
1135 use crate::props::basic::color::ColorU;
1136
1137 #[test]
1138 fn test_parse_scrollbar_width() {
1139 assert_eq!(
1140 parse_layout_scrollbar_width("auto").unwrap(),
1141 LayoutScrollbarWidth::Auto
1142 );
1143 assert_eq!(
1144 parse_layout_scrollbar_width("thin").unwrap(),
1145 LayoutScrollbarWidth::Thin
1146 );
1147 assert_eq!(
1148 parse_layout_scrollbar_width("none").unwrap(),
1149 LayoutScrollbarWidth::None
1150 );
1151 assert!(parse_layout_scrollbar_width("thick").is_err());
1152 }
1153
1154 #[test]
1155 fn test_parse_scrollbar_color() {
1156 assert_eq!(
1157 parse_style_scrollbar_color("auto").unwrap(),
1158 StyleScrollbarColor::Auto
1159 );
1160
1161 let custom = parse_style_scrollbar_color("red blue").unwrap();
1162 assert_eq!(
1163 custom,
1164 StyleScrollbarColor::Custom(ScrollbarColorCustom {
1165 thumb: ColorU::RED,
1166 track: ColorU::BLUE
1167 })
1168 );
1169
1170 let custom_hex = parse_style_scrollbar_color("#ff0000 #0000ff").unwrap();
1171 assert_eq!(
1172 custom_hex,
1173 StyleScrollbarColor::Custom(ScrollbarColorCustom {
1174 thumb: ColorU::RED,
1175 track: ColorU::BLUE
1176 })
1177 );
1178
1179 assert!(parse_style_scrollbar_color("red").is_err());
1180 assert!(parse_style_scrollbar_color("red blue green").is_err());
1181 }
1182}
1183
1184#[cfg(test)]
1185#[allow(clippy::unreadable_literal, clippy::float_cmp)]
1186mod autotest_generated {
1187 use super::*;
1188 use crate::codegen::format::FormatAsRustCode;
1189
1190 #[cfg(feature = "parser")]
1194 const TWO_POW_24: u32 = 16_777_216;
1195
1196 #[cfg(feature = "parser")]
1198 const GARBAGE: &[&str] = &[
1199 "",
1200 " ",
1201 " ",
1202 "\t\n",
1203 "\u{a0}", "\0",
1205 "\u{1F600}", "e\u{0301}", "\u{202e}auto", "аuto", "AUTO",
1210 "auto;",
1211 "auto garbage",
1212 "-1",
1213 "NaN",
1214 "inf",
1215 "0x10",
1216 "9223372036854775807", "1e400",
1218 "{[(<",
1219 ];
1220
1221 fn assert_physics_invariants(p: ScrollPhysics, name: &str) {
1230 for (field, v) in [
1231 ("deceleration_rate", p.deceleration_rate),
1232 ("min_velocity_threshold", p.min_velocity_threshold),
1233 ("max_velocity", p.max_velocity),
1234 ("wheel_multiplier", p.wheel_multiplier),
1235 ("overscroll_elasticity", p.overscroll_elasticity),
1236 ("max_overscroll_distance", p.max_overscroll_distance),
1237 ] {
1238 assert!(v.is_finite(), "{name}.{field} is not finite: {v}");
1239 assert!(!v.is_nan(), "{name}.{field} is NaN");
1240 assert!(v >= 0.0, "{name}.{field} is negative: {v}");
1241 }
1242
1243 assert!(
1244 p.deceleration_rate > 0.0 && p.deceleration_rate < 1.0,
1245 "{name}.deceleration_rate must stay inside (0.0, 1.0) or momentum never stops: {}",
1246 p.deceleration_rate
1247 );
1248 assert!(
1249 (0.0..=1.0).contains(&p.overscroll_elasticity),
1250 "{name}.overscroll_elasticity out of [0.0, 1.0]: {}",
1251 p.overscroll_elasticity
1252 );
1253 assert!(
1254 p.max_velocity > p.min_velocity_threshold,
1255 "{name}: max_velocity ({}) must exceed min_velocity_threshold ({})",
1256 p.max_velocity,
1257 p.min_velocity_threshold
1258 );
1259 assert!(
1260 p.wheel_multiplier > 0.0,
1261 "{name}.wheel_multiplier must be > 0 or the wheel does nothing"
1262 );
1263 assert!(
1264 p.timer_interval_ms > 0,
1265 "{name}.timer_interval_ms == 0 would spin the physics timer"
1266 );
1267 assert!(
1268 p.smooth_scroll_duration_ms > 0,
1269 "{name}.smooth_scroll_duration_ms == 0 makes `scroll-behavior: smooth` a no-op"
1270 );
1271 }
1272
1273 #[test]
1274 fn scroll_physics_presets_hold_their_invariants() {
1275 assert_physics_invariants(ScrollPhysics::default(), "default");
1276 assert_physics_invariants(ScrollPhysics::ios(), "ios");
1277 assert_physics_invariants(ScrollPhysics::macos(), "macos");
1278 assert_physics_invariants(ScrollPhysics::windows(), "windows");
1279 assert_physics_invariants(ScrollPhysics::android(), "android");
1280 }
1281
1282 #[test]
1283 fn scroll_physics_presets_are_pure_and_distinct() {
1284 assert_eq!(ScrollPhysics::ios(), ScrollPhysics::ios());
1286 assert_eq!(ScrollPhysics::windows(), ScrollPhysics::windows());
1287
1288 assert_ne!(ScrollPhysics::ios(), ScrollPhysics::macos());
1290 assert_ne!(ScrollPhysics::ios(), ScrollPhysics::android());
1291 assert_ne!(ScrollPhysics::macos(), ScrollPhysics::windows());
1292 assert_ne!(ScrollPhysics::android(), ScrollPhysics::windows());
1293 assert_ne!(ScrollPhysics::default(), ScrollPhysics::ios());
1294 }
1295
1296 #[test]
1299 fn scroll_physics_presets_match_their_documented_platform_behavior() {
1300 let win = ScrollPhysics::windows();
1301 assert_eq!(win.overscroll_elasticity, 0.0);
1302 assert_eq!(win.max_overscroll_distance, 0.0);
1303 assert!(!win.invert_direction);
1304
1305 assert!(ScrollPhysics::ios().invert_direction);
1306 assert!(ScrollPhysics::macos().invert_direction);
1307 assert!(!ScrollPhysics::android().invert_direction);
1308
1309 assert!(ScrollPhysics::ios().deceleration_rate > ScrollPhysics::windows().deceleration_rate);
1311
1312 assert_eq!(ScrollPhysics::default().overscroll_elasticity, 0.0);
1314 }
1315
1316 #[test]
1321 fn fade_delay_and_duration_constructors_store_their_argument_verbatim() {
1322 for ms in [0u32, 1, 16, 500, u32::MAX / 2, u32::MAX - 1, u32::MAX] {
1323 assert_eq!(ScrollbarFadeDelay::new(ms).ms, ms);
1324 assert_eq!(ScrollbarFadeDuration::new(ms).ms, ms);
1325 }
1326 }
1327
1328 #[test]
1329 fn fade_zero_constants_agree_with_new_and_default() {
1330 assert_eq!(ScrollbarFadeDelay::ZERO, ScrollbarFadeDelay::new(0));
1331 assert_eq!(ScrollbarFadeDelay::ZERO, ScrollbarFadeDelay::default());
1332 assert_eq!(ScrollbarFadeDuration::ZERO, ScrollbarFadeDuration::new(0));
1333 assert_eq!(ScrollbarFadeDuration::ZERO, ScrollbarFadeDuration::default());
1334 assert_eq!(ScrollbarFadeDelay::ZERO.ms, 0);
1335 assert_eq!(ScrollbarFadeDuration::ZERO.ms, 0);
1336 }
1337
1338 #[test]
1341 fn fade_delay_orders_by_millisecond_count() {
1342 assert!(ScrollbarFadeDelay::new(0) < ScrollbarFadeDelay::new(1));
1343 assert!(ScrollbarFadeDelay::new(499) < ScrollbarFadeDelay::new(500));
1344 assert!(ScrollbarFadeDelay::new(u32::MAX) > ScrollbarFadeDelay::new(u32::MAX - 1));
1345 assert!(ScrollbarFadeDuration::new(0) < ScrollbarFadeDuration::new(u32::MAX));
1346 }
1347
1348 #[test]
1353 fn enum_printers_emit_the_css_keywords() {
1354 assert_eq!(ScrollBehavior::Auto.print_as_css_value(), "auto");
1355 assert_eq!(ScrollBehavior::Smooth.print_as_css_value(), "smooth");
1356 assert_eq!(ScrollBehavior::default(), ScrollBehavior::Auto);
1357
1358 assert_eq!(OverscrollBehavior::Auto.print_as_css_value(), "auto");
1359 assert_eq!(OverscrollBehavior::Contain.print_as_css_value(), "contain");
1360 assert_eq!(OverscrollBehavior::None.print_as_css_value(), "none");
1361 assert_eq!(OverscrollBehavior::default(), OverscrollBehavior::Auto);
1362
1363 assert_eq!(OverflowScrolling::Auto.print_as_css_value(), "auto");
1364 assert_eq!(OverflowScrolling::Touch.print_as_css_value(), "touch");
1365 assert_eq!(OverflowScrolling::default(), OverflowScrolling::Auto);
1366
1367 assert_eq!(LayoutScrollbarWidth::Auto.print_as_css_value(), "auto");
1368 assert_eq!(LayoutScrollbarWidth::Thin.print_as_css_value(), "thin");
1369 assert_eq!(LayoutScrollbarWidth::None.print_as_css_value(), "none");
1370 assert_eq!(LayoutScrollbarWidth::default(), LayoutScrollbarWidth::Auto);
1371
1372 assert_eq!(ScrollbarVisibilityMode::Always.print_as_css_value(), "always");
1373 assert_eq!(
1374 ScrollbarVisibilityMode::WhenScrolling.print_as_css_value(),
1375 "when-scrolling"
1376 );
1377 assert_eq!(ScrollbarVisibilityMode::Auto.print_as_css_value(), "auto");
1378 assert_eq!(
1379 ScrollbarVisibilityMode::default(),
1380 ScrollbarVisibilityMode::Always
1381 );
1382 }
1383
1384 #[test]
1388 fn fade_printers_special_case_zero_and_keep_the_unit_otherwise() {
1389 assert_eq!(ScrollbarFadeDelay::new(0).print_as_css_value(), "0");
1390 assert_eq!(ScrollbarFadeDelay::new(1).print_as_css_value(), "1ms");
1391 assert_eq!(ScrollbarFadeDelay::new(500).print_as_css_value(), "500ms");
1392 assert_eq!(
1393 ScrollbarFadeDelay::new(u32::MAX).print_as_css_value(),
1394 "4294967295ms"
1395 );
1396 assert_eq!(ScrollbarFadeDuration::new(0).print_as_css_value(), "0");
1397 assert_eq!(ScrollbarFadeDuration::new(200).print_as_css_value(), "200ms");
1398 assert_eq!(
1399 ScrollbarFadeDuration::new(u32::MAX).print_as_css_value(),
1400 "4294967295ms"
1401 );
1402 }
1403
1404 #[test]
1405 fn scrollbar_color_printer_emits_two_eight_digit_hashes() {
1406 assert_eq!(StyleScrollbarColor::Auto.print_as_css_value(), "auto");
1407 assert_eq!(StyleScrollbarColor::default(), StyleScrollbarColor::Auto);
1408
1409 let custom = StyleScrollbarColor::Custom(ScrollbarColorCustom {
1410 thumb: ColorU::RED,
1411 track: ColorU::TRANSPARENT,
1412 });
1413 assert_eq!(custom.print_as_css_value(), "#ff0000ff #00000000");
1414 }
1415
1416 #[test]
1419 fn aggregate_printers_do_not_panic_and_mention_both_axes() {
1420 let printed = ScrollbarStyle::default().print_as_css_value();
1421 assert!(printed.contains("horz("), "{printed}");
1422 assert!(printed.contains("vert("), "{printed}");
1423
1424 let info = ScrollbarInfo::default().print_as_css_value();
1425 assert!(info.contains("width:"), "{info}");
1426 assert!(info.contains("thumb:"), "{info}");
1427 assert!(info.contains("resizer:"), "{info}");
1428 }
1429
1430 #[test]
1435 fn format_as_rust_code_emits_constructible_expressions() {
1436 assert_eq!(
1437 LayoutScrollbarWidth::Thin.format_as_rust_code(0),
1438 "LayoutScrollbarWidth::Thin"
1439 );
1440 assert_eq!(
1441 LayoutScrollbarWidth::None.format_as_rust_code(7),
1442 "LayoutScrollbarWidth::None",
1443 "indent depth must not leak into a unit-variant literal"
1444 );
1445 assert_eq!(
1446 ScrollbarVisibilityMode::WhenScrolling.format_as_rust_code(0),
1447 "ScrollbarVisibilityMode::WhenScrolling"
1448 );
1449 assert_eq!(
1450 StyleScrollbarColor::Auto.format_as_rust_code(0),
1451 "StyleScrollbarColor::Auto"
1452 );
1453
1454 assert_eq!(
1456 ScrollbarFadeDelay::new(0).format_as_rust_code(0),
1457 "ScrollbarFadeDelay::new(0)"
1458 );
1459 assert_eq!(
1460 ScrollbarFadeDelay::new(u32::MAX).format_as_rust_code(3),
1461 "ScrollbarFadeDelay::new(4294967295)"
1462 );
1463 assert_eq!(
1464 ScrollbarFadeDuration::new(u32::MAX).format_as_rust_code(0),
1465 "ScrollbarFadeDuration::new(4294967295)"
1466 );
1467 }
1468
1469 #[test]
1470 fn format_as_rust_code_of_aggregates_does_not_panic() {
1471 let custom = StyleScrollbarColor::Custom(ScrollbarColorCustom {
1472 thumb: ColorU::TRANSPARENT,
1473 track: ColorU::WHITE,
1474 })
1475 .format_as_rust_code(0);
1476 assert!(
1477 custom.starts_with("StyleScrollbarColor::Custom(ScrollbarColorCustom {"),
1478 "{custom}"
1479 );
1480 assert!(custom.contains("thumb:") && custom.contains("track:"), "{custom}");
1481
1482 for tabs in [0usize, 1, 4] {
1483 let code = ScrollbarStyle::default().format_as_rust_code(tabs);
1484 assert!(code.starts_with("ScrollbarStyle {"), "{code}");
1485 assert!(code.contains("horizontal:"), "{code}");
1486 assert!(code.contains("vertical:"), "{code}");
1487 }
1488 }
1489
1490 #[test]
1495 fn scrollbar_info_default_is_the_classic_light_constant() {
1496 assert_eq!(ScrollbarInfo::default(), SCROLLBAR_CLASSIC_LIGHT);
1497
1498 let style = ScrollbarStyle::default();
1499 assert_eq!(style.horizontal, SCROLLBAR_CLASSIC_LIGHT);
1500 assert_eq!(style.vertical, SCROLLBAR_CLASSIC_LIGHT);
1501 }
1502
1503 #[test]
1507 fn computed_default_mirrors_the_default_scrollbar_info() {
1508 let computed = ComputedScrollbarStyle::default();
1509 let info = ScrollbarInfo::default();
1510
1511 assert_eq!(computed.width, Some(info.width));
1512 assert_eq!(
1513 computed.thumb_color,
1514 Some(ColorU { r: 193, g: 193, b: 193, a: 255 })
1515 );
1516 assert_eq!(
1517 computed.track_color,
1518 Some(ColorU { r: 241, g: 241, b: 241, a: 255 })
1519 );
1520 assert!(
1521 computed.thumb_color.is_some() && computed.track_color.is_some(),
1522 "the classic-light default must resolve to solid colors, not None"
1523 );
1524 }
1525
1526 #[test]
1530 fn preset_constants_agree_on_the_overlay_clipping_flag() {
1531 for info in [SCROLLBAR_CLASSIC_LIGHT, SCROLLBAR_CLASSIC_DARK] {
1532 assert!(!info.clip_to_container_border);
1533 assert_eq!(info.scroll_behavior, ScrollBehavior::Auto);
1534 }
1535 for info in [
1536 SCROLLBAR_MACOS_LIGHT,
1537 SCROLLBAR_MACOS_DARK,
1538 SCROLLBAR_IOS_LIGHT,
1539 SCROLLBAR_IOS_DARK,
1540 SCROLLBAR_ANDROID_LIGHT,
1541 SCROLLBAR_ANDROID_DARK,
1542 ] {
1543 assert!(info.clip_to_container_border);
1544 assert_eq!(info.scroll_behavior, ScrollBehavior::Smooth);
1545 }
1546 for info in [SCROLLBAR_WINDOWS_LIGHT, SCROLLBAR_WINDOWS_DARK] {
1547 assert!(!info.clip_to_container_border);
1548 assert_eq!(info.overscroll_behavior_x, OverscrollBehavior::None);
1549 assert_eq!(info.overscroll_behavior_y, OverscrollBehavior::None);
1550 }
1551 }
1552
1553 #[test]
1556 fn light_and_dark_presets_share_their_geometry() {
1557 for (light, dark) in [
1558 (SCROLLBAR_CLASSIC_LIGHT, SCROLLBAR_CLASSIC_DARK),
1559 (SCROLLBAR_MACOS_LIGHT, SCROLLBAR_MACOS_DARK),
1560 (SCROLLBAR_WINDOWS_LIGHT, SCROLLBAR_WINDOWS_DARK),
1561 (SCROLLBAR_IOS_LIGHT, SCROLLBAR_IOS_DARK),
1562 (SCROLLBAR_ANDROID_LIGHT, SCROLLBAR_ANDROID_DARK),
1563 ] {
1564 assert_eq!(light.width, dark.width);
1565 assert_eq!(light.padding_left, dark.padding_left);
1566 assert_eq!(light.padding_right, dark.padding_right);
1567 assert_eq!(light.clip_to_container_border, dark.clip_to_container_border);
1568 assert_ne!(light.thumb, dark.thumb, "light/dark thumbs must differ");
1569 }
1570 }
1571
1572 #[cfg(feature = "parser")]
1577 #[test]
1578 fn scrollbar_width_parses_the_three_legal_keywords() {
1579 assert_eq!(
1580 parse_layout_scrollbar_width("auto"),
1581 Ok(LayoutScrollbarWidth::Auto)
1582 );
1583 assert_eq!(
1584 parse_layout_scrollbar_width("thin"),
1585 Ok(LayoutScrollbarWidth::Thin)
1586 );
1587 assert_eq!(
1588 parse_layout_scrollbar_width("none"),
1589 Ok(LayoutScrollbarWidth::None)
1590 );
1591 }
1592
1593 #[cfg(feature = "parser")]
1594 #[test]
1595 fn scrollbar_width_trims_surrounding_whitespace_but_rejects_inner_junk() {
1596 assert_eq!(
1597 parse_layout_scrollbar_width(" \t thin \n "),
1598 Ok(LayoutScrollbarWidth::Thin)
1599 );
1600 assert!(parse_layout_scrollbar_width("thin;").is_err());
1601 assert!(parse_layout_scrollbar_width("thin thin").is_err());
1602 assert!(parse_layout_scrollbar_width("th in").is_err());
1603 }
1604
1605 #[cfg(feature = "parser")]
1609 #[test]
1610 fn scrollbar_width_keyword_matching_is_case_sensitive() {
1611 assert!(parse_layout_scrollbar_width("AUTO").is_err());
1612 assert!(parse_layout_scrollbar_width("Thin").is_err());
1613 assert!(parse_layout_scrollbar_width("NONE").is_err());
1614 }
1615
1616 #[cfg(feature = "parser")]
1617 #[test]
1618 fn scrollbar_width_rejects_every_garbage_input_without_panicking() {
1619 for input in GARBAGE {
1620 assert!(
1621 parse_layout_scrollbar_width(input).is_err(),
1622 "expected {input:?} to be rejected"
1623 );
1624 }
1625 }
1626
1627 #[cfg(feature = "parser")]
1630 #[test]
1631 fn scrollbar_width_error_keeps_the_raw_untrimmed_input() {
1632 let raw = " thick ";
1633 assert_eq!(
1634 parse_layout_scrollbar_width(raw),
1635 Err(LayoutScrollbarWidthParseError::InvalidValue(raw))
1636 );
1637 let msg = format!("{}", parse_layout_scrollbar_width(raw).unwrap_err());
1638 assert!(msg.contains(raw), "{msg}");
1639 }
1640
1641 #[cfg(feature = "parser")]
1642 #[test]
1643 fn scrollbar_width_survives_a_megabyte_of_input_and_deep_nesting() {
1644 let huge = "a".repeat(1_000_000);
1645 assert!(parse_layout_scrollbar_width(&huge).is_err());
1646
1647 let repeated_token = "auto".repeat(250_000);
1648 assert!(parse_layout_scrollbar_width(&repeated_token).is_err());
1649
1650 let nested = "(".repeat(10_000);
1651 assert!(parse_layout_scrollbar_width(&nested).is_err());
1652 }
1653
1654 #[cfg(feature = "parser")]
1655 #[test]
1656 fn scrollbar_width_round_trips_through_its_printer() {
1657 for value in [
1658 LayoutScrollbarWidth::Auto,
1659 LayoutScrollbarWidth::Thin,
1660 LayoutScrollbarWidth::None,
1661 ] {
1662 let encoded = value.print_as_css_value();
1663 assert_eq!(
1664 parse_layout_scrollbar_width(&encoded),
1665 Ok(value),
1666 "{encoded} did not decode back to {value:?}"
1667 );
1668 }
1669 }
1670
1671 #[cfg(feature = "parser")]
1676 #[test]
1677 fn scrollbar_color_needs_exactly_two_colors_or_the_auto_keyword() {
1678 assert_eq!(parse_style_scrollbar_color("auto"), Ok(StyleScrollbarColor::Auto));
1679 assert_eq!(
1680 parse_style_scrollbar_color(" auto "),
1681 Ok(StyleScrollbarColor::Auto)
1682 );
1683 assert_eq!(
1684 parse_style_scrollbar_color("red blue"),
1685 Ok(StyleScrollbarColor::Custom(ScrollbarColorCustom {
1686 thumb: ColorU::RED,
1687 track: ColorU::BLUE,
1688 }))
1689 );
1690
1691 for input in ["red", "#fff", "red blue green", "a b c d"] {
1693 assert!(
1694 matches!(
1695 parse_style_scrollbar_color(input),
1696 Err(StyleScrollbarColorParseError::InvalidValue(_))
1697 ),
1698 "expected {input:?} to be an InvalidValue error"
1699 );
1700 }
1701 }
1702
1703 #[cfg(feature = "parser")]
1706 #[test]
1707 fn scrollbar_color_accepts_any_whitespace_run_as_the_separator() {
1708 let expected = StyleScrollbarColor::Custom(ScrollbarColorCustom {
1709 thumb: ColorU::RED,
1710 track: ColorU::BLUE,
1711 });
1712 assert_eq!(parse_style_scrollbar_color("red\tblue"), Ok(expected));
1713 assert_eq!(parse_style_scrollbar_color("red\n blue"), Ok(expected));
1714 assert_eq!(parse_style_scrollbar_color(" red blue "), Ok(expected));
1715 }
1716
1717 #[cfg(feature = "parser")]
1720 #[test]
1721 fn scrollbar_color_names_are_case_insensitive_but_the_auto_keyword_is_not() {
1722 assert_eq!(
1723 parse_style_scrollbar_color("RED BLUE"),
1724 Ok(StyleScrollbarColor::Custom(ScrollbarColorCustom {
1725 thumb: ColorU::RED,
1726 track: ColorU::BLUE,
1727 }))
1728 );
1729 assert!(matches!(
1731 parse_style_scrollbar_color("AUTO"),
1732 Err(StyleScrollbarColorParseError::InvalidValue(_))
1733 ));
1734 assert!(matches!(
1736 parse_style_scrollbar_color("auto auto"),
1737 Err(StyleScrollbarColorParseError::Color(_))
1738 ));
1739 }
1740
1741 #[cfg(feature = "parser")]
1746 #[test]
1747 fn scrollbar_color_rejects_functional_colors_containing_spaces() {
1748 assert_eq!(
1749 parse_style_scrollbar_color("rgb(255,0,0) blue"),
1750 Ok(StyleScrollbarColor::Custom(ScrollbarColorCustom {
1751 thumb: ColorU::RED,
1752 track: ColorU::BLUE,
1753 }))
1754 );
1755 assert!(matches!(
1756 parse_style_scrollbar_color("rgb(255, 0, 0) blue"),
1757 Err(StyleScrollbarColorParseError::InvalidValue(_))
1758 ));
1759 }
1760
1761 #[cfg(feature = "parser")]
1762 #[test]
1763 fn scrollbar_color_reports_which_component_failed() {
1764 assert!(matches!(
1766 parse_style_scrollbar_color("notacolor blue"),
1767 Err(StyleScrollbarColorParseError::Color(_))
1768 ));
1769 assert!(matches!(
1770 parse_style_scrollbar_color("red notacolor"),
1771 Err(StyleScrollbarColorParseError::Color(_))
1772 ));
1773 assert!(matches!(
1774 parse_style_scrollbar_color("#gggggg #000000"),
1775 Err(StyleScrollbarColorParseError::Color(_))
1776 ));
1777 }
1778
1779 #[cfg(feature = "parser")]
1780 #[test]
1781 fn scrollbar_color_rejects_garbage_without_panicking() {
1782 for input in GARBAGE {
1783 assert!(
1784 parse_style_scrollbar_color(input).is_err(),
1785 "expected {input:?} to be rejected"
1786 );
1787 }
1788 for input in [
1790 "0 0",
1791 "-0 -0",
1792 "NaN NaN",
1793 "inf inf",
1794 "9223372036854775807 1",
1795 "1e400 1e400",
1796 "-1 -1",
1797 ] {
1798 assert!(
1799 parse_style_scrollbar_color(input).is_err(),
1800 "expected {input:?} to be rejected"
1801 );
1802 }
1803 }
1804
1805 #[cfg(feature = "parser")]
1806 #[test]
1807 fn scrollbar_color_survives_huge_and_deeply_nested_input() {
1808 let huge = "z".repeat(500_000);
1809 let two_huge = format!("{huge} {huge}");
1810 assert!(parse_style_scrollbar_color(&two_huge).is_err());
1811
1812 let nested = "(".repeat(10_000);
1813 assert!(parse_style_scrollbar_color(&format!("{nested} {nested}")).is_err());
1814
1815 let many = "red ".repeat(100_000);
1817 assert!(matches!(
1818 parse_style_scrollbar_color(&many),
1819 Err(StyleScrollbarColorParseError::InvalidValue(_))
1820 ));
1821 }
1822
1823 #[cfg(feature = "parser")]
1827 #[test]
1828 fn scrollbar_color_error_carries_the_trimmed_input() {
1829 assert_eq!(
1830 parse_style_scrollbar_color(" red "),
1831 Err(StyleScrollbarColorParseError::InvalidValue("red"))
1832 );
1833 }
1834
1835 #[cfg(feature = "parser")]
1836 #[test]
1837 fn scrollbar_color_round_trips_through_its_printer() {
1838 let samples = [
1839 StyleScrollbarColor::Auto,
1840 StyleScrollbarColor::Custom(ScrollbarColorCustom {
1841 thumb: ColorU::RED,
1842 track: ColorU::BLUE,
1843 }),
1844 StyleScrollbarColor::Custom(ScrollbarColorCustom {
1845 thumb: ColorU::TRANSPARENT,
1846 track: ColorU::TRANSPARENT,
1847 }),
1848 StyleScrollbarColor::Custom(ScrollbarColorCustom {
1849 thumb: ColorU { r: 0, g: 0, b: 0, a: 100 },
1850 track: ColorU { r: 1, g: 2, b: 3, a: 4 },
1851 }),
1852 StyleScrollbarColor::Custom(ScrollbarColorCustom {
1853 thumb: ColorU::WHITE,
1854 track: ColorU::BLACK,
1855 }),
1856 ];
1857 for value in samples {
1858 let encoded = value.print_as_css_value();
1859 assert_eq!(
1860 parse_style_scrollbar_color(&encoded),
1861 Ok(value),
1862 "{encoded} did not decode back to {value:?}"
1863 );
1864 }
1865 }
1866
1867 #[cfg(feature = "parser")]
1872 #[test]
1873 fn visibility_mode_parses_its_three_keywords_and_trims() {
1874 assert_eq!(
1875 parse_scrollbar_visibility_mode("always"),
1876 Ok(ScrollbarVisibilityMode::Always)
1877 );
1878 assert_eq!(
1879 parse_scrollbar_visibility_mode(" when-scrolling\t"),
1880 Ok(ScrollbarVisibilityMode::WhenScrolling)
1881 );
1882 assert_eq!(
1883 parse_scrollbar_visibility_mode("auto"),
1884 Ok(ScrollbarVisibilityMode::Auto)
1885 );
1886 }
1887
1888 #[cfg(feature = "parser")]
1889 #[test]
1890 fn visibility_mode_rejects_near_misses_and_garbage() {
1891 for input in [
1892 "when scrolling", "whenscrolling",
1894 "when-scrolling-",
1895 "-when-scrolling",
1896 "ALWAYS",
1897 "always;",
1898 "always auto",
1899 ] {
1900 assert!(
1901 parse_scrollbar_visibility_mode(input).is_err(),
1902 "expected {input:?} to be rejected"
1903 );
1904 }
1905 for input in GARBAGE {
1906 assert!(
1907 parse_scrollbar_visibility_mode(input).is_err(),
1908 "expected {input:?} to be rejected"
1909 );
1910 }
1911 }
1912
1913 #[cfg(feature = "parser")]
1914 #[test]
1915 fn visibility_mode_survives_huge_and_nested_input() {
1916 assert!(parse_scrollbar_visibility_mode(&"a".repeat(1_000_000)).is_err());
1917 assert!(parse_scrollbar_visibility_mode(&"always".repeat(200_000)).is_err());
1918 assert!(parse_scrollbar_visibility_mode(&"[".repeat(10_000)).is_err());
1919 }
1920
1921 #[cfg(feature = "parser")]
1922 #[test]
1923 fn visibility_mode_round_trips_through_its_printer() {
1924 for value in [
1925 ScrollbarVisibilityMode::Always,
1926 ScrollbarVisibilityMode::WhenScrolling,
1927 ScrollbarVisibilityMode::Auto,
1928 ] {
1929 let encoded = value.print_as_css_value();
1930 assert_eq!(
1931 parse_scrollbar_visibility_mode(&encoded),
1932 Ok(value),
1933 "{encoded} did not decode back to {value:?}"
1934 );
1935 }
1936 }
1937
1938 #[cfg(feature = "parser")]
1943 #[test]
1944 fn parse_time_ms_accepts_bare_zero_and_both_units() {
1945 assert_eq!(parse_time_ms("0"), Some(0));
1946 assert_eq!(parse_time_ms("0ms"), Some(0));
1947 assert_eq!(parse_time_ms("0s"), Some(0));
1948 assert_eq!(parse_time_ms("500ms"), Some(500));
1949 assert_eq!(parse_time_ms("1s"), Some(1000));
1950 assert_eq!(parse_time_ms("1.5s"), Some(1500));
1951 assert_eq!(parse_time_ms(" 200ms "), Some(200));
1952 assert_eq!(parse_time_ms("200MS"), Some(200), "units are case-insensitive");
1953 }
1954
1955 #[cfg(feature = "parser")]
1958 #[test]
1959 fn parse_time_ms_requires_an_attached_unit() {
1960 assert_eq!(parse_time_ms("500"), None);
1961 assert_eq!(parse_time_ms("1 s"), None);
1962 assert_eq!(parse_time_ms("500 ms"), None);
1963 assert_eq!(parse_time_ms("ms"), None);
1964 assert_eq!(parse_time_ms("s"), None);
1965 assert_eq!(parse_time_ms("500px"), None);
1966 assert_eq!(parse_time_ms("500msms"), None);
1967 }
1968
1969 #[cfg(feature = "parser")]
1970 #[test]
1971 fn parse_time_ms_rejects_empty_blank_unicode_and_garbage() {
1972 for input in ["", " ", " ", "\t\n", "\u{1F600}", "e\u{0301}", "٥ms", "500ms"] {
1973 assert_eq!(parse_time_ms(input), None, "expected {input:?} to be rejected");
1974 }
1975 }
1976
1977 #[cfg(feature = "parser")]
1978 #[test]
1979 fn parse_time_ms_rejects_negative_durations() {
1980 assert_eq!(parse_time_ms("-1ms"), None);
1981 assert_eq!(parse_time_ms("-0.5s"), None);
1982 assert_eq!(parse_time_ms("-inf ms"), None);
1983 }
1984
1985 #[cfg(feature = "parser")]
1988 #[test]
1989 fn parse_time_ms_accepts_negative_zero_as_zero() {
1990 assert_eq!(parse_time_ms("-0ms"), Some(0));
1991 assert_eq!(parse_time_ms("-0.0s"), Some(0));
1992 }
1993
1994 #[cfg(feature = "parser")]
1999 #[test]
2000 fn parse_time_ms_saturates_on_non_finite_and_huge_values() {
2001 assert_eq!(parse_time_ms("infms"), Some(u32::MAX));
2002 assert_eq!(parse_time_ms("infinityms"), Some(u32::MAX));
2003 assert_eq!(parse_time_ms("infs"), Some(u32::MAX));
2004 assert_eq!(parse_time_ms("nanms"), Some(0));
2005 assert_eq!(parse_time_ms("NaNms"), Some(0));
2006
2007 assert_eq!(parse_time_ms("1e30ms"), Some(u32::MAX));
2008 assert_eq!(parse_time_ms("1e400ms"), Some(u32::MAX), "overflows f32 to inf");
2009 assert_eq!(parse_time_ms("4294967296ms"), Some(u32::MAX), "2^32 clamps");
2010 assert_eq!(parse_time_ms("1e-30ms"), Some(0), "underflows to zero");
2011
2012 let long_number = format!("{}ms", "9".repeat(100_000));
2014 assert_eq!(parse_time_ms(&long_number), Some(u32::MAX));
2015 }
2016
2017 #[cfg(feature = "parser")]
2020 #[test]
2021 fn parse_time_ms_saturates_when_seconds_overflow_milliseconds() {
2022 assert_eq!(parse_time_ms("1000s"), Some(1_000_000));
2024 assert_eq!(parse_time_ms("16777s"), Some(16_777_000));
2025
2026 assert_eq!(parse_time_ms("4294968s"), Some(u32::MAX));
2028 assert_eq!(parse_time_ms("5000000s"), Some(u32::MAX));
2029
2030 let ms = parse_time_ms("4294967s").expect("4294967s must parse");
2033 assert!(
2034 ms.abs_diff(4_294_967_000) <= 512,
2035 "4294967s decoded to {ms}, which is nowhere near 4294967000ms"
2036 );
2037
2038 assert_eq!(parse_time_ms("0.0005s"), Some(0), "sub-ms truncates toward zero");
2039 }
2040
2041 #[cfg(feature = "parser")]
2046 #[test]
2047 fn fade_parsers_accept_the_documented_syntax() {
2048 assert_eq!(
2049 parse_scrollbar_fade_delay("500ms"),
2050 Ok(ScrollbarFadeDelay::new(500))
2051 );
2052 assert_eq!(parse_scrollbar_fade_delay("0"), Ok(ScrollbarFadeDelay::ZERO));
2053 assert_eq!(
2054 parse_scrollbar_fade_delay(" 1s "),
2055 Ok(ScrollbarFadeDelay::new(1000))
2056 );
2057 assert_eq!(
2058 parse_scrollbar_fade_duration("200ms"),
2059 Ok(ScrollbarFadeDuration::new(200))
2060 );
2061 assert_eq!(
2062 parse_scrollbar_fade_duration("0"),
2063 Ok(ScrollbarFadeDuration::ZERO)
2064 );
2065 }
2066
2067 #[cfg(feature = "parser")]
2068 #[test]
2069 fn fade_parsers_reject_garbage_and_keep_the_raw_input_in_the_error() {
2070 for input in GARBAGE {
2071 assert!(
2072 parse_scrollbar_fade_delay(input).is_err(),
2073 "delay: expected {input:?} to be rejected"
2074 );
2075 assert!(
2076 parse_scrollbar_fade_duration(input).is_err(),
2077 "duration: expected {input:?} to be rejected"
2078 );
2079 }
2080
2081 let raw = " bogus ";
2082 assert_eq!(
2083 parse_scrollbar_fade_delay(raw),
2084 Err(ScrollbarFadeDelayParseError::InvalidValue(raw))
2085 );
2086 assert_eq!(
2087 parse_scrollbar_fade_duration(raw),
2088 Err(ScrollbarFadeDurationParseError::InvalidValue(raw))
2089 );
2090 }
2091
2092 #[cfg(feature = "parser")]
2093 #[test]
2094 fn fade_parsers_reject_negative_delays() {
2095 assert!(parse_scrollbar_fade_delay("-1ms").is_err());
2096 assert!(parse_scrollbar_fade_delay("-500ms").is_err());
2097 assert!(parse_scrollbar_fade_duration("-0.5s").is_err());
2098 }
2099
2100 #[cfg(feature = "parser")]
2101 #[test]
2102 fn fade_parsers_saturate_instead_of_overflowing() {
2103 assert_eq!(
2104 parse_scrollbar_fade_delay("1e30ms"),
2105 Ok(ScrollbarFadeDelay::new(u32::MAX))
2106 );
2107 assert_eq!(
2108 parse_scrollbar_fade_duration("99999999999999s"),
2109 Ok(ScrollbarFadeDuration::new(u32::MAX))
2110 );
2111 }
2112
2113 #[cfg(feature = "parser")]
2114 #[test]
2115 fn fade_parsers_survive_huge_and_nested_input() {
2116 assert!(parse_scrollbar_fade_delay(&"a".repeat(1_000_000)).is_err());
2117 assert!(parse_scrollbar_fade_duration(&"0ms".repeat(300_000)).is_err());
2118 assert!(parse_scrollbar_fade_delay(&"(".repeat(10_000)).is_err());
2119 assert!(parse_scrollbar_fade_duration(&"[".repeat(10_000)).is_err());
2120 }
2121
2122 #[cfg(feature = "parser")]
2127 #[test]
2128 fn fade_delay_round_trips_exactly_up_to_two_pow_24() {
2129 for ms in [
2130 0u32,
2131 1,
2132 8,
2133 16,
2134 200,
2135 500,
2136 65_535,
2137 1_000_000,
2138 TWO_POW_24 - 1,
2139 TWO_POW_24,
2140 u32::MAX,
2141 ] {
2142 let value = ScrollbarFadeDelay::new(ms);
2143 let encoded = value.print_as_css_value();
2144 assert_eq!(
2145 parse_scrollbar_fade_delay(&encoded),
2146 Ok(value),
2147 "{ms}ms encoded as {encoded:?} did not decode back"
2148 );
2149
2150 let value = ScrollbarFadeDuration::new(ms);
2151 let encoded = value.print_as_css_value();
2152 assert_eq!(
2153 parse_scrollbar_fade_duration(&encoded),
2154 Ok(value),
2155 "{ms}ms encoded as {encoded:?} did not decode back"
2156 );
2157 }
2158 }
2159
2160 #[cfg(feature = "parser")]
2164 #[test]
2165 fn fade_delay_round_trip_is_lossy_above_two_pow_24() {
2166 let value = ScrollbarFadeDelay::new(TWO_POW_24 + 1);
2167 let decoded = parse_scrollbar_fade_delay(&value.print_as_css_value()).unwrap();
2168 assert_ne!(decoded, value, "expected precision loss above 2^24");
2169 assert_eq!(decoded.ms, TWO_POW_24, "must snap down to the nearest f32");
2170 }
2171
2172 fn error_payloads() -> [String; 6] {
2179 [
2180 String::new(),
2181 String::from(" "),
2182 String::from("thick"),
2183 String::from("\u{1F600}\u{0301}"),
2184 String::from("nul\0inside"),
2185 "x".repeat(100_000),
2186 ]
2187 }
2188
2189 #[test]
2190 fn layout_scrollbar_width_error_round_trips_through_owned_and_back() {
2191 for payload in error_payloads() {
2192 let shared = LayoutScrollbarWidthParseError::InvalidValue(&payload);
2193 let owned = shared.to_contained();
2194 assert_eq!(
2195 owned,
2196 LayoutScrollbarWidthParseErrorOwned::InvalidValue(payload.clone().into())
2197 );
2198 assert_eq!(owned.to_shared(), shared, "owned -> shared lost information");
2199 assert_eq!(
2200 owned.to_shared().to_contained(),
2201 owned,
2202 "conversion is not idempotent"
2203 );
2204 }
2205 }
2206
2207 #[test]
2208 fn visibility_mode_error_round_trips_through_owned_and_back() {
2209 for payload in error_payloads() {
2210 let shared = ScrollbarVisibilityModeParseError::InvalidValue(&payload);
2211 let owned = shared.to_contained();
2212 assert_eq!(owned.to_shared(), shared);
2213 assert_eq!(owned.to_shared().to_contained(), owned);
2214 }
2215 }
2216
2217 #[test]
2218 fn fade_delay_and_duration_errors_round_trip_through_owned_and_back() {
2219 for payload in error_payloads() {
2220 let delay = ScrollbarFadeDelayParseError::InvalidValue(&payload);
2221 let owned_delay = delay.to_contained();
2222 assert_eq!(owned_delay.to_shared(), delay);
2223 assert_eq!(owned_delay.to_shared().to_contained(), owned_delay);
2224
2225 let duration = ScrollbarFadeDurationParseError::InvalidValue(&payload);
2226 let owned_duration = duration.to_contained();
2227 assert_eq!(owned_duration.to_shared(), duration);
2228 assert_eq!(owned_duration.to_shared().to_contained(), owned_duration);
2229 }
2230 }
2231
2232 #[test]
2233 fn scrollbar_color_invalid_value_error_round_trips_through_owned_and_back() {
2234 for payload in error_payloads() {
2235 let shared = StyleScrollbarColorParseError::InvalidValue(&payload);
2236 let owned = shared.to_contained();
2237 assert_eq!(
2238 owned,
2239 StyleScrollbarColorParseErrorOwned::InvalidValue(payload.clone().into())
2240 );
2241 assert_eq!(owned.to_shared(), shared);
2242 assert_eq!(owned.to_shared().to_contained(), owned);
2243 }
2244 }
2245
2246 #[cfg(feature = "parser")]
2249 #[test]
2250 fn scrollbar_color_nested_color_error_round_trips_through_owned_and_back() {
2251 let shared = parse_style_scrollbar_color("notacolor blue").unwrap_err();
2252 assert!(matches!(shared, StyleScrollbarColorParseError::Color(_)));
2253
2254 let owned = shared.to_contained();
2255 assert!(matches!(owned, StyleScrollbarColorParseErrorOwned::Color(_)));
2256 assert_eq!(owned.to_shared(), shared, "nested color error lost information");
2257 assert_eq!(owned.to_shared().to_contained(), owned);
2258 }
2259
2260 #[cfg(feature = "parser")]
2263 #[test]
2264 fn error_display_mentions_the_offending_input() {
2265 let width = parse_layout_scrollbar_width("thick").unwrap_err();
2266 assert!(format!("{width}").contains("thick"), "{width}");
2267 assert!(format!("{width:?}").contains("thick"), "{width:?}");
2268
2269 let color = parse_style_scrollbar_color("red").unwrap_err();
2270 assert!(format!("{color}").contains("red"), "{color}");
2271
2272 let vis = parse_scrollbar_visibility_mode("sometimes").unwrap_err();
2273 assert!(format!("{vis}").contains("sometimes"), "{vis}");
2274
2275 let delay = parse_scrollbar_fade_delay("soon").unwrap_err();
2276 assert!(format!("{delay}").contains("soon"), "{delay}");
2277
2278 let duration = parse_scrollbar_fade_duration("briefly").unwrap_err();
2279 assert!(format!("{duration}").contains("briefly"), "{duration}");
2280 }
2281
2282 #[test]
2285 fn error_display_does_not_panic_on_exotic_payloads() {
2286 for payload in error_payloads() {
2287 let err = LayoutScrollbarWidthParseError::InvalidValue(&payload);
2288 assert!(!format!("{err}").is_empty());
2289 let owned = err.to_contained();
2290 assert!(!format!("{}", owned.to_shared()).is_empty());
2291 }
2292 }
2293}