1use crate::corety::AzString;
4use alloc::string::{String, ToString};
5
6use crate::props::{
7 basic::color::{parse_css_color, ColorU, CssColorParseError, CssColorParseErrorOwned},
8 formatter::PrintAsCssValue,
9 layout::{
10 dimensions::LayoutWidth,
11 spacing::{LayoutPaddingLeft, LayoutPaddingRight},
12 },
13 style::background::StyleBackgroundContent,
14};
15
16#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
23#[repr(C)]
24pub enum ScrollBehavior {
25 #[default]
27 Auto,
28 Smooth,
30}
31
32impl PrintAsCssValue for ScrollBehavior {
33 fn print_as_css_value(&self) -> String {
34 match self {
35 Self::Auto => "auto".to_string(),
36 Self::Smooth => "smooth".to_string(),
37 }
38 }
39}
40
41#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
44#[repr(C)]
45pub enum OverscrollBehavior {
46 #[default]
48 Auto,
49 Contain,
51 None,
53}
54
55impl PrintAsCssValue for OverscrollBehavior {
56 fn print_as_css_value(&self) -> String {
57 match self {
58 Self::Auto => "auto".to_string(),
59 Self::Contain => "contain".to_string(),
60 Self::None => "none".to_string(),
61 }
62 }
63}
64
65#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
74#[repr(C)]
75pub struct ScrollPhysics {
76 pub smooth_scroll_duration_ms: u32,
79
80 pub deceleration_rate: f32,
84
85 pub min_velocity_threshold: f32,
88
89 pub max_velocity: f32,
91
92 pub wheel_multiplier: f32,
95
96 pub invert_direction: bool,
98
99 pub overscroll_elasticity: f32,
102
103 pub max_overscroll_distance: f32,
106
107 pub bounce_back_duration_ms: u32,
110
111 pub timer_interval_ms: u32,
115
116 pub wheel_animate_bounce_ms: u32,
122}
123
124impl Default for ScrollPhysics {
125 fn default() -> Self {
126 Self {
127 smooth_scroll_duration_ms: 300,
128 deceleration_rate: 0.95,
129 min_velocity_threshold: 50.0,
130 max_velocity: 8000.0,
131 wheel_multiplier: 1.0,
132 invert_direction: false,
133 overscroll_elasticity: 0.0, max_overscroll_distance: 100.0,
135 bounce_back_duration_ms: 400,
136 timer_interval_ms: 16,
137 wheel_animate_bounce_ms: 120,
138 }
139 }
140}
141
142impl ScrollPhysics {
143 #[must_use]
145 pub const fn ios() -> Self {
146 Self {
147 smooth_scroll_duration_ms: 300,
148 deceleration_rate: 0.998,
149 min_velocity_threshold: 20.0,
150 max_velocity: 8000.0,
151 wheel_multiplier: 1.0,
152 invert_direction: true, overscroll_elasticity: 0.5,
154 max_overscroll_distance: 120.0,
155 bounce_back_duration_ms: 500,
156 timer_interval_ms: 16,
157 wheel_animate_bounce_ms: 120,
158 }
159 }
160
161 #[must_use]
163 pub const fn macos() -> Self {
164 Self {
165 smooth_scroll_duration_ms: 250,
166 deceleration_rate: 0.997,
167 min_velocity_threshold: 30.0,
168 max_velocity: 6000.0,
169 wheel_multiplier: 1.0,
170 invert_direction: true, overscroll_elasticity: 0.3,
172 max_overscroll_distance: 80.0,
173 bounce_back_duration_ms: 400,
174 timer_interval_ms: 16,
175 wheel_animate_bounce_ms: 120,
176 }
177 }
178
179 #[must_use]
181 pub const fn windows() -> Self {
182 Self {
183 smooth_scroll_duration_ms: 200,
184 deceleration_rate: 0.9,
185 min_velocity_threshold: 100.0,
186 max_velocity: 4000.0,
187 wheel_multiplier: 1.0,
188 invert_direction: false,
189 overscroll_elasticity: 0.0,
190 max_overscroll_distance: 0.0,
191 bounce_back_duration_ms: 200,
192 timer_interval_ms: 16,
193 wheel_animate_bounce_ms: 120,
194 }
195 }
196
197 #[must_use]
199 pub const fn android() -> Self {
200 Self {
201 smooth_scroll_duration_ms: 250,
202 deceleration_rate: 0.996,
203 min_velocity_threshold: 40.0,
204 max_velocity: 8000.0,
205 wheel_multiplier: 1.0,
206 invert_direction: false,
207 overscroll_elasticity: 0.2, max_overscroll_distance: 60.0,
209 bounce_back_duration_ms: 300,
210 timer_interval_ms: 16,
211 wheel_animate_bounce_ms: 120,
212 }
213 }
214}
215
216impl_option!(
217 ScrollPhysics,
218 OptionScrollPhysics,
219 [Debug, Copy, Clone, PartialEq, PartialOrd]
220);
221
222#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
240#[repr(C)]
241pub enum ScrollbarVisibilityMode {
242 #[default]
245 Always,
246 WhenScrolling,
249 Auto,
251}
252
253impl PrintAsCssValue for ScrollbarVisibilityMode {
254 fn print_as_css_value(&self) -> String {
255 match self {
256 Self::Always => "always".to_string(),
257 Self::WhenScrolling => "when-scrolling".to_string(),
258 Self::Auto => "auto".to_string(),
259 }
260 }
261}
262
263#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
274#[repr(C)]
275pub struct ScrollbarFadeDelay {
276 pub ms: u32,
278}
279
280impl ScrollbarFadeDelay {
281 #[must_use]
282 pub const fn new(ms: u32) -> Self {
283 Self { ms }
284 }
285 pub const ZERO: Self = Self { ms: 0 };
286}
287
288impl PrintAsCssValue for ScrollbarFadeDelay {
289 fn print_as_css_value(&self) -> String {
290 if self.ms == 0 {
291 "0".to_string()
292 } else {
293 format!("{}ms", self.ms)
294 }
295 }
296}
297
298#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
309#[repr(C)]
310pub struct ScrollbarFadeDuration {
311 pub ms: u32,
313}
314
315impl ScrollbarFadeDuration {
316 #[must_use]
317 pub const fn new(ms: u32) -> Self {
318 Self { ms }
319 }
320 pub const ZERO: Self = Self { ms: 0 };
321}
322
323impl PrintAsCssValue for ScrollbarFadeDuration {
324 fn print_as_css_value(&self) -> String {
325 if self.ms == 0 {
326 "0".to_string()
327 } else {
328 format!("{}ms", self.ms)
329 }
330 }
331}
332
333#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
347#[repr(C)]
348pub enum OverflowScrolling {
349 #[default]
351 Auto,
352 Touch,
354}
355
356impl PrintAsCssValue for OverflowScrolling {
357 fn print_as_css_value(&self) -> String {
358 match self {
359 Self::Auto => "auto".to_string(),
360 Self::Touch => "touch".to_string(),
361 }
362 }
363}
364
365#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
371#[repr(C)]
372#[derive(Default)]
373pub enum LayoutScrollbarWidth {
374 #[default]
375 Auto,
376 Thin,
377 None,
378}
379
380impl PrintAsCssValue for LayoutScrollbarWidth {
381 fn print_as_css_value(&self) -> String {
382 match self {
383 Self::Auto => "auto".to_string(),
384 Self::Thin => "thin".to_string(),
385 Self::None => "none".to_string(),
386 }
387 }
388}
389
390#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
392#[repr(C)]
393pub struct ScrollbarColorCustom {
394 pub thumb: ColorU,
395 pub track: ColorU,
396}
397
398#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
400#[repr(C, u8)]
401#[derive(Default)]
402pub enum StyleScrollbarColor {
403 #[default]
404 Auto,
405 Custom(ScrollbarColorCustom),
406}
407
408impl PrintAsCssValue for StyleScrollbarColor {
409 fn print_as_css_value(&self) -> String {
410 match self {
411 Self::Auto => "auto".to_string(),
412 Self::Custom(c) => format!("{} {}", c.thumb.to_hash(), c.track.to_hash()),
413 }
414 }
415}
416
417#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
421#[repr(C)]
422pub struct ScrollbarInfo {
423 pub width: LayoutWidth,
425 pub padding_left: LayoutPaddingLeft,
428 pub padding_right: LayoutPaddingRight,
430 pub track: StyleBackgroundContent,
434 pub thumb: StyleBackgroundContent,
436 pub button: StyleBackgroundContent,
438 pub corner: StyleBackgroundContent,
441 pub resizer: StyleBackgroundContent,
444 pub clip_to_container_border: bool,
449 pub scroll_behavior: ScrollBehavior,
451 pub overscroll_behavior_x: OverscrollBehavior,
453 pub overscroll_behavior_y: OverscrollBehavior,
455 pub overflow_scrolling: OverflowScrolling,
458}
459
460impl Default for ScrollbarInfo {
461 fn default() -> Self {
462 SCROLLBAR_CLASSIC_LIGHT
463 }
464}
465
466impl PrintAsCssValue for ScrollbarInfo {
467 fn print_as_css_value(&self) -> String {
468 format!(
470 "width: {}; padding-left: {}; padding-right: {}; track: {}; thumb: {}; button: {}; \
471 corner: {}; resizer: {}",
472 self.width.print_as_css_value(),
473 self.padding_left.print_as_css_value(),
474 self.padding_right.print_as_css_value(),
475 self.track.print_as_css_value(),
476 self.thumb.print_as_css_value(),
477 self.button.print_as_css_value(),
478 self.corner.print_as_css_value(),
479 self.resizer.print_as_css_value(),
480 )
481 }
482}
483
484#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
486#[repr(C)]
487pub struct ScrollbarStyle {
488 pub horizontal: ScrollbarInfo,
490 pub vertical: ScrollbarInfo,
492}
493
494impl PrintAsCssValue for ScrollbarStyle {
495 fn print_as_css_value(&self) -> String {
496 format!(
498 "horz({}), vert({})",
499 self.horizontal.print_as_css_value(),
500 self.vertical.print_as_css_value()
501 )
502 }
503}
504
505impl crate::codegen::format::FormatAsRustCode for ScrollbarStyle {
507 fn format_as_rust_code(&self, tabs: usize) -> String {
508 let t = String::from(" ").repeat(tabs);
509 let t1 = String::from(" ").repeat(tabs + 1);
510 format!(
511 "ScrollbarStyle {{\r\n{}horizontal: {},\r\n{}vertical: {},\r\n{}}}",
512 t1,
513 crate::codegen::format::format_scrollbar_info(&self.horizontal, tabs + 1),
514 t1,
515 crate::codegen::format::format_scrollbar_info(&self.vertical, tabs + 1),
516 t,
517 )
518 }
519}
520
521impl crate::codegen::format::FormatAsRustCode for OverscrollBehavior {
522 fn format_as_rust_code(&self, _tabs: usize) -> String {
523 match self {
524 Self::Auto => String::from("OverscrollBehavior::Auto"),
525 Self::Contain => String::from("OverscrollBehavior::Contain"),
526 Self::None => String::from("OverscrollBehavior::None"),
527 }
528 }
529}
530
531impl crate::codegen::format::FormatAsRustCode for LayoutScrollbarWidth {
532 fn format_as_rust_code(&self, _tabs: usize) -> String {
533 match self {
534 Self::Auto => String::from("LayoutScrollbarWidth::Auto"),
535 Self::Thin => String::from("LayoutScrollbarWidth::Thin"),
536 Self::None => String::from("LayoutScrollbarWidth::None"),
537 }
538 }
539}
540
541impl crate::codegen::format::FormatAsRustCode for StyleScrollbarColor {
542 fn format_as_rust_code(&self, _tabs: usize) -> String {
543 match self {
544 Self::Auto => String::from("StyleScrollbarColor::Auto"),
545 Self::Custom(c) => format!(
546 "StyleScrollbarColor::Custom(ScrollbarColorCustom {{ thumb: {}, track: {} }})",
547 crate::codegen::format::format_color_value(&c.thumb),
548 crate::codegen::format::format_color_value(&c.track)
549 ),
550 }
551 }
552}
553
554impl crate::codegen::format::FormatAsRustCode for ScrollbarVisibilityMode {
555 fn format_as_rust_code(&self, _tabs: usize) -> String {
556 match self {
557 Self::Always => String::from("ScrollbarVisibilityMode::Always"),
558 Self::WhenScrolling => String::from("ScrollbarVisibilityMode::WhenScrolling"),
559 Self::Auto => String::from("ScrollbarVisibilityMode::Auto"),
560 }
561 }
562}
563
564impl crate::codegen::format::FormatAsRustCode for ScrollbarFadeDelay {
565 fn format_as_rust_code(&self, _tabs: usize) -> String {
566 format!("ScrollbarFadeDelay::new({})", self.ms)
567 }
568}
569
570impl crate::codegen::format::FormatAsRustCode for ScrollbarFadeDuration {
571 fn format_as_rust_code(&self, _tabs: usize) -> String {
572 format!("ScrollbarFadeDuration::new({})", self.ms)
573 }
574}
575
576#[derive(Debug, Clone, PartialEq)]
581pub struct ComputedScrollbarStyle {
582 pub width: Option<LayoutWidth>,
584 pub thumb_color: Option<ColorU>,
586 pub track_color: Option<ColorU>,
588 pub handle_width: Option<f32>,
597 pub handle_radius: Option<f32>,
602}
603
604impl Default for ComputedScrollbarStyle {
605 fn default() -> Self {
606 let default_info = ScrollbarInfo::default();
607 Self {
608 width: Some(default_info.width), handle_width: None,
610 handle_radius: None,
611 thumb_color: match default_info.thumb {
612 StyleBackgroundContent::Color(c) => Some(c),
613 _ => None,
614 },
615 track_color: match default_info.track {
616 StyleBackgroundContent::Color(c) => Some(c),
617 _ => None,
618 },
619 }
620 }
621}
622
623pub const SCROLLBAR_CLASSIC_LIGHT: ScrollbarInfo = ScrollbarInfo {
627 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(17)),
628 padding_left: LayoutPaddingLeft {
629 inner: crate::props::basic::pixel::PixelValue::const_px(2),
630 },
631 padding_right: LayoutPaddingRight {
632 inner: crate::props::basic::pixel::PixelValue::const_px(2),
633 },
634 track: StyleBackgroundContent::Color(ColorU {
635 r: 241,
636 g: 241,
637 b: 241,
638 a: 255,
639 }),
640 thumb: StyleBackgroundContent::Color(ColorU {
641 r: 193,
642 g: 193,
643 b: 193,
644 a: 255,
645 }),
646 button: StyleBackgroundContent::Color(ColorU {
647 r: 163,
648 g: 163,
649 b: 163,
650 a: 255,
651 }),
652 corner: StyleBackgroundContent::Color(ColorU {
653 r: 241,
654 g: 241,
655 b: 241,
656 a: 255,
657 }),
658 resizer: StyleBackgroundContent::Color(ColorU {
659 r: 241,
660 g: 241,
661 b: 241,
662 a: 255,
663 }),
664 clip_to_container_border: false,
665 scroll_behavior: ScrollBehavior::Auto,
666 overscroll_behavior_x: OverscrollBehavior::Auto,
667 overscroll_behavior_y: OverscrollBehavior::Auto,
668 overflow_scrolling: OverflowScrolling::Auto,
669};
670
671pub const SCROLLBAR_CLASSIC_DARK: ScrollbarInfo = ScrollbarInfo {
673 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(17)),
674 padding_left: LayoutPaddingLeft {
675 inner: crate::props::basic::pixel::PixelValue::const_px(2),
676 },
677 padding_right: LayoutPaddingRight {
678 inner: crate::props::basic::pixel::PixelValue::const_px(2),
679 },
680 track: StyleBackgroundContent::Color(ColorU {
681 r: 45,
682 g: 45,
683 b: 45,
684 a: 255,
685 }),
686 thumb: StyleBackgroundContent::Color(ColorU {
687 r: 100,
688 g: 100,
689 b: 100,
690 a: 255,
691 }),
692 button: StyleBackgroundContent::Color(ColorU {
693 r: 120,
694 g: 120,
695 b: 120,
696 a: 255,
697 }),
698 corner: StyleBackgroundContent::Color(ColorU {
699 r: 45,
700 g: 45,
701 b: 45,
702 a: 255,
703 }),
704 resizer: StyleBackgroundContent::Color(ColorU {
705 r: 45,
706 g: 45,
707 b: 45,
708 a: 255,
709 }),
710 clip_to_container_border: false,
711 scroll_behavior: ScrollBehavior::Auto,
712 overscroll_behavior_x: OverscrollBehavior::Auto,
713 overscroll_behavior_y: OverscrollBehavior::Auto,
714 overflow_scrolling: OverflowScrolling::Auto,
715};
716
717pub const SCROLLBAR_MACOS_LIGHT: ScrollbarInfo = ScrollbarInfo {
719 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(8)),
720 padding_left: LayoutPaddingLeft {
721 inner: crate::props::basic::pixel::PixelValue::const_px(0),
722 },
723 padding_right: LayoutPaddingRight {
724 inner: crate::props::basic::pixel::PixelValue::const_px(0),
725 },
726 track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
727 thumb: StyleBackgroundContent::Color(ColorU {
728 r: 0,
729 g: 0,
730 b: 0,
731 a: 100,
732 }), button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
734 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
735 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
736 clip_to_container_border: true, scroll_behavior: ScrollBehavior::Smooth,
738 overscroll_behavior_x: OverscrollBehavior::Auto,
739 overscroll_behavior_y: OverscrollBehavior::Auto,
740 overflow_scrolling: OverflowScrolling::Auto,
741};
742
743pub const SCROLLBAR_MACOS_DARK: ScrollbarInfo = ScrollbarInfo {
745 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(8)),
746 padding_left: LayoutPaddingLeft {
747 inner: crate::props::basic::pixel::PixelValue::const_px(0),
748 },
749 padding_right: LayoutPaddingRight {
750 inner: crate::props::basic::pixel::PixelValue::const_px(0),
751 },
752 track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
753 thumb: StyleBackgroundContent::Color(ColorU {
754 r: 255,
755 g: 255,
756 b: 255,
757 a: 100,
758 }), button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
760 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
761 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
762 clip_to_container_border: true, scroll_behavior: ScrollBehavior::Smooth,
764 overscroll_behavior_x: OverscrollBehavior::Auto,
765 overscroll_behavior_y: OverscrollBehavior::Auto,
766 overflow_scrolling: OverflowScrolling::Auto,
767};
768
769pub const SCROLLBAR_WINDOWS_LIGHT: ScrollbarInfo = ScrollbarInfo {
771 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(12)),
772 padding_left: LayoutPaddingLeft {
773 inner: crate::props::basic::pixel::PixelValue::const_px(0),
774 },
775 padding_right: LayoutPaddingRight {
776 inner: crate::props::basic::pixel::PixelValue::const_px(0),
777 },
778 track: StyleBackgroundContent::Color(ColorU {
779 r: 241,
780 g: 241,
781 b: 241,
782 a: 255,
783 }),
784 thumb: StyleBackgroundContent::Color(ColorU {
785 r: 130,
786 g: 130,
787 b: 130,
788 a: 255,
789 }),
790 button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
791 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
792 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
793 clip_to_container_border: false,
794 scroll_behavior: ScrollBehavior::Auto,
795 overscroll_behavior_x: OverscrollBehavior::None,
796 overscroll_behavior_y: OverscrollBehavior::None,
797 overflow_scrolling: OverflowScrolling::Auto,
798};
799
800pub const SCROLLBAR_WINDOWS_DARK: ScrollbarInfo = ScrollbarInfo {
802 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(12)),
803 padding_left: LayoutPaddingLeft {
804 inner: crate::props::basic::pixel::PixelValue::const_px(0),
805 },
806 padding_right: LayoutPaddingRight {
807 inner: crate::props::basic::pixel::PixelValue::const_px(0),
808 },
809 track: StyleBackgroundContent::Color(ColorU {
810 r: 32,
811 g: 32,
812 b: 32,
813 a: 255,
814 }),
815 thumb: StyleBackgroundContent::Color(ColorU {
816 r: 110,
817 g: 110,
818 b: 110,
819 a: 255,
820 }),
821 button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
822 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
823 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
824 clip_to_container_border: false,
825 scroll_behavior: ScrollBehavior::Auto,
826 overscroll_behavior_x: OverscrollBehavior::None,
827 overscroll_behavior_y: OverscrollBehavior::None,
828 overflow_scrolling: OverflowScrolling::Auto,
829};
830
831pub const SCROLLBAR_IOS_LIGHT: ScrollbarInfo = ScrollbarInfo {
833 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(7)),
834 padding_left: LayoutPaddingLeft {
835 inner: crate::props::basic::pixel::PixelValue::const_px(0),
836 },
837 padding_right: LayoutPaddingRight {
838 inner: crate::props::basic::pixel::PixelValue::const_px(0),
839 },
840 track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
841 thumb: StyleBackgroundContent::Color(ColorU {
842 r: 0,
843 g: 0,
844 b: 0,
845 a: 102,
846 }), button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
848 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
849 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
850 clip_to_container_border: true, scroll_behavior: ScrollBehavior::Smooth,
852 overscroll_behavior_x: OverscrollBehavior::Auto,
853 overscroll_behavior_y: OverscrollBehavior::Auto,
854 overflow_scrolling: OverflowScrolling::Auto,
855};
856
857pub const SCROLLBAR_IOS_DARK: ScrollbarInfo = ScrollbarInfo {
859 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(7)),
860 padding_left: LayoutPaddingLeft {
861 inner: crate::props::basic::pixel::PixelValue::const_px(0),
862 },
863 padding_right: LayoutPaddingRight {
864 inner: crate::props::basic::pixel::PixelValue::const_px(0),
865 },
866 track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
867 thumb: StyleBackgroundContent::Color(ColorU {
868 r: 255,
869 g: 255,
870 b: 255,
871 a: 102,
872 }), button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
874 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
875 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
876 clip_to_container_border: true, scroll_behavior: ScrollBehavior::Smooth,
878 overscroll_behavior_x: OverscrollBehavior::Auto,
879 overscroll_behavior_y: OverscrollBehavior::Auto,
880 overflow_scrolling: OverflowScrolling::Auto,
881};
882
883pub const SCROLLBAR_ANDROID_LIGHT: ScrollbarInfo = ScrollbarInfo {
885 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(6)),
886 padding_left: LayoutPaddingLeft {
887 inner: crate::props::basic::pixel::PixelValue::const_px(0),
888 },
889 padding_right: LayoutPaddingRight {
890 inner: crate::props::basic::pixel::PixelValue::const_px(0),
891 },
892 track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
893 thumb: StyleBackgroundContent::Color(ColorU {
894 r: 0,
895 g: 0,
896 b: 0,
897 a: 102,
898 }), button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
900 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
901 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
902 clip_to_container_border: true, scroll_behavior: ScrollBehavior::Smooth,
904 overscroll_behavior_x: OverscrollBehavior::Contain,
905 overscroll_behavior_y: OverscrollBehavior::Auto,
906 overflow_scrolling: OverflowScrolling::Auto,
907};
908
909pub const SCROLLBAR_ANDROID_DARK: ScrollbarInfo = ScrollbarInfo {
911 width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(6)),
912 padding_left: LayoutPaddingLeft {
913 inner: crate::props::basic::pixel::PixelValue::const_px(0),
914 },
915 padding_right: LayoutPaddingRight {
916 inner: crate::props::basic::pixel::PixelValue::const_px(0),
917 },
918 track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
919 thumb: StyleBackgroundContent::Color(ColorU {
920 r: 255,
921 g: 255,
922 b: 255,
923 a: 102,
924 }), button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
926 corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
927 resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
928 clip_to_container_border: true, scroll_behavior: ScrollBehavior::Smooth,
930 overscroll_behavior_x: OverscrollBehavior::Contain,
931 overscroll_behavior_y: OverscrollBehavior::Auto,
932 overflow_scrolling: OverflowScrolling::Auto,
933};
934
935#[derive(Clone, PartialEq, Eq)]
938pub enum OverscrollBehaviorParseError<'a> {
939 InvalidValue(&'a str),
940}
941impl_debug_as_display!(OverscrollBehaviorParseError<'a>);
942impl_display! { OverscrollBehaviorParseError<'a>, {
943 InvalidValue(v) => format!("Invalid overscroll-behavior value: \"{}\"", v),
944}}
945
946#[derive(Debug, Clone, PartialEq, Eq)]
947#[repr(C, u8)]
948pub enum OverscrollBehaviorParseErrorOwned {
949 InvalidValue(AzString),
950}
951impl OverscrollBehaviorParseError<'_> {
952 #[must_use]
953 pub fn to_contained(&self) -> OverscrollBehaviorParseErrorOwned {
954 match self {
955 Self::InvalidValue(s) => {
956 OverscrollBehaviorParseErrorOwned::InvalidValue((*s).to_string().into())
957 }
958 }
959 }
960}
961impl OverscrollBehaviorParseErrorOwned {
962 #[must_use]
963 pub fn to_shared(&self) -> OverscrollBehaviorParseError<'_> {
964 match self {
965 Self::InvalidValue(s) => OverscrollBehaviorParseError::InvalidValue(s.as_str()),
966 }
967 }
968}
969
970#[cfg(feature = "parser")]
971pub fn parse_overscroll_behavior(
981 input: &str,
982) -> Result<OverscrollBehavior, OverscrollBehaviorParseError<'_>> {
983 match input.trim() {
984 "auto" => Ok(OverscrollBehavior::Auto),
985 "contain" => Ok(OverscrollBehavior::Contain),
986 "none" => Ok(OverscrollBehavior::None),
987 _ => Err(OverscrollBehaviorParseError::InvalidValue(input)),
988 }
989}
990
991#[derive(Clone, PartialEq, Eq)]
992pub enum LayoutScrollbarWidthParseError<'a> {
993 InvalidValue(&'a str),
994}
995impl_debug_as_display!(LayoutScrollbarWidthParseError<'a>);
996impl_display! { LayoutScrollbarWidthParseError<'a>, {
997 InvalidValue(v) => format!("Invalid scrollbar-width value: \"{}\"", v),
998}}
999
1000#[derive(Debug, Clone, PartialEq, Eq)]
1001#[repr(C, u8)]
1002pub enum LayoutScrollbarWidthParseErrorOwned {
1003 InvalidValue(AzString),
1004}
1005impl LayoutScrollbarWidthParseError<'_> {
1006 #[must_use]
1007 pub fn to_contained(&self) -> LayoutScrollbarWidthParseErrorOwned {
1008 match self {
1009 Self::InvalidValue(s) => {
1010 LayoutScrollbarWidthParseErrorOwned::InvalidValue((*s).to_string().into())
1011 }
1012 }
1013 }
1014}
1015impl LayoutScrollbarWidthParseErrorOwned {
1016 #[must_use]
1017 pub fn to_shared(&self) -> LayoutScrollbarWidthParseError<'_> {
1018 match self {
1019 Self::InvalidValue(s) => LayoutScrollbarWidthParseError::InvalidValue(s.as_str()),
1020 }
1021 }
1022}
1023
1024#[cfg(feature = "parser")]
1025pub fn parse_layout_scrollbar_width(
1029 input: &str,
1030) -> Result<LayoutScrollbarWidth, LayoutScrollbarWidthParseError<'_>> {
1031 match input.trim() {
1032 "auto" => Ok(LayoutScrollbarWidth::Auto),
1033 "thin" => Ok(LayoutScrollbarWidth::Thin),
1034 "none" => Ok(LayoutScrollbarWidth::None),
1035 _ => Err(LayoutScrollbarWidthParseError::InvalidValue(input)),
1036 }
1037}
1038
1039#[derive(Clone, PartialEq)]
1040pub enum StyleScrollbarColorParseError<'a> {
1041 InvalidValue(&'a str),
1042 Color(CssColorParseError<'a>),
1043}
1044impl_debug_as_display!(StyleScrollbarColorParseError<'a>);
1045impl_display! { StyleScrollbarColorParseError<'a>, {
1046 InvalidValue(v) => format!("Invalid scrollbar-color value: \"{}\"", v),
1047 Color(e) => format!("Invalid color in scrollbar-color: {}", e),
1048}}
1049impl_from!(CssColorParseError<'a>, StyleScrollbarColorParseError::Color);
1050
1051#[derive(Debug, Clone, PartialEq)]
1052#[repr(C, u8)]
1053pub enum StyleScrollbarColorParseErrorOwned {
1054 InvalidValue(AzString),
1055 Color(CssColorParseErrorOwned),
1056}
1057impl StyleScrollbarColorParseError<'_> {
1058 #[must_use]
1059 pub fn to_contained(&self) -> StyleScrollbarColorParseErrorOwned {
1060 match self {
1061 Self::InvalidValue(s) => {
1062 StyleScrollbarColorParseErrorOwned::InvalidValue((*s).to_string().into())
1063 }
1064 Self::Color(e) => StyleScrollbarColorParseErrorOwned::Color(e.to_contained()),
1065 }
1066 }
1067}
1068impl StyleScrollbarColorParseErrorOwned {
1069 #[must_use]
1070 pub fn to_shared(&self) -> StyleScrollbarColorParseError<'_> {
1071 match self {
1072 Self::InvalidValue(s) => StyleScrollbarColorParseError::InvalidValue(s.as_str()),
1073 Self::Color(e) => StyleScrollbarColorParseError::Color(e.to_shared()),
1074 }
1075 }
1076}
1077
1078#[cfg(feature = "parser")]
1079pub fn parse_style_scrollbar_color(
1083 input: &str,
1084) -> Result<StyleScrollbarColor, StyleScrollbarColorParseError<'_>> {
1085 let input = input.trim();
1086 if input == "auto" {
1087 return Ok(StyleScrollbarColor::Auto);
1088 }
1089
1090 let mut parts = input.split_whitespace();
1091 let thumb_str = parts
1092 .next()
1093 .ok_or(StyleScrollbarColorParseError::InvalidValue(input))?;
1094 let track_str = parts
1095 .next()
1096 .ok_or(StyleScrollbarColorParseError::InvalidValue(input))?;
1097
1098 if parts.next().is_some() {
1099 return Err(StyleScrollbarColorParseError::InvalidValue(input));
1100 }
1101
1102 let thumb = parse_css_color(thumb_str)?;
1103 let track = parse_css_color(track_str)?;
1104
1105 Ok(StyleScrollbarColor::Custom(ScrollbarColorCustom {
1106 thumb,
1107 track,
1108 }))
1109}
1110
1111#[derive(Clone, PartialEq, Eq)]
1114pub enum ScrollbarVisibilityModeParseError<'a> {
1115 InvalidValue(&'a str),
1116}
1117impl_debug_as_display!(ScrollbarVisibilityModeParseError<'a>);
1118impl_display! { ScrollbarVisibilityModeParseError<'a>, {
1119 InvalidValue(v) => format!("Invalid scrollbar-visibility value: \"{}\"", v),
1120}}
1121
1122#[derive(Debug, Clone, PartialEq, Eq)]
1123#[repr(C, u8)]
1124pub enum ScrollbarVisibilityModeParseErrorOwned {
1125 InvalidValue(AzString),
1126}
1127impl ScrollbarVisibilityModeParseError<'_> {
1128 #[must_use]
1129 pub fn to_contained(&self) -> ScrollbarVisibilityModeParseErrorOwned {
1130 match self {
1131 Self::InvalidValue(s) => {
1132 ScrollbarVisibilityModeParseErrorOwned::InvalidValue((*s).to_string().into())
1133 }
1134 }
1135 }
1136}
1137impl ScrollbarVisibilityModeParseErrorOwned {
1138 #[must_use]
1139 pub fn to_shared(&self) -> ScrollbarVisibilityModeParseError<'_> {
1140 match self {
1141 Self::InvalidValue(s) => ScrollbarVisibilityModeParseError::InvalidValue(s.as_str()),
1142 }
1143 }
1144}
1145
1146#[cfg(feature = "parser")]
1147pub fn parse_scrollbar_visibility_mode(
1151 input: &str,
1152) -> Result<ScrollbarVisibilityMode, ScrollbarVisibilityModeParseError<'_>> {
1153 match input.trim() {
1154 "always" => Ok(ScrollbarVisibilityMode::Always),
1155 "when-scrolling" => Ok(ScrollbarVisibilityMode::WhenScrolling),
1156 "auto" => Ok(ScrollbarVisibilityMode::Auto),
1157 _ => Err(ScrollbarVisibilityModeParseError::InvalidValue(input)),
1158 }
1159}
1160
1161#[derive(Clone, PartialEq, Eq)]
1164pub enum ScrollbarFadeDelayParseError<'a> {
1165 InvalidValue(&'a str),
1166}
1167impl_debug_as_display!(ScrollbarFadeDelayParseError<'a>);
1168impl_display! { ScrollbarFadeDelayParseError<'a>, {
1169 InvalidValue(v) => format!("Invalid scrollbar-fade-delay value: \"{}\"", v),
1170}}
1171
1172#[derive(Debug, Clone, PartialEq, Eq)]
1173#[repr(C, u8)]
1174pub enum ScrollbarFadeDelayParseErrorOwned {
1175 InvalidValue(AzString),
1176}
1177impl ScrollbarFadeDelayParseError<'_> {
1178 #[must_use]
1179 pub fn to_contained(&self) -> ScrollbarFadeDelayParseErrorOwned {
1180 match self {
1181 Self::InvalidValue(s) => {
1182 ScrollbarFadeDelayParseErrorOwned::InvalidValue((*s).to_string().into())
1183 }
1184 }
1185 }
1186}
1187impl ScrollbarFadeDelayParseErrorOwned {
1188 #[must_use]
1189 pub fn to_shared(&self) -> ScrollbarFadeDelayParseError<'_> {
1190 match self {
1191 Self::InvalidValue(s) => ScrollbarFadeDelayParseError::InvalidValue(s.as_str()),
1192 }
1193 }
1194}
1195
1196#[cfg(feature = "parser")]
1202fn parse_time_ms(input: &str) -> Option<u32> {
1203 crate::props::basic::time::parse_duration(input)
1204 .ok()
1205 .map(|d| d.millis())
1206}
1207
1208#[cfg(feature = "parser")]
1209pub fn parse_scrollbar_fade_delay(
1213 input: &str,
1214) -> Result<ScrollbarFadeDelay, ScrollbarFadeDelayParseError<'_>> {
1215 parse_time_ms(input)
1216 .map(ScrollbarFadeDelay::new)
1217 .ok_or(ScrollbarFadeDelayParseError::InvalidValue(input))
1218}
1219
1220#[derive(Clone, PartialEq, Eq)]
1223pub enum ScrollbarFadeDurationParseError<'a> {
1224 InvalidValue(&'a str),
1225}
1226impl_debug_as_display!(ScrollbarFadeDurationParseError<'a>);
1227impl_display! { ScrollbarFadeDurationParseError<'a>, {
1228 InvalidValue(v) => format!("Invalid scrollbar-fade-duration value: \"{}\"", v),
1229}}
1230
1231#[derive(Debug, Clone, PartialEq, Eq)]
1232#[repr(C, u8)]
1233pub enum ScrollbarFadeDurationParseErrorOwned {
1234 InvalidValue(AzString),
1235}
1236impl ScrollbarFadeDurationParseError<'_> {
1237 #[must_use]
1238 pub fn to_contained(&self) -> ScrollbarFadeDurationParseErrorOwned {
1239 match self {
1240 Self::InvalidValue(s) => {
1241 ScrollbarFadeDurationParseErrorOwned::InvalidValue((*s).to_string().into())
1242 }
1243 }
1244 }
1245}
1246impl ScrollbarFadeDurationParseErrorOwned {
1247 #[must_use]
1248 pub fn to_shared(&self) -> ScrollbarFadeDurationParseError<'_> {
1249 match self {
1250 Self::InvalidValue(s) => ScrollbarFadeDurationParseError::InvalidValue(s.as_str()),
1251 }
1252 }
1253}
1254
1255#[cfg(feature = "parser")]
1256pub fn parse_scrollbar_fade_duration(
1260 input: &str,
1261) -> Result<ScrollbarFadeDuration, ScrollbarFadeDurationParseError<'_>> {
1262 parse_time_ms(input)
1263 .map(ScrollbarFadeDuration::new)
1264 .ok_or(ScrollbarFadeDurationParseError::InvalidValue(input))
1265}
1266
1267#[cfg(all(test, feature = "parser"))]
1268mod tests {
1269 use super::*;
1270 use crate::props::basic::color::ColorU;
1271
1272 #[test]
1273 fn test_parse_scrollbar_width() {
1274 assert_eq!(
1275 parse_layout_scrollbar_width("auto").unwrap(),
1276 LayoutScrollbarWidth::Auto
1277 );
1278 assert_eq!(
1279 parse_layout_scrollbar_width("thin").unwrap(),
1280 LayoutScrollbarWidth::Thin
1281 );
1282 assert_eq!(
1283 parse_layout_scrollbar_width("none").unwrap(),
1284 LayoutScrollbarWidth::None
1285 );
1286 assert!(parse_layout_scrollbar_width("thick").is_err());
1287 }
1288
1289 #[test]
1290 fn test_parse_scrollbar_color() {
1291 assert_eq!(
1292 parse_style_scrollbar_color("auto").unwrap(),
1293 StyleScrollbarColor::Auto
1294 );
1295
1296 let custom = parse_style_scrollbar_color("red blue").unwrap();
1297 assert_eq!(
1298 custom,
1299 StyleScrollbarColor::Custom(ScrollbarColorCustom {
1300 thumb: ColorU::RED,
1301 track: ColorU::BLUE
1302 })
1303 );
1304
1305 let custom_hex = parse_style_scrollbar_color("#ff0000 #0000ff").unwrap();
1306 assert_eq!(
1307 custom_hex,
1308 StyleScrollbarColor::Custom(ScrollbarColorCustom {
1309 thumb: ColorU::RED,
1310 track: ColorU::BLUE
1311 })
1312 );
1313
1314 assert!(parse_style_scrollbar_color("red").is_err());
1315 assert!(parse_style_scrollbar_color("red blue green").is_err());
1316 }
1317}
1318
1319#[cfg(test)]
1320#[allow(clippy::unreadable_literal, clippy::float_cmp)]
1321mod autotest_generated {
1322 use super::*;
1323 use crate::codegen::format::FormatAsRustCode;
1324
1325 #[cfg(feature = "parser")]
1329 const TWO_POW_24: u32 = 16_777_216;
1330
1331 #[cfg(feature = "parser")]
1333 const GARBAGE: &[&str] = &[
1334 "",
1335 " ",
1336 " ",
1337 "\t\n",
1338 "\u{a0}", "\0",
1340 "\u{1F600}", "e\u{0301}", "\u{202e}auto", "аuto", "AUTO",
1345 "auto;",
1346 "auto garbage",
1347 "-1",
1348 "NaN",
1349 "inf",
1350 "0x10",
1351 "9223372036854775807", "1e400",
1353 "{[(<",
1354 ];
1355
1356 fn assert_physics_invariants(p: ScrollPhysics, name: &str) {
1365 for (field, v) in [
1366 ("deceleration_rate", p.deceleration_rate),
1367 ("min_velocity_threshold", p.min_velocity_threshold),
1368 ("max_velocity", p.max_velocity),
1369 ("wheel_multiplier", p.wheel_multiplier),
1370 ("overscroll_elasticity", p.overscroll_elasticity),
1371 ("max_overscroll_distance", p.max_overscroll_distance),
1372 ] {
1373 assert!(v.is_finite(), "{name}.{field} is not finite: {v}");
1374 assert!(!v.is_nan(), "{name}.{field} is NaN");
1375 assert!(v >= 0.0, "{name}.{field} is negative: {v}");
1376 }
1377
1378 assert!(
1379 p.deceleration_rate > 0.0 && p.deceleration_rate < 1.0,
1380 "{name}.deceleration_rate must stay inside (0.0, 1.0) or momentum never stops: {}",
1381 p.deceleration_rate
1382 );
1383 assert!(
1384 (0.0..=1.0).contains(&p.overscroll_elasticity),
1385 "{name}.overscroll_elasticity out of [0.0, 1.0]: {}",
1386 p.overscroll_elasticity
1387 );
1388 assert!(
1389 p.max_velocity > p.min_velocity_threshold,
1390 "{name}: max_velocity ({}) must exceed min_velocity_threshold ({})",
1391 p.max_velocity,
1392 p.min_velocity_threshold
1393 );
1394 assert!(
1395 p.wheel_multiplier > 0.0,
1396 "{name}.wheel_multiplier must be > 0 or the wheel does nothing"
1397 );
1398 assert!(
1399 p.timer_interval_ms > 0,
1400 "{name}.timer_interval_ms == 0 would spin the physics timer"
1401 );
1402 assert!(
1403 p.smooth_scroll_duration_ms > 0,
1404 "{name}.smooth_scroll_duration_ms == 0 makes `scroll-behavior: smooth` a no-op"
1405 );
1406 }
1407
1408 #[test]
1409 fn scroll_physics_presets_hold_their_invariants() {
1410 assert_physics_invariants(ScrollPhysics::default(), "default");
1411 assert_physics_invariants(ScrollPhysics::ios(), "ios");
1412 assert_physics_invariants(ScrollPhysics::macos(), "macos");
1413 assert_physics_invariants(ScrollPhysics::windows(), "windows");
1414 assert_physics_invariants(ScrollPhysics::android(), "android");
1415 }
1416
1417 #[test]
1418 fn scroll_physics_presets_are_pure_and_distinct() {
1419 assert_eq!(ScrollPhysics::ios(), ScrollPhysics::ios());
1421 assert_eq!(ScrollPhysics::windows(), ScrollPhysics::windows());
1422
1423 assert_ne!(ScrollPhysics::ios(), ScrollPhysics::macos());
1425 assert_ne!(ScrollPhysics::ios(), ScrollPhysics::android());
1426 assert_ne!(ScrollPhysics::macos(), ScrollPhysics::windows());
1427 assert_ne!(ScrollPhysics::android(), ScrollPhysics::windows());
1428 assert_ne!(ScrollPhysics::default(), ScrollPhysics::ios());
1429 }
1430
1431 #[test]
1434 fn scroll_physics_presets_match_their_documented_platform_behavior() {
1435 let win = ScrollPhysics::windows();
1436 assert_eq!(win.overscroll_elasticity, 0.0);
1437 assert_eq!(win.max_overscroll_distance, 0.0);
1438 assert!(!win.invert_direction);
1439
1440 assert!(ScrollPhysics::ios().invert_direction);
1441 assert!(ScrollPhysics::macos().invert_direction);
1442 assert!(!ScrollPhysics::android().invert_direction);
1443
1444 assert!(
1446 ScrollPhysics::ios().deceleration_rate > ScrollPhysics::windows().deceleration_rate
1447 );
1448
1449 assert_eq!(ScrollPhysics::default().overscroll_elasticity, 0.0);
1451 }
1452
1453 #[test]
1458 fn fade_delay_and_duration_constructors_store_their_argument_verbatim() {
1459 for ms in [0u32, 1, 16, 500, u32::MAX / 2, u32::MAX - 1, u32::MAX] {
1460 assert_eq!(ScrollbarFadeDelay::new(ms).ms, ms);
1461 assert_eq!(ScrollbarFadeDuration::new(ms).ms, ms);
1462 }
1463 }
1464
1465 #[test]
1466 fn fade_zero_constants_agree_with_new_and_default() {
1467 assert_eq!(ScrollbarFadeDelay::ZERO, ScrollbarFadeDelay::new(0));
1468 assert_eq!(ScrollbarFadeDelay::ZERO, ScrollbarFadeDelay::default());
1469 assert_eq!(ScrollbarFadeDuration::ZERO, ScrollbarFadeDuration::new(0));
1470 assert_eq!(
1471 ScrollbarFadeDuration::ZERO,
1472 ScrollbarFadeDuration::default()
1473 );
1474 assert_eq!(ScrollbarFadeDelay::ZERO.ms, 0);
1475 assert_eq!(ScrollbarFadeDuration::ZERO.ms, 0);
1476 }
1477
1478 #[test]
1481 fn fade_delay_orders_by_millisecond_count() {
1482 assert!(ScrollbarFadeDelay::new(0) < ScrollbarFadeDelay::new(1));
1483 assert!(ScrollbarFadeDelay::new(499) < ScrollbarFadeDelay::new(500));
1484 assert!(ScrollbarFadeDelay::new(u32::MAX) > ScrollbarFadeDelay::new(u32::MAX - 1));
1485 assert!(ScrollbarFadeDuration::new(0) < ScrollbarFadeDuration::new(u32::MAX));
1486 }
1487
1488 #[test]
1493 fn enum_printers_emit_the_css_keywords() {
1494 assert_eq!(ScrollBehavior::Auto.print_as_css_value(), "auto");
1495 assert_eq!(ScrollBehavior::Smooth.print_as_css_value(), "smooth");
1496 assert_eq!(ScrollBehavior::default(), ScrollBehavior::Auto);
1497
1498 assert_eq!(OverscrollBehavior::Auto.print_as_css_value(), "auto");
1499 assert_eq!(OverscrollBehavior::Contain.print_as_css_value(), "contain");
1500 assert_eq!(OverscrollBehavior::None.print_as_css_value(), "none");
1501 assert_eq!(OverscrollBehavior::default(), OverscrollBehavior::Auto);
1502
1503 assert_eq!(OverflowScrolling::Auto.print_as_css_value(), "auto");
1504 assert_eq!(OverflowScrolling::Touch.print_as_css_value(), "touch");
1505 assert_eq!(OverflowScrolling::default(), OverflowScrolling::Auto);
1506
1507 assert_eq!(LayoutScrollbarWidth::Auto.print_as_css_value(), "auto");
1508 assert_eq!(LayoutScrollbarWidth::Thin.print_as_css_value(), "thin");
1509 assert_eq!(LayoutScrollbarWidth::None.print_as_css_value(), "none");
1510 assert_eq!(LayoutScrollbarWidth::default(), LayoutScrollbarWidth::Auto);
1511
1512 assert_eq!(
1513 ScrollbarVisibilityMode::Always.print_as_css_value(),
1514 "always"
1515 );
1516 assert_eq!(
1517 ScrollbarVisibilityMode::WhenScrolling.print_as_css_value(),
1518 "when-scrolling"
1519 );
1520 assert_eq!(ScrollbarVisibilityMode::Auto.print_as_css_value(), "auto");
1521 assert_eq!(
1522 ScrollbarVisibilityMode::default(),
1523 ScrollbarVisibilityMode::Always
1524 );
1525 }
1526
1527 #[test]
1531 fn fade_printers_special_case_zero_and_keep_the_unit_otherwise() {
1532 assert_eq!(ScrollbarFadeDelay::new(0).print_as_css_value(), "0");
1533 assert_eq!(ScrollbarFadeDelay::new(1).print_as_css_value(), "1ms");
1534 assert_eq!(ScrollbarFadeDelay::new(500).print_as_css_value(), "500ms");
1535 assert_eq!(
1536 ScrollbarFadeDelay::new(u32::MAX).print_as_css_value(),
1537 "4294967295ms"
1538 );
1539 assert_eq!(ScrollbarFadeDuration::new(0).print_as_css_value(), "0");
1540 assert_eq!(
1541 ScrollbarFadeDuration::new(200).print_as_css_value(),
1542 "200ms"
1543 );
1544 assert_eq!(
1545 ScrollbarFadeDuration::new(u32::MAX).print_as_css_value(),
1546 "4294967295ms"
1547 );
1548 }
1549
1550 #[test]
1551 fn scrollbar_color_printer_emits_two_eight_digit_hashes() {
1552 assert_eq!(StyleScrollbarColor::Auto.print_as_css_value(), "auto");
1553 assert_eq!(StyleScrollbarColor::default(), StyleScrollbarColor::Auto);
1554
1555 let custom = StyleScrollbarColor::Custom(ScrollbarColorCustom {
1556 thumb: ColorU::RED,
1557 track: ColorU::TRANSPARENT,
1558 });
1559 assert_eq!(custom.print_as_css_value(), "#ff0000ff #00000000");
1560 }
1561
1562 #[test]
1565 fn aggregate_printers_do_not_panic_and_mention_both_axes() {
1566 let printed = ScrollbarStyle::default().print_as_css_value();
1567 assert!(printed.contains("horz("), "{printed}");
1568 assert!(printed.contains("vert("), "{printed}");
1569
1570 let info = ScrollbarInfo::default().print_as_css_value();
1571 assert!(info.contains("width:"), "{info}");
1572 assert!(info.contains("thumb:"), "{info}");
1573 assert!(info.contains("resizer:"), "{info}");
1574 }
1575
1576 #[test]
1581 fn format_as_rust_code_emits_constructible_expressions() {
1582 assert_eq!(
1583 LayoutScrollbarWidth::Thin.format_as_rust_code(0),
1584 "LayoutScrollbarWidth::Thin"
1585 );
1586 assert_eq!(
1587 LayoutScrollbarWidth::None.format_as_rust_code(7),
1588 "LayoutScrollbarWidth::None",
1589 "indent depth must not leak into a unit-variant literal"
1590 );
1591 assert_eq!(
1592 ScrollbarVisibilityMode::WhenScrolling.format_as_rust_code(0),
1593 "ScrollbarVisibilityMode::WhenScrolling"
1594 );
1595 assert_eq!(
1596 StyleScrollbarColor::Auto.format_as_rust_code(0),
1597 "StyleScrollbarColor::Auto"
1598 );
1599
1600 assert_eq!(
1602 ScrollbarFadeDelay::new(0).format_as_rust_code(0),
1603 "ScrollbarFadeDelay::new(0)"
1604 );
1605 assert_eq!(
1606 ScrollbarFadeDelay::new(u32::MAX).format_as_rust_code(3),
1607 "ScrollbarFadeDelay::new(4294967295)"
1608 );
1609 assert_eq!(
1610 ScrollbarFadeDuration::new(u32::MAX).format_as_rust_code(0),
1611 "ScrollbarFadeDuration::new(4294967295)"
1612 );
1613 }
1614
1615 #[test]
1616 fn format_as_rust_code_of_aggregates_does_not_panic() {
1617 let custom = StyleScrollbarColor::Custom(ScrollbarColorCustom {
1618 thumb: ColorU::TRANSPARENT,
1619 track: ColorU::WHITE,
1620 })
1621 .format_as_rust_code(0);
1622 assert!(
1623 custom.starts_with("StyleScrollbarColor::Custom(ScrollbarColorCustom {"),
1624 "{custom}"
1625 );
1626 assert!(
1627 custom.contains("thumb:") && custom.contains("track:"),
1628 "{custom}"
1629 );
1630
1631 for tabs in [0usize, 1, 4] {
1632 let code = ScrollbarStyle::default().format_as_rust_code(tabs);
1633 assert!(code.starts_with("ScrollbarStyle {"), "{code}");
1634 assert!(code.contains("horizontal:"), "{code}");
1635 assert!(code.contains("vertical:"), "{code}");
1636 }
1637 }
1638
1639 #[test]
1644 fn scrollbar_info_default_is_the_classic_light_constant() {
1645 assert_eq!(ScrollbarInfo::default(), SCROLLBAR_CLASSIC_LIGHT);
1646
1647 let style = ScrollbarStyle::default();
1648 assert_eq!(style.horizontal, SCROLLBAR_CLASSIC_LIGHT);
1649 assert_eq!(style.vertical, SCROLLBAR_CLASSIC_LIGHT);
1650 }
1651
1652 #[test]
1656 fn computed_default_mirrors_the_default_scrollbar_info() {
1657 let computed = ComputedScrollbarStyle::default();
1658 let info = ScrollbarInfo::default();
1659
1660 assert_eq!(computed.width, Some(info.width));
1661 assert_eq!(
1662 computed.thumb_color,
1663 Some(ColorU {
1664 r: 193,
1665 g: 193,
1666 b: 193,
1667 a: 255
1668 })
1669 );
1670 assert_eq!(
1671 computed.track_color,
1672 Some(ColorU {
1673 r: 241,
1674 g: 241,
1675 b: 241,
1676 a: 255
1677 })
1678 );
1679 assert!(
1680 computed.thumb_color.is_some() && computed.track_color.is_some(),
1681 "the classic-light default must resolve to solid colors, not None"
1682 );
1683 }
1684
1685 #[test]
1689 fn preset_constants_agree_on_the_overlay_clipping_flag() {
1690 for info in [SCROLLBAR_CLASSIC_LIGHT, SCROLLBAR_CLASSIC_DARK] {
1691 assert!(!info.clip_to_container_border);
1692 assert_eq!(info.scroll_behavior, ScrollBehavior::Auto);
1693 }
1694 for info in [
1695 SCROLLBAR_MACOS_LIGHT,
1696 SCROLLBAR_MACOS_DARK,
1697 SCROLLBAR_IOS_LIGHT,
1698 SCROLLBAR_IOS_DARK,
1699 SCROLLBAR_ANDROID_LIGHT,
1700 SCROLLBAR_ANDROID_DARK,
1701 ] {
1702 assert!(info.clip_to_container_border);
1703 assert_eq!(info.scroll_behavior, ScrollBehavior::Smooth);
1704 }
1705 for info in [SCROLLBAR_WINDOWS_LIGHT, SCROLLBAR_WINDOWS_DARK] {
1706 assert!(!info.clip_to_container_border);
1707 assert_eq!(info.overscroll_behavior_x, OverscrollBehavior::None);
1708 assert_eq!(info.overscroll_behavior_y, OverscrollBehavior::None);
1709 }
1710 }
1711
1712 #[test]
1715 fn light_and_dark_presets_share_their_geometry() {
1716 for (light, dark) in [
1717 (SCROLLBAR_CLASSIC_LIGHT, SCROLLBAR_CLASSIC_DARK),
1718 (SCROLLBAR_MACOS_LIGHT, SCROLLBAR_MACOS_DARK),
1719 (SCROLLBAR_WINDOWS_LIGHT, SCROLLBAR_WINDOWS_DARK),
1720 (SCROLLBAR_IOS_LIGHT, SCROLLBAR_IOS_DARK),
1721 (SCROLLBAR_ANDROID_LIGHT, SCROLLBAR_ANDROID_DARK),
1722 ] {
1723 assert_eq!(light.width, dark.width);
1724 assert_eq!(light.padding_left, dark.padding_left);
1725 assert_eq!(light.padding_right, dark.padding_right);
1726 assert_eq!(
1727 light.clip_to_container_border,
1728 dark.clip_to_container_border
1729 );
1730 assert_ne!(light.thumb, dark.thumb, "light/dark thumbs must differ");
1731 }
1732 }
1733
1734 #[cfg(feature = "parser")]
1739 #[test]
1740 fn scrollbar_width_parses_the_three_legal_keywords() {
1741 assert_eq!(
1742 parse_layout_scrollbar_width("auto"),
1743 Ok(LayoutScrollbarWidth::Auto)
1744 );
1745 assert_eq!(
1746 parse_layout_scrollbar_width("thin"),
1747 Ok(LayoutScrollbarWidth::Thin)
1748 );
1749 assert_eq!(
1750 parse_layout_scrollbar_width("none"),
1751 Ok(LayoutScrollbarWidth::None)
1752 );
1753 }
1754
1755 #[cfg(feature = "parser")]
1756 #[test]
1757 fn scrollbar_width_trims_surrounding_whitespace_but_rejects_inner_junk() {
1758 assert_eq!(
1759 parse_layout_scrollbar_width(" \t thin \n "),
1760 Ok(LayoutScrollbarWidth::Thin)
1761 );
1762 assert!(parse_layout_scrollbar_width("thin;").is_err());
1763 assert!(parse_layout_scrollbar_width("thin thin").is_err());
1764 assert!(parse_layout_scrollbar_width("th in").is_err());
1765 }
1766
1767 #[cfg(feature = "parser")]
1771 #[test]
1772 fn scrollbar_width_keyword_matching_is_case_sensitive() {
1773 assert!(parse_layout_scrollbar_width("AUTO").is_err());
1774 assert!(parse_layout_scrollbar_width("Thin").is_err());
1775 assert!(parse_layout_scrollbar_width("NONE").is_err());
1776 }
1777
1778 #[cfg(feature = "parser")]
1779 #[test]
1780 fn scrollbar_width_rejects_every_garbage_input_without_panicking() {
1781 for input in GARBAGE {
1782 assert!(
1783 parse_layout_scrollbar_width(input).is_err(),
1784 "expected {input:?} to be rejected"
1785 );
1786 }
1787 }
1788
1789 #[cfg(feature = "parser")]
1792 #[test]
1793 fn scrollbar_width_error_keeps_the_raw_untrimmed_input() {
1794 let raw = " thick ";
1795 assert_eq!(
1796 parse_layout_scrollbar_width(raw),
1797 Err(LayoutScrollbarWidthParseError::InvalidValue(raw))
1798 );
1799 let msg = format!("{}", parse_layout_scrollbar_width(raw).unwrap_err());
1800 assert!(msg.contains(raw), "{msg}");
1801 }
1802
1803 #[cfg(feature = "parser")]
1804 #[test]
1805 fn scrollbar_width_survives_a_megabyte_of_input_and_deep_nesting() {
1806 let huge = "a".repeat(1_000_000);
1807 assert!(parse_layout_scrollbar_width(&huge).is_err());
1808
1809 let repeated_token = "auto".repeat(250_000);
1810 assert!(parse_layout_scrollbar_width(&repeated_token).is_err());
1811
1812 let nested = "(".repeat(10_000);
1813 assert!(parse_layout_scrollbar_width(&nested).is_err());
1814 }
1815
1816 #[cfg(feature = "parser")]
1817 #[test]
1818 fn scrollbar_width_round_trips_through_its_printer() {
1819 for value in [
1820 LayoutScrollbarWidth::Auto,
1821 LayoutScrollbarWidth::Thin,
1822 LayoutScrollbarWidth::None,
1823 ] {
1824 let encoded = value.print_as_css_value();
1825 assert_eq!(
1826 parse_layout_scrollbar_width(&encoded),
1827 Ok(value),
1828 "{encoded} did not decode back to {value:?}"
1829 );
1830 }
1831 }
1832
1833 #[cfg(feature = "parser")]
1838 #[test]
1839 fn scrollbar_color_needs_exactly_two_colors_or_the_auto_keyword() {
1840 assert_eq!(
1841 parse_style_scrollbar_color("auto"),
1842 Ok(StyleScrollbarColor::Auto)
1843 );
1844 assert_eq!(
1845 parse_style_scrollbar_color(" auto "),
1846 Ok(StyleScrollbarColor::Auto)
1847 );
1848 assert_eq!(
1849 parse_style_scrollbar_color("red blue"),
1850 Ok(StyleScrollbarColor::Custom(ScrollbarColorCustom {
1851 thumb: ColorU::RED,
1852 track: ColorU::BLUE,
1853 }))
1854 );
1855
1856 for input in ["red", "#fff", "red blue green", "a b c d"] {
1858 assert!(
1859 matches!(
1860 parse_style_scrollbar_color(input),
1861 Err(StyleScrollbarColorParseError::InvalidValue(_))
1862 ),
1863 "expected {input:?} to be an InvalidValue error"
1864 );
1865 }
1866 }
1867
1868 #[cfg(feature = "parser")]
1871 #[test]
1872 fn scrollbar_color_accepts_any_whitespace_run_as_the_separator() {
1873 let expected = StyleScrollbarColor::Custom(ScrollbarColorCustom {
1874 thumb: ColorU::RED,
1875 track: ColorU::BLUE,
1876 });
1877 assert_eq!(parse_style_scrollbar_color("red\tblue"), Ok(expected));
1878 assert_eq!(parse_style_scrollbar_color("red\n blue"), Ok(expected));
1879 assert_eq!(
1880 parse_style_scrollbar_color(" red blue "),
1881 Ok(expected)
1882 );
1883 }
1884
1885 #[cfg(feature = "parser")]
1888 #[test]
1889 fn scrollbar_color_names_are_case_insensitive_but_the_auto_keyword_is_not() {
1890 assert_eq!(
1891 parse_style_scrollbar_color("RED BLUE"),
1892 Ok(StyleScrollbarColor::Custom(ScrollbarColorCustom {
1893 thumb: ColorU::RED,
1894 track: ColorU::BLUE,
1895 }))
1896 );
1897 assert!(matches!(
1899 parse_style_scrollbar_color("AUTO"),
1900 Err(StyleScrollbarColorParseError::InvalidValue(_))
1901 ));
1902 assert!(matches!(
1904 parse_style_scrollbar_color("auto auto"),
1905 Err(StyleScrollbarColorParseError::Color(_))
1906 ));
1907 }
1908
1909 #[cfg(feature = "parser")]
1914 #[test]
1915 fn scrollbar_color_rejects_functional_colors_containing_spaces() {
1916 assert_eq!(
1917 parse_style_scrollbar_color("rgb(255,0,0) blue"),
1918 Ok(StyleScrollbarColor::Custom(ScrollbarColorCustom {
1919 thumb: ColorU::RED,
1920 track: ColorU::BLUE,
1921 }))
1922 );
1923 assert!(matches!(
1924 parse_style_scrollbar_color("rgb(255, 0, 0) blue"),
1925 Err(StyleScrollbarColorParseError::InvalidValue(_))
1926 ));
1927 }
1928
1929 #[cfg(feature = "parser")]
1930 #[test]
1931 fn scrollbar_color_reports_which_component_failed() {
1932 assert!(matches!(
1934 parse_style_scrollbar_color("notacolor blue"),
1935 Err(StyleScrollbarColorParseError::Color(_))
1936 ));
1937 assert!(matches!(
1938 parse_style_scrollbar_color("red notacolor"),
1939 Err(StyleScrollbarColorParseError::Color(_))
1940 ));
1941 assert!(matches!(
1942 parse_style_scrollbar_color("#gggggg #000000"),
1943 Err(StyleScrollbarColorParseError::Color(_))
1944 ));
1945 }
1946
1947 #[cfg(feature = "parser")]
1948 #[test]
1949 fn scrollbar_color_rejects_garbage_without_panicking() {
1950 for input in GARBAGE {
1951 assert!(
1952 parse_style_scrollbar_color(input).is_err(),
1953 "expected {input:?} to be rejected"
1954 );
1955 }
1956 for input in [
1958 "0 0",
1959 "-0 -0",
1960 "NaN NaN",
1961 "inf inf",
1962 "9223372036854775807 1",
1963 "1e400 1e400",
1964 "-1 -1",
1965 ] {
1966 assert!(
1967 parse_style_scrollbar_color(input).is_err(),
1968 "expected {input:?} to be rejected"
1969 );
1970 }
1971 }
1972
1973 #[cfg(feature = "parser")]
1974 #[test]
1975 fn scrollbar_color_survives_huge_and_deeply_nested_input() {
1976 let huge = "z".repeat(500_000);
1977 let two_huge = format!("{huge} {huge}");
1978 assert!(parse_style_scrollbar_color(&two_huge).is_err());
1979
1980 let nested = "(".repeat(10_000);
1981 assert!(parse_style_scrollbar_color(&format!("{nested} {nested}")).is_err());
1982
1983 let many = "red ".repeat(100_000);
1985 assert!(matches!(
1986 parse_style_scrollbar_color(&many),
1987 Err(StyleScrollbarColorParseError::InvalidValue(_))
1988 ));
1989 }
1990
1991 #[cfg(feature = "parser")]
1995 #[test]
1996 fn scrollbar_color_error_carries_the_trimmed_input() {
1997 assert_eq!(
1998 parse_style_scrollbar_color(" red "),
1999 Err(StyleScrollbarColorParseError::InvalidValue("red"))
2000 );
2001 }
2002
2003 #[cfg(feature = "parser")]
2004 #[test]
2005 fn scrollbar_color_round_trips_through_its_printer() {
2006 let samples = [
2007 StyleScrollbarColor::Auto,
2008 StyleScrollbarColor::Custom(ScrollbarColorCustom {
2009 thumb: ColorU::RED,
2010 track: ColorU::BLUE,
2011 }),
2012 StyleScrollbarColor::Custom(ScrollbarColorCustom {
2013 thumb: ColorU::TRANSPARENT,
2014 track: ColorU::TRANSPARENT,
2015 }),
2016 StyleScrollbarColor::Custom(ScrollbarColorCustom {
2017 thumb: ColorU {
2018 r: 0,
2019 g: 0,
2020 b: 0,
2021 a: 100,
2022 },
2023 track: ColorU {
2024 r: 1,
2025 g: 2,
2026 b: 3,
2027 a: 4,
2028 },
2029 }),
2030 StyleScrollbarColor::Custom(ScrollbarColorCustom {
2031 thumb: ColorU::WHITE,
2032 track: ColorU::BLACK,
2033 }),
2034 ];
2035 for value in samples {
2036 let encoded = value.print_as_css_value();
2037 assert_eq!(
2038 parse_style_scrollbar_color(&encoded),
2039 Ok(value),
2040 "{encoded} did not decode back to {value:?}"
2041 );
2042 }
2043 }
2044
2045 #[cfg(feature = "parser")]
2050 #[test]
2051 fn visibility_mode_parses_its_three_keywords_and_trims() {
2052 assert_eq!(
2053 parse_scrollbar_visibility_mode("always"),
2054 Ok(ScrollbarVisibilityMode::Always)
2055 );
2056 assert_eq!(
2057 parse_scrollbar_visibility_mode(" when-scrolling\t"),
2058 Ok(ScrollbarVisibilityMode::WhenScrolling)
2059 );
2060 assert_eq!(
2061 parse_scrollbar_visibility_mode("auto"),
2062 Ok(ScrollbarVisibilityMode::Auto)
2063 );
2064 }
2065
2066 #[cfg(feature = "parser")]
2067 #[test]
2068 fn visibility_mode_rejects_near_misses_and_garbage() {
2069 for input in [
2070 "when scrolling", "whenscrolling",
2072 "when-scrolling-",
2073 "-when-scrolling",
2074 "ALWAYS",
2075 "always;",
2076 "always auto",
2077 ] {
2078 assert!(
2079 parse_scrollbar_visibility_mode(input).is_err(),
2080 "expected {input:?} to be rejected"
2081 );
2082 }
2083 for input in GARBAGE {
2084 assert!(
2085 parse_scrollbar_visibility_mode(input).is_err(),
2086 "expected {input:?} to be rejected"
2087 );
2088 }
2089 }
2090
2091 #[cfg(feature = "parser")]
2092 #[test]
2093 fn visibility_mode_survives_huge_and_nested_input() {
2094 assert!(parse_scrollbar_visibility_mode(&"a".repeat(1_000_000)).is_err());
2095 assert!(parse_scrollbar_visibility_mode(&"always".repeat(200_000)).is_err());
2096 assert!(parse_scrollbar_visibility_mode(&"[".repeat(10_000)).is_err());
2097 }
2098
2099 #[cfg(feature = "parser")]
2100 #[test]
2101 fn visibility_mode_round_trips_through_its_printer() {
2102 for value in [
2103 ScrollbarVisibilityMode::Always,
2104 ScrollbarVisibilityMode::WhenScrolling,
2105 ScrollbarVisibilityMode::Auto,
2106 ] {
2107 let encoded = value.print_as_css_value();
2108 assert_eq!(
2109 parse_scrollbar_visibility_mode(&encoded),
2110 Ok(value),
2111 "{encoded} did not decode back to {value:?}"
2112 );
2113 }
2114 }
2115
2116 #[cfg(feature = "parser")]
2121 #[test]
2122 fn parse_time_ms_accepts_bare_zero_and_both_units() {
2123 assert_eq!(parse_time_ms("0"), Some(0));
2124 assert_eq!(parse_time_ms("0ms"), Some(0));
2125 assert_eq!(parse_time_ms("0s"), Some(0));
2126 assert_eq!(parse_time_ms("500ms"), Some(500));
2127 assert_eq!(parse_time_ms("1s"), Some(1000));
2128 assert_eq!(parse_time_ms("1.5s"), Some(1500));
2129 assert_eq!(parse_time_ms(" 200ms "), Some(200));
2130 assert_eq!(
2131 parse_time_ms("200MS"),
2132 Some(200),
2133 "units are case-insensitive"
2134 );
2135 }
2136
2137 #[cfg(feature = "parser")]
2142 #[test]
2143 fn parse_time_ms_converts_the_tick_unit_to_milliseconds() {
2144 assert_eq!(parse_time_ms("60t"), Some(1000));
2145 assert_eq!(parse_time_ms("30t"), Some(500));
2146 assert_eq!(parse_time_ms("1t"), Some(16));
2147 assert_eq!(parse_time_ms("0t"), Some(0));
2148 assert_ne!(parse_time_ms("60t"), Some(60), "ticks passed through as ms");
2149 }
2150
2151 #[cfg(feature = "parser")]
2154 #[test]
2155 fn parse_time_ms_requires_an_attached_unit() {
2156 assert_eq!(parse_time_ms("500"), None);
2157 assert_eq!(parse_time_ms("1 s"), None);
2158 assert_eq!(parse_time_ms("500 ms"), None);
2159 assert_eq!(parse_time_ms("ms"), None);
2160 assert_eq!(parse_time_ms("s"), None);
2161 assert_eq!(parse_time_ms("500px"), None);
2162 assert_eq!(parse_time_ms("500msms"), None);
2163 }
2164
2165 #[cfg(feature = "parser")]
2166 #[test]
2167 fn parse_time_ms_rejects_empty_blank_unicode_and_garbage() {
2168 for input in [
2169 "",
2170 " ",
2171 " ",
2172 "\t\n",
2173 "\u{1F600}",
2174 "e\u{0301}",
2175 "٥ms",
2176 "500ms",
2177 ] {
2178 assert_eq!(
2179 parse_time_ms(input),
2180 None,
2181 "expected {input:?} to be rejected"
2182 );
2183 }
2184 }
2185
2186 #[cfg(feature = "parser")]
2187 #[test]
2188 fn parse_time_ms_rejects_negative_durations() {
2189 assert_eq!(parse_time_ms("-1ms"), None);
2190 assert_eq!(parse_time_ms("-0.5s"), None);
2191 assert_eq!(parse_time_ms("-inf ms"), None);
2192 }
2193
2194 #[cfg(feature = "parser")]
2197 #[test]
2198 fn parse_time_ms_accepts_negative_zero_as_zero() {
2199 assert_eq!(parse_time_ms("-0ms"), Some(0));
2200 assert_eq!(parse_time_ms("-0.0s"), Some(0));
2201 }
2202
2203 #[cfg(feature = "parser")]
2208 #[test]
2209 fn parse_time_ms_saturates_on_non_finite_and_huge_values() {
2210 assert_eq!(parse_time_ms("infms"), Some(u32::MAX));
2211 assert_eq!(parse_time_ms("infinityms"), Some(u32::MAX));
2212 assert_eq!(parse_time_ms("infs"), Some(u32::MAX));
2213 assert_eq!(parse_time_ms("nanms"), Some(0));
2214 assert_eq!(parse_time_ms("NaNms"), Some(0));
2215
2216 assert_eq!(parse_time_ms("1e30ms"), Some(u32::MAX));
2217 assert_eq!(
2218 parse_time_ms("1e400ms"),
2219 Some(u32::MAX),
2220 "overflows f32 to inf"
2221 );
2222 assert_eq!(parse_time_ms("4294967296ms"), Some(u32::MAX), "2^32 clamps");
2223 assert_eq!(parse_time_ms("1e-30ms"), Some(0), "underflows to zero");
2224
2225 let long_number = format!("{}ms", "9".repeat(100_000));
2227 assert_eq!(parse_time_ms(&long_number), Some(u32::MAX));
2228 }
2229
2230 #[cfg(feature = "parser")]
2233 #[test]
2234 fn parse_time_ms_saturates_when_seconds_overflow_milliseconds() {
2235 assert_eq!(parse_time_ms("1000s"), Some(1_000_000));
2237 assert_eq!(parse_time_ms("16777s"), Some(16_777_000));
2238
2239 assert_eq!(parse_time_ms("4294968s"), Some(u32::MAX));
2241 assert_eq!(parse_time_ms("5000000s"), Some(u32::MAX));
2242
2243 let ms = parse_time_ms("4294967s").expect("4294967s must parse");
2246 assert!(
2247 ms.abs_diff(4_294_967_000) <= 512,
2248 "4294967s decoded to {ms}, which is nowhere near 4294967000ms"
2249 );
2250
2251 assert_eq!(
2252 parse_time_ms("0.0005s"),
2253 Some(0),
2254 "sub-ms truncates toward zero"
2255 );
2256 }
2257
2258 #[cfg(feature = "parser")]
2263 #[test]
2264 fn fade_parsers_accept_the_documented_syntax() {
2265 assert_eq!(
2266 parse_scrollbar_fade_delay("500ms"),
2267 Ok(ScrollbarFadeDelay::new(500))
2268 );
2269 assert_eq!(
2270 parse_scrollbar_fade_delay("0"),
2271 Ok(ScrollbarFadeDelay::ZERO)
2272 );
2273 assert_eq!(
2274 parse_scrollbar_fade_delay(" 1s "),
2275 Ok(ScrollbarFadeDelay::new(1000))
2276 );
2277 assert_eq!(
2278 parse_scrollbar_fade_duration("200ms"),
2279 Ok(ScrollbarFadeDuration::new(200))
2280 );
2281 assert_eq!(
2282 parse_scrollbar_fade_duration("0"),
2283 Ok(ScrollbarFadeDuration::ZERO)
2284 );
2285 }
2286
2287 #[cfg(feature = "parser")]
2288 #[test]
2289 fn fade_parsers_reject_garbage_and_keep_the_raw_input_in_the_error() {
2290 for input in GARBAGE {
2291 assert!(
2292 parse_scrollbar_fade_delay(input).is_err(),
2293 "delay: expected {input:?} to be rejected"
2294 );
2295 assert!(
2296 parse_scrollbar_fade_duration(input).is_err(),
2297 "duration: expected {input:?} to be rejected"
2298 );
2299 }
2300
2301 let raw = " bogus ";
2302 assert_eq!(
2303 parse_scrollbar_fade_delay(raw),
2304 Err(ScrollbarFadeDelayParseError::InvalidValue(raw))
2305 );
2306 assert_eq!(
2307 parse_scrollbar_fade_duration(raw),
2308 Err(ScrollbarFadeDurationParseError::InvalidValue(raw))
2309 );
2310 }
2311
2312 #[cfg(feature = "parser")]
2313 #[test]
2314 fn fade_parsers_reject_negative_delays() {
2315 assert!(parse_scrollbar_fade_delay("-1ms").is_err());
2316 assert!(parse_scrollbar_fade_delay("-500ms").is_err());
2317 assert!(parse_scrollbar_fade_duration("-0.5s").is_err());
2318 }
2319
2320 #[cfg(feature = "parser")]
2321 #[test]
2322 fn fade_parsers_saturate_instead_of_overflowing() {
2323 assert_eq!(
2324 parse_scrollbar_fade_delay("1e30ms"),
2325 Ok(ScrollbarFadeDelay::new(u32::MAX))
2326 );
2327 assert_eq!(
2328 parse_scrollbar_fade_duration("99999999999999s"),
2329 Ok(ScrollbarFadeDuration::new(u32::MAX))
2330 );
2331 }
2332
2333 #[cfg(feature = "parser")]
2334 #[test]
2335 fn fade_parsers_survive_huge_and_nested_input() {
2336 assert!(parse_scrollbar_fade_delay(&"a".repeat(1_000_000)).is_err());
2337 assert!(parse_scrollbar_fade_duration(&"0ms".repeat(300_000)).is_err());
2338 assert!(parse_scrollbar_fade_delay(&"(".repeat(10_000)).is_err());
2339 assert!(parse_scrollbar_fade_duration(&"[".repeat(10_000)).is_err());
2340 }
2341
2342 #[cfg(feature = "parser")]
2347 #[test]
2348 fn fade_delay_round_trips_exactly_up_to_two_pow_24() {
2349 for ms in [
2350 0u32,
2351 1,
2352 8,
2353 16,
2354 200,
2355 500,
2356 65_535,
2357 1_000_000,
2358 TWO_POW_24 - 1,
2359 TWO_POW_24,
2360 u32::MAX,
2361 ] {
2362 let value = ScrollbarFadeDelay::new(ms);
2363 let encoded = value.print_as_css_value();
2364 assert_eq!(
2365 parse_scrollbar_fade_delay(&encoded),
2366 Ok(value),
2367 "{ms}ms encoded as {encoded:?} did not decode back"
2368 );
2369
2370 let value = ScrollbarFadeDuration::new(ms);
2371 let encoded = value.print_as_css_value();
2372 assert_eq!(
2373 parse_scrollbar_fade_duration(&encoded),
2374 Ok(value),
2375 "{ms}ms encoded as {encoded:?} did not decode back"
2376 );
2377 }
2378 }
2379
2380 #[cfg(feature = "parser")]
2384 #[test]
2385 fn fade_delay_round_trip_is_lossy_above_two_pow_24() {
2386 let value = ScrollbarFadeDelay::new(TWO_POW_24 + 1);
2387 let decoded = parse_scrollbar_fade_delay(&value.print_as_css_value()).unwrap();
2388 assert_ne!(decoded, value, "expected precision loss above 2^24");
2389 assert_eq!(decoded.ms, TWO_POW_24, "must snap down to the nearest f32");
2390 }
2391
2392 fn error_payloads() -> [String; 6] {
2399 [
2400 String::new(),
2401 String::from(" "),
2402 String::from("thick"),
2403 String::from("\u{1F600}\u{0301}"),
2404 String::from("nul\0inside"),
2405 "x".repeat(100_000),
2406 ]
2407 }
2408
2409 #[test]
2410 fn layout_scrollbar_width_error_round_trips_through_owned_and_back() {
2411 for payload in error_payloads() {
2412 let shared = LayoutScrollbarWidthParseError::InvalidValue(&payload);
2413 let owned = shared.to_contained();
2414 assert_eq!(
2415 owned,
2416 LayoutScrollbarWidthParseErrorOwned::InvalidValue(payload.clone().into())
2417 );
2418 assert_eq!(
2419 owned.to_shared(),
2420 shared,
2421 "owned -> shared lost information"
2422 );
2423 assert_eq!(
2424 owned.to_shared().to_contained(),
2425 owned,
2426 "conversion is not idempotent"
2427 );
2428 }
2429 }
2430
2431 #[test]
2432 fn visibility_mode_error_round_trips_through_owned_and_back() {
2433 for payload in error_payloads() {
2434 let shared = ScrollbarVisibilityModeParseError::InvalidValue(&payload);
2435 let owned = shared.to_contained();
2436 assert_eq!(owned.to_shared(), shared);
2437 assert_eq!(owned.to_shared().to_contained(), owned);
2438 }
2439 }
2440
2441 #[test]
2442 fn fade_delay_and_duration_errors_round_trip_through_owned_and_back() {
2443 for payload in error_payloads() {
2444 let delay = ScrollbarFadeDelayParseError::InvalidValue(&payload);
2445 let owned_delay = delay.to_contained();
2446 assert_eq!(owned_delay.to_shared(), delay);
2447 assert_eq!(owned_delay.to_shared().to_contained(), owned_delay);
2448
2449 let duration = ScrollbarFadeDurationParseError::InvalidValue(&payload);
2450 let owned_duration = duration.to_contained();
2451 assert_eq!(owned_duration.to_shared(), duration);
2452 assert_eq!(owned_duration.to_shared().to_contained(), owned_duration);
2453 }
2454 }
2455
2456 #[test]
2457 fn scrollbar_color_invalid_value_error_round_trips_through_owned_and_back() {
2458 for payload in error_payloads() {
2459 let shared = StyleScrollbarColorParseError::InvalidValue(&payload);
2460 let owned = shared.to_contained();
2461 assert_eq!(
2462 owned,
2463 StyleScrollbarColorParseErrorOwned::InvalidValue(payload.clone().into())
2464 );
2465 assert_eq!(owned.to_shared(), shared);
2466 assert_eq!(owned.to_shared().to_contained(), owned);
2467 }
2468 }
2469
2470 #[cfg(feature = "parser")]
2473 #[test]
2474 fn scrollbar_color_nested_color_error_round_trips_through_owned_and_back() {
2475 let shared = parse_style_scrollbar_color("notacolor blue").unwrap_err();
2476 assert!(matches!(shared, StyleScrollbarColorParseError::Color(_)));
2477
2478 let owned = shared.to_contained();
2479 assert!(matches!(
2480 owned,
2481 StyleScrollbarColorParseErrorOwned::Color(_)
2482 ));
2483 assert_eq!(
2484 owned.to_shared(),
2485 shared,
2486 "nested color error lost information"
2487 );
2488 assert_eq!(owned.to_shared().to_contained(), owned);
2489 }
2490
2491 #[cfg(feature = "parser")]
2494 #[test]
2495 fn error_display_mentions_the_offending_input() {
2496 let width = parse_layout_scrollbar_width("thick").unwrap_err();
2497 assert!(format!("{width}").contains("thick"), "{width}");
2498 assert!(format!("{width:?}").contains("thick"), "{width:?}");
2499
2500 let color = parse_style_scrollbar_color("red").unwrap_err();
2501 assert!(format!("{color}").contains("red"), "{color}");
2502
2503 let vis = parse_scrollbar_visibility_mode("sometimes").unwrap_err();
2504 assert!(format!("{vis}").contains("sometimes"), "{vis}");
2505
2506 let delay = parse_scrollbar_fade_delay("soon").unwrap_err();
2507 assert!(format!("{delay}").contains("soon"), "{delay}");
2508
2509 let duration = parse_scrollbar_fade_duration("briefly").unwrap_err();
2510 assert!(format!("{duration}").contains("briefly"), "{duration}");
2511 }
2512
2513 #[test]
2516 fn error_display_does_not_panic_on_exotic_payloads() {
2517 for payload in error_payloads() {
2518 let err = LayoutScrollbarWidthParseError::InvalidValue(&payload);
2519 assert!(!format!("{err}").is_empty());
2520 let owned = err.to_contained();
2521 assert!(!format!("{}", owned.to_shared()).is_empty());
2522 }
2523 }
2524}