Skip to main content

azul_css/props/style/
scrollbar.rs

1//! CSS properties for styling scrollbars.
2
3use 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// ============================================================================
19// CSS Standard Scroll Behavior Properties
20// ============================================================================
21
22/// CSS `scroll-behavior` property - controls smooth scrolling
23/// <https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior>
24#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
25#[repr(C)]
26pub enum ScrollBehavior {
27    /// Scrolling jumps instantly to the final position
28    #[default]
29    Auto,
30    /// Scrolling animates smoothly to the final position
31    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/// CSS `overscroll-behavior` property - controls overscroll effects
44/// <https://developer.mozilla.org/en-US/docs/Web/CSS/overscroll-behavior>
45#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
46#[repr(C)]
47pub enum OverscrollBehavior {
48    /// Default scroll overflow behavior (bounce/glow effects, scroll chaining)
49    #[default]
50    Auto,
51    /// Prevents scroll chaining to parent elements, but allows local overscroll effects
52    Contain,
53    /// No scroll chaining and no overscroll effects (hard stop at boundaries)
54    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// ============================================================================
68// Extended Scroll Configuration (Azul-specific)
69// ============================================================================
70
71/// Scroll physics configuration for momentum scrolling
72///
73/// This controls how scrolling feels - the "weight" and "friction" of the scroll.
74/// Different platforms have different scroll physics (iOS vs Android vs Windows).
75#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
76#[repr(C)]
77pub struct ScrollPhysics {
78    /// Smooth scroll animation duration in milliseconds (default: 300ms)
79    /// Only used when scroll-behavior: smooth
80    pub smooth_scroll_duration_ms: u32,
81
82    /// Deceleration rate for momentum scrolling (0.0 = instant stop, 1.0 = never stops)
83    /// Typical values: 0.95 (fast deceleration) to 0.998 (slow, iOS-like)
84    /// Default: 0.95
85    pub deceleration_rate: f32,
86
87    /// Minimum velocity threshold to start momentum scrolling (pixels/second)
88    /// Below this, scrolling stops immediately. Default: 50.0
89    pub min_velocity_threshold: f32,
90
91    /// Maximum scroll velocity (pixels/second). Default: 8000.0
92    pub max_velocity: f32,
93
94    /// Scroll wheel multiplier. Default: 1.0
95    /// Values > 1.0 make scrolling faster, < 1.0 slower
96    pub wheel_multiplier: f32,
97
98    /// Whether to invert scroll direction (natural scrolling). Default: false
99    pub invert_direction: bool,
100
101    /// Overscroll elasticity (0.0 = no bounce, 1.0 = full bounce like iOS)
102    /// Only applies when overscroll-behavior: auto. Default: 0.0 (no bounce)
103    pub overscroll_elasticity: f32,
104
105    /// Maximum overscroll distance in pixels before rubber-banding stops
106    /// Default: 100.0
107    pub max_overscroll_distance: f32,
108
109    /// Bounce-back duration when releasing overscroll (milliseconds)
110    /// Default: 400
111    pub bounce_back_duration_ms: u32,
112
113    /// Timer tick interval in milliseconds for the scroll physics timer.
114    /// Should match the monitor refresh rate (e.g. 16ms for 60Hz, 8ms for 120Hz).
115    /// Default: 16 (60 Hz)
116    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, // No bounce by default (Windows-like)
129            max_overscroll_distance: 100.0,
130            bounce_back_duration_ms: 400,
131            timer_interval_ms: 16,
132        }
133    }
134}
135
136impl ScrollPhysics {
137    /// iOS-like scroll physics with momentum and bounce
138    #[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, // Natural scrolling
146            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    /// macOS-like scroll physics
154    #[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, // Natural scrolling by default
162            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    /// Windows-like scroll physics (no momentum, no bounce)
170    #[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    /// Android-like scroll physics
186    #[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, // Subtle glow effect
195            max_overscroll_distance: 60.0,
196            bounce_back_duration_ms: 300,
197            timer_interval_ms: 16,
198        }
199    }
200}
201
202// ============================================================================
203// Scrollbar Visibility Mode (CSS: -azul-scrollbar-visibility)
204// ============================================================================
205
206/// Controls when the scrollbar is displayed.
207///
208/// This is a per-element CSS property (`-azul-scrollbar-visibility`) that
209/// determines the scrollbar presentation style. It interacts with the
210/// OS-level `ScrollbarPreferences.visibility` (from System Preferences)
211/// when set to `Auto`.
212///
213/// - `Always`: Classic, always-visible scrollbar (Chrome/Windows/Linux default).
214///   Scrollbar reserves layout space.
215/// - `WhenScrolling`: Overlay scrollbar that fades in on scroll activity
216///   and fades out after a delay. Does not reserve layout space.
217/// - `Auto`: Use the OS preference. On macOS this typically means `WhenScrolling`,
218///   on Windows/Linux this typically means `Always`.
219#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
220#[repr(C)]
221pub enum ScrollbarVisibilityMode {
222    /// Scrollbar is always visible (Chrome/Windows/Linux default).
223    /// Reserves layout space.
224    #[default]
225    Always,
226    /// Scrollbar appears on scroll and fades out after inactivity.
227    /// Does not reserve layout space (overlay).
228    WhenScrolling,
229    /// Use the OS-level scrollbar preference.
230    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// ============================================================================
244// Scrollbar Fade Delay (CSS: -azul-scrollbar-fade-delay)
245// ============================================================================
246
247/// Time in milliseconds before the overlay scrollbar starts fading out.
248///
249/// A value of 0 means the scrollbar never fades (always visible).
250/// Typical values: 500ms (macOS), 0ms (Windows).
251///
252/// CSS syntax: `-azul-scrollbar-fade-delay: 500ms;` or `-azul-scrollbar-fade-delay: 0;`
253#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
254#[repr(C)]
255pub struct ScrollbarFadeDelay {
256    /// Delay in milliseconds
257    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// ============================================================================
272// Scrollbar Fade Duration (CSS: -azul-scrollbar-fade-duration)
273// ============================================================================
274
275/// Duration in milliseconds of the scrollbar fade-out animation.
276///
277/// A value of 0 means instant disappearance (no animation).
278/// Typical values: 200ms (macOS), 0ms (Windows).
279///
280/// CSS syntax: `-azul-scrollbar-fade-duration: 200ms;` or `-azul-scrollbar-fade-duration: 0;`
281#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
282#[repr(C)]
283pub struct ScrollbarFadeDuration {
284    /// Duration in milliseconds
285    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// ============================================================================
300// Per-node Overflow Scrolling Mode (CSS: -azul-overflow-scrolling)
301// ============================================================================
302
303/// Controls per-node rubber-banding / momentum scrolling behavior.
304///
305/// Analogous to `-webkit-overflow-scrolling` on iOS Safari.
306///
307/// - `Auto`: Use the global `ScrollPhysics` from `SystemStyle`. On platforms
308///   with `overscroll_elasticity == 0.0` (e.g. Windows), this means no rubber-banding.
309/// - `Touch`: Force momentum scrolling with rubber-banding on this node,
310///   regardless of the global `ScrollPhysics` setting. Uses iOS-like elasticity
311///   if the global elasticity is zero.
312#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
313#[repr(C)]
314pub enum OverflowScrolling {
315    /// Use the global scroll physics (platform default). No rubber-banding on Windows.
316    #[default]
317    Auto,
318    /// Force rubber-banding / momentum scrolling on this node (like iOS/macOS).
319    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// ============================================================================
332// Standard Properties
333// ============================================================================
334
335/// Represents the standard `scrollbar-width` property.
336#[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/// Wrapper struct for custom scrollbar colors (thumb and track)
358#[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/// Represents the standard `scrollbar-color` property.
366#[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// -- -webkit-prefixed Properties --
386
387/// Holds info necessary for layouting / styling -webkit-scrollbar properties.
388#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
389#[repr(C)]
390pub struct ScrollbarInfo {
391    /// Total width (or height for vertical scrollbars) of the scrollbar in pixels
392    pub width: LayoutWidth,
393    /// Padding of the scrollbar tracker, in pixels. The inner bar is `width - padding` pixels
394    /// wide.
395    pub padding_left: LayoutPaddingLeft,
396    /// Padding of the scrollbar (right)
397    pub padding_right: LayoutPaddingRight,
398    /// Style of the scrollbar background
399    /// (`-webkit-scrollbar` / `-webkit-scrollbar-track` / `-webkit-scrollbar-track-piece`
400    /// combined)
401    pub track: StyleBackgroundContent,
402    /// Style of the scrollbar thumbs (the "up" / "down" arrows), (`-webkit-scrollbar-thumb`)
403    pub thumb: StyleBackgroundContent,
404    /// Styles the directional buttons on the scrollbar (`-webkit-scrollbar-button`)
405    pub button: StyleBackgroundContent,
406    /// If two scrollbars are present, addresses the (usually) bottom corner
407    /// of the scrollable element, where two scrollbars might meet (`-webkit-scrollbar-corner`)
408    pub corner: StyleBackgroundContent,
409    /// Addresses the draggable resizing handle that appears above the
410    /// `corner` at the bottom corner of some elements (`-webkit-resizer`)
411    pub resizer: StyleBackgroundContent,
412    /// Whether to clip the scrollbar to the container's border-radius.
413    /// When true, if the container has rounded corners, the scrollbar will be
414    /// clipped to those rounded corners instead of having rectangular edges.
415    /// Default is false for classic scrollbars, true for overlay scrollbars.
416    pub clip_to_container_border: bool,
417    /// Scroll behavior for this scrollbar's container (auto or smooth)
418    pub scroll_behavior: ScrollBehavior,
419    /// Overscroll behavior for the X axis
420    pub overscroll_behavior_x: OverscrollBehavior,
421    /// Overscroll behavior for the Y axis  
422    pub overscroll_behavior_y: OverscrollBehavior,
423    /// Per-node overflow scrolling mode (`-azul-overflow-scrolling: auto | touch`)
424    /// `Touch` forces rubber-banding on this node even when the global physics has no bounce.
425    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        // This is a custom format, not standard CSS
437        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/// Scrollbar style for both horizontal and vertical scrollbars.
453#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
454#[repr(C)]
455pub struct ScrollbarStyle {
456    /// Horizontal scrollbar style, if any
457    pub horizontal: ScrollbarInfo,
458    /// Vertical scrollbar style, if any
459    pub vertical: ScrollbarInfo,
460}
461
462impl PrintAsCssValue for ScrollbarStyle {
463    fn print_as_css_value(&self) -> String {
464        // This is a custom format, not standard CSS
465        format!(
466            "horz({}), vert({})",
467            self.horizontal.print_as_css_value(),
468            self.vertical.print_as_css_value()
469        )
470    }
471}
472
473// Formatting to Rust code
474impl 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// --- Final Computed Style ---
535
536/// The final, resolved style for a scrollbar, after considering both
537/// standard and -webkit- properties. This struct is intended for use by the layout engine.
538#[derive(Debug, Clone, PartialEq, Eq)]
539pub struct ComputedScrollbarStyle {
540    /// The width of the scrollbar. `None` signifies `scrollbar-width: none`.
541    pub width: Option<LayoutWidth>,
542    /// The color of the scrollbar thumb. `None` means use UA default.
543    pub thumb_color: Option<ColorU>,
544    /// The color of the scrollbar track. `None` means use UA default.
545    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), // Default width from UA/platform
553            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
565// --- Default Style Constants ---
566
567/// A classic light-themed scrollbar, similar to older Windows versions.
568pub 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
613/// A classic dark-themed scrollbar.
614pub 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
659/// A modern, thin, overlay scrollbar inspired by macOS (Light Theme).
660pub 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    }), // semi-transparent black
675    button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
676    corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
677    resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
678    clip_to_container_border: true, // Overlay scrollbars should clip to rounded borders
679    scroll_behavior: ScrollBehavior::Smooth,
680    overscroll_behavior_x: OverscrollBehavior::Auto,
681    overscroll_behavior_y: OverscrollBehavior::Auto,
682    overflow_scrolling: OverflowScrolling::Auto,
683};
684
685/// A modern, thin, overlay scrollbar inspired by macOS (Dark Theme).
686pub 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    }), // semi-transparent white
701    button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
702    corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
703    resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
704    clip_to_container_border: true, // Overlay scrollbars should clip to rounded borders
705    scroll_behavior: ScrollBehavior::Smooth,
706    overscroll_behavior_x: OverscrollBehavior::Auto,
707    overscroll_behavior_y: OverscrollBehavior::Auto,
708    overflow_scrolling: OverflowScrolling::Auto,
709};
710
711/// A modern scrollbar inspired by Windows 11 (Light Theme).
712pub 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
742/// A modern scrollbar inspired by Windows 11 (Dark Theme).
743pub 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
773/// A modern, thin, overlay scrollbar inspired by iOS (Light Theme).
774pub 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    }), // rgba(0,0,0,0.4)
789    button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
790    corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
791    resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
792    clip_to_container_border: true, // Overlay scrollbars should clip to rounded borders
793    scroll_behavior: ScrollBehavior::Smooth,
794    overscroll_behavior_x: OverscrollBehavior::Auto,
795    overscroll_behavior_y: OverscrollBehavior::Auto,
796    overflow_scrolling: OverflowScrolling::Auto,
797};
798
799/// A modern, thin, overlay scrollbar inspired by iOS (Dark Theme).
800pub 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    }), // rgba(255,255,255,0.4)
815    button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
816    corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
817    resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
818    clip_to_container_border: true, // Overlay scrollbars should clip to rounded borders
819    scroll_behavior: ScrollBehavior::Smooth,
820    overscroll_behavior_x: OverscrollBehavior::Auto,
821    overscroll_behavior_y: OverscrollBehavior::Auto,
822    overflow_scrolling: OverflowScrolling::Auto,
823};
824
825/// A modern, thin, overlay scrollbar inspired by Android (Light Theme).
826pub 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    }), // rgba(0,0,0,0.4)
841    button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
842    corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
843    resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
844    clip_to_container_border: true, // Overlay scrollbars should clip to rounded borders
845    scroll_behavior: ScrollBehavior::Smooth,
846    overscroll_behavior_x: OverscrollBehavior::Contain,
847    overscroll_behavior_y: OverscrollBehavior::Auto,
848    overflow_scrolling: OverflowScrolling::Auto,
849};
850
851/// A modern, thin, overlay scrollbar inspired by Android (Dark Theme).
852pub 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    }), // rgba(255,255,255,0.4)
867    button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
868    corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
869    resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
870    clip_to_container_border: true, // Overlay scrollbars should clip to rounded borders
871    scroll_behavior: ScrollBehavior::Smooth,
872    overscroll_behavior_x: OverscrollBehavior::Contain,
873    overscroll_behavior_y: OverscrollBehavior::Auto,
874    overflow_scrolling: OverflowScrolling::Auto,
875};
876
877// --- PARSERS ---
878
879#[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")]
911/// # Errors
912///
913/// Returns an error if `input` is not a valid CSS `scrollbar-width` value.
914pub 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")]
963/// # Errors
964///
965/// Returns an error if `input` is not a valid CSS `scrollbar-color` value.
966pub 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// --- Scrollbar Visibility Mode Parser ---
996
997#[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")]
1027/// # Errors
1028///
1029/// Returns an error if `input` is not a valid CSS `scrollbar-visibility-mode` value.
1030pub 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// --- Scrollbar Fade Delay Parser ---
1042
1043#[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")]
1078/// # Errors
1079///
1080/// Returns an error if `input` is not a valid CSS `scrollbar-fade-delay` value.
1081pub 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// --- Scrollbar Fade Duration Parser ---
1090
1091#[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")]
1121/// # Errors
1122///
1123/// Returns an error if `input` is not a valid CSS `scrollbar-fade-duration` value.
1124pub 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    /// Largest integer an `f32` represents exactly (`2^24`). Every millisecond
1191    /// count at or below this survives the `f32` hop inside `parse_duration`;
1192    /// above it, neighbouring `f32`s are more than 1ms apart.
1193    #[cfg(feature = "parser")]
1194    const TWO_POW_24: u32 = 16_777_216;
1195
1196    /// Inputs that must never parse as anything, whatever the property.
1197    #[cfg(feature = "parser")]
1198    const GARBAGE: &[&str] = &[
1199        "",
1200        " ",
1201        "   ",
1202        "\t\n",
1203        "\u{a0}",          // non-breaking space (trimmed away -> empty)
1204        "\0",
1205        "\u{1F600}",       // emoji
1206        "e\u{0301}",       // combining acute accent
1207        "\u{202e}auto",    // RTL override prefix
1208        "аuto",            // Cyrillic 'а' homoglyph
1209        "AUTO",
1210        "auto;",
1211        "auto garbage",
1212        "-1",
1213        "NaN",
1214        "inf",
1215        "0x10",
1216        "9223372036854775807", // i64::MAX
1217        "1e400",
1218        "{[(<",
1219    ];
1220
1221    // ======================================================================
1222    // ScrollPhysics presets  (other)
1223    // ======================================================================
1224
1225    /// Every preset must be usable as-is by a scroll animator: no NaN/inf can
1226    /// reach the physics integrator, the deceleration rate has to stay strictly
1227    /// below 1.0 (at 1.0 momentum never decays -> the scroll timer never stops),
1228    /// and the timer tick must be non-zero (a 0ms tick is a busy-loop).
1229    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        // Called twice: no interior state, no drift.
1285        assert_eq!(ScrollPhysics::ios(), ScrollPhysics::ios());
1286        assert_eq!(ScrollPhysics::windows(), ScrollPhysics::windows());
1287
1288        // A preset that silently equals another would mean a copy/paste bug.
1289        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    /// The documented platform character of each preset, asserted rather than
1297    /// assumed: Windows must not bounce, iOS/macOS must scroll naturally.
1298    #[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        // iOS is the "slowest to stop" of the presets.
1310        assert!(ScrollPhysics::ios().deceleration_rate > ScrollPhysics::windows().deceleration_rate);
1311
1312        // The default is Windows-like: no bounce.
1313        assert_eq!(ScrollPhysics::default().overscroll_elasticity, 0.0);
1314    }
1315
1316    // ======================================================================
1317    // ScrollbarFadeDelay::new / ScrollbarFadeDuration::new  (constructors)
1318    // ======================================================================
1319
1320    #[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    /// The derived `Ord` must order by milliseconds, not by declaration order of
1339    /// some future field, otherwise "fades sooner" comparisons invert.
1340    #[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    // ======================================================================
1349    // print_as_css_value  (encoders)
1350    // ======================================================================
1351
1352    #[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    /// `0` is printed unit-less (a bare `0` is legal CSS for a time), everything
1385    /// else carries the `ms` unit — dropping the unit on a non-zero value would
1386    /// emit invalid CSS.
1387    #[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    /// The aggregate printers are non-standard debug formats; they must at least
1417    /// not panic and must include both sub-scrollbars.
1418    #[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    // ======================================================================
1431    // FormatAsRustCode  (codegen encoders)
1432    // ======================================================================
1433
1434    #[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        // The `new(..)` codegen must round-trip the exact u32, including the extremes.
1455        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    // ======================================================================
1491    // Defaults / constants  (invariants)
1492    // ======================================================================
1493
1494    #[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    /// `ComputedScrollbarStyle::default()` reads its colors out of the default
1504    /// `ScrollbarInfo`; if the default track/thumb ever became a gradient the
1505    /// `match` would silently fall through to `None` (UA default) instead.
1506    #[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    /// Overlay presets must clip to the container border, classic (space-reserving)
1527    /// ones must not — the flag is what decides whether the bar is drawn inside
1528    /// rounded corners.
1529    #[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    /// Light and dark variants of the same platform must differ only in color,
1554    /// never in geometry — a width drift would make theme switching relayout.
1555    #[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    // ======================================================================
1573    // parse_layout_scrollbar_width  (parser)
1574    // ======================================================================
1575
1576    #[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    /// Keyword matching is byte-exact: CSS keywords are case-insensitive in the
1606    /// spec, so an upper-case `AUTO` being rejected here is a real (if minor)
1607    /// conformance gap. Pinned so a future fix is a deliberate change.
1608    #[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    /// The error must carry the caller's *untrimmed* slice so diagnostics can
1628    /// point back at the original source text.
1629    #[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    // ======================================================================
1672    // parse_style_scrollbar_color  (parser)
1673    // ======================================================================
1674
1675    #[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        // Too few / too many components: rejected as InvalidValue, not as a color error.
1692        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    /// Component splitting is on *any* whitespace run, so tabs, newlines and
1704    /// repeated spaces are all legal separators.
1705    #[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    /// Color *names* are case-insensitive (the color parser lowercases), unlike
1718    /// the `auto` keyword right above it, which is compared verbatim.
1719    #[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        // "AUTO" is a single token -> not the auto keyword, and not two colors.
1730        assert!(matches!(
1731            parse_style_scrollbar_color("AUTO"),
1732            Err(StyleScrollbarColorParseError::InvalidValue(_))
1733        ));
1734        // ...and `auto` is not a named color either, so it cannot sneak in as one.
1735        assert!(matches!(
1736            parse_style_scrollbar_color("auto auto"),
1737            Err(StyleScrollbarColorParseError::Color(_))
1738        ));
1739    }
1740
1741    /// Whitespace-splitting happens *before* the color parser runs, so a
1742    /// functional color with spaces after its commas is torn into pieces.
1743    /// `rgb(255, 0, 0) blue` is valid CSS but is rejected here; the space-free
1744    /// spelling works. Pinned as a known limitation.
1745    #[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        // A bad thumb is reported as a color error, not as InvalidValue.
1765        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        // Boundary numerics as color components.
1789        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        // Many components: must be rejected on count, not walked color-by-color.
1816        let many = "red ".repeat(100_000);
1817        assert!(matches!(
1818            parse_style_scrollbar_color(&many),
1819            Err(StyleScrollbarColorParseError::InvalidValue(_))
1820        ));
1821    }
1822
1823    /// The color error carries the *trimmed* input (the function rebinds `input`
1824    /// to the trimmed slice), unlike `parse_layout_scrollbar_width`, which keeps
1825    /// the raw slice. Pinned so the inconsistency is visible.
1826    #[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    // ======================================================================
1868    // parse_scrollbar_visibility_mode  (parser)
1869    // ======================================================================
1870
1871    #[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", // space instead of hyphen
1893            "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    // ======================================================================
1939    // parse_time_ms  (private parser)
1940    // ======================================================================
1941
1942    #[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    /// A unit is mandatory (except for a bare `0`) and must be attached to the
1956    /// number — `"1 s"` has an interior space and cannot parse.
1957    #[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    /// Negative *zero* is not less than zero in IEEE-754, so it slips past the
1986    /// `< 0.0` guard and casts to 0 — harmless, but worth pinning.
1987    #[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    /// The float -> u32 cast saturates instead of wrapping or panicking:
1995    /// `inf` clamps to `u32::MAX`, `NaN` becomes 0. Both are *safe* (no UB, no
1996    /// panic), but note that `"infms"` and `"NaNms"` are accepted as durations
1997    /// at all — a stricter parser would reject non-finite times outright.
1998    #[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        // A million digits must saturate, not hang.
2013        let long_number = format!("{}ms", "9".repeat(100_000));
2014        assert_eq!(parse_time_ms(&long_number), Some(u32::MAX));
2015    }
2016
2017    /// Seconds are multiplied by 1000 *before* the cast, so a value that fits in
2018    /// a u32 as seconds can still saturate as milliseconds.
2019    #[cfg(feature = "parser")]
2020    #[test]
2021    fn parse_time_ms_saturates_when_seconds_overflow_milliseconds() {
2022        // Exact while the millisecond product stays inside f32's integer range.
2023        assert_eq!(parse_time_ms("1000s"), Some(1_000_000));
2024        assert_eq!(parse_time_ms("16777s"), Some(16_777_000));
2025
2026        // Past u32::MAX milliseconds the cast clamps instead of wrapping.
2027        assert_eq!(parse_time_ms("4294968s"), Some(u32::MAX));
2028        assert_eq!(parse_time_ms("5000000s"), Some(u32::MAX));
2029
2030        // Just under the clamp, the f32 product is only accurate to ~256ms
2031        // (the ulp at that magnitude) — near, but no longer exact.
2032        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    // ======================================================================
2042    // parse_scrollbar_fade_delay / parse_scrollbar_fade_duration  (parsers)
2043    // ======================================================================
2044
2045    #[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    /// encode -> decode is the identity for every millisecond count an `f32` can
2123    /// represent exactly (`<= 2^24`), including the `0` special case and the
2124    /// `u32::MAX` extreme (whose f32 rounding lands back on `u32::MAX` after the
2125    /// saturating cast).
2126    #[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    /// Above 2^24 the round-trip is lossy: the value is snapped to the nearest
2161    /// representable `f32`. Pinned as a precision limit of the shared duration
2162    /// parser (a delay is never realistically > 4.6 hours, so this is benign).
2163    #[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    // ======================================================================
2173    // Error to_contained / to_shared  (getters)
2174    // ======================================================================
2175
2176    /// Strings that stress the owned<->borrowed error conversions: empty, blank,
2177    /// multibyte, embedded NUL, and a 100k-byte payload.
2178    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    /// The nested `Color` variant must delegate to the color error's own
2247    /// conversion rather than flattening to a string.
2248    #[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    /// Error `Display` must always name the offending input, otherwise a CSS
2261    /// diagnostic is useless. (`Debug` is implemented as `Display` here.)
2262    #[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    /// Displaying an error whose payload is empty or exotic must not panic on a
2283    /// byte/char boundary.
2284    #[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}