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, 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    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    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    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    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    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    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::format_rust_code::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::format_rust_code::format_scrollbar_info(&self.horizontal, tabs + 1),
482            t1,
483            crate::format_rust_code::format_scrollbar_info(&self.vertical, tabs + 1),
484            t,
485        )
486    }
487}
488
489impl crate::format_rust_code::FormatAsRustCode for LayoutScrollbarWidth {
490    fn format_as_rust_code(&self, _tabs: usize) -> String {
491        match self {
492            LayoutScrollbarWidth::Auto => String::from("LayoutScrollbarWidth::Auto"),
493            LayoutScrollbarWidth::Thin => String::from("LayoutScrollbarWidth::Thin"),
494            LayoutScrollbarWidth::None => String::from("LayoutScrollbarWidth::None"),
495        }
496    }
497}
498
499impl crate::format_rust_code::FormatAsRustCode for StyleScrollbarColor {
500    fn format_as_rust_code(&self, _tabs: usize) -> String {
501        match self {
502            StyleScrollbarColor::Auto => String::from("StyleScrollbarColor::Auto"),
503            StyleScrollbarColor::Custom(c) => format!(
504                "StyleScrollbarColor::Custom(ScrollbarColorCustom {{ thumb: {}, track: {} }})",
505                crate::format_rust_code::format_color_value(&c.thumb),
506                crate::format_rust_code::format_color_value(&c.track)
507            ),
508        }
509    }
510}
511
512impl crate::format_rust_code::FormatAsRustCode for ScrollbarVisibilityMode {
513    fn format_as_rust_code(&self, _tabs: usize) -> String {
514        match self {
515            ScrollbarVisibilityMode::Always => String::from("ScrollbarVisibilityMode::Always"),
516            ScrollbarVisibilityMode::WhenScrolling => String::from("ScrollbarVisibilityMode::WhenScrolling"),
517            ScrollbarVisibilityMode::Auto => String::from("ScrollbarVisibilityMode::Auto"),
518        }
519    }
520}
521
522impl crate::format_rust_code::FormatAsRustCode for ScrollbarFadeDelay {
523    fn format_as_rust_code(&self, _tabs: usize) -> String {
524        format!("ScrollbarFadeDelay::new({})", self.ms)
525    }
526}
527
528impl crate::format_rust_code::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)]
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)]
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)]
889#[repr(C, u8)]
890pub enum LayoutScrollbarWidthParseErrorOwned {
891    InvalidValue(AzString),
892}
893impl<'a> LayoutScrollbarWidthParseError<'a> {
894    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    pub fn to_shared<'a>(&'a self) -> LayoutScrollbarWidthParseError<'a> {
904        match self {
905            Self::InvalidValue(s) => LayoutScrollbarWidthParseError::InvalidValue(s.as_str()),
906        }
907    }
908}
909
910#[cfg(feature = "parser")]
911pub fn parse_layout_scrollbar_width<'a>(
912    input: &'a str,
913) -> Result<LayoutScrollbarWidth, LayoutScrollbarWidthParseError<'a>> {
914    match input.trim() {
915        "auto" => Ok(LayoutScrollbarWidth::Auto),
916        "thin" => Ok(LayoutScrollbarWidth::Thin),
917        "none" => Ok(LayoutScrollbarWidth::None),
918        _ => Err(LayoutScrollbarWidthParseError::InvalidValue(input)),
919    }
920}
921
922#[derive(Clone, PartialEq)]
923pub enum StyleScrollbarColorParseError<'a> {
924    InvalidValue(&'a str),
925    Color(CssColorParseError<'a>),
926}
927impl_debug_as_display!(StyleScrollbarColorParseError<'a>);
928impl_display! { StyleScrollbarColorParseError<'a>, {
929    InvalidValue(v) => format!("Invalid scrollbar-color value: \"{}\"", v),
930    Color(e) => format!("Invalid color in scrollbar-color: {}", e),
931}}
932impl_from!(CssColorParseError<'a>, StyleScrollbarColorParseError::Color);
933
934#[derive(Debug, Clone, PartialEq)]
935#[repr(C, u8)]
936pub enum StyleScrollbarColorParseErrorOwned {
937    InvalidValue(AzString),
938    Color(CssColorParseErrorOwned),
939}
940impl<'a> StyleScrollbarColorParseError<'a> {
941    pub fn to_contained(&self) -> StyleScrollbarColorParseErrorOwned {
942        match self {
943            Self::InvalidValue(s) => {
944                StyleScrollbarColorParseErrorOwned::InvalidValue(s.to_string().into())
945            }
946            Self::Color(e) => StyleScrollbarColorParseErrorOwned::Color(e.to_contained()),
947        }
948    }
949}
950impl StyleScrollbarColorParseErrorOwned {
951    pub fn to_shared<'a>(&'a self) -> StyleScrollbarColorParseError<'a> {
952        match self {
953            Self::InvalidValue(s) => StyleScrollbarColorParseError::InvalidValue(s.as_str()),
954            Self::Color(e) => StyleScrollbarColorParseError::Color(e.to_shared()),
955        }
956    }
957}
958
959#[cfg(feature = "parser")]
960pub fn parse_style_scrollbar_color<'a>(
961    input: &'a str,
962) -> Result<StyleScrollbarColor, StyleScrollbarColorParseError<'a>> {
963    let input = input.trim();
964    if input == "auto" {
965        return Ok(StyleScrollbarColor::Auto);
966    }
967
968    let mut parts = input.split_whitespace();
969    let thumb_str = parts
970        .next()
971        .ok_or(StyleScrollbarColorParseError::InvalidValue(input))?;
972    let track_str = parts
973        .next()
974        .ok_or(StyleScrollbarColorParseError::InvalidValue(input))?;
975
976    if parts.next().is_some() {
977        return Err(StyleScrollbarColorParseError::InvalidValue(input));
978    }
979
980    let thumb = parse_css_color(thumb_str)?;
981    let track = parse_css_color(track_str)?;
982
983    Ok(StyleScrollbarColor::Custom(ScrollbarColorCustom {
984        thumb,
985        track,
986    }))
987}
988
989// --- Scrollbar Visibility Mode Parser ---
990
991#[derive(Clone, PartialEq)]
992pub enum ScrollbarVisibilityModeParseError<'a> {
993    InvalidValue(&'a str),
994}
995impl_debug_as_display!(ScrollbarVisibilityModeParseError<'a>);
996impl_display! { ScrollbarVisibilityModeParseError<'a>, {
997    InvalidValue(v) => format!("Invalid scrollbar-visibility value: \"{}\"", v),
998}}
999
1000#[derive(Debug, Clone, PartialEq)]
1001#[repr(C, u8)]
1002pub enum ScrollbarVisibilityModeParseErrorOwned {
1003    InvalidValue(AzString),
1004}
1005impl<'a> ScrollbarVisibilityModeParseError<'a> {
1006    pub fn to_contained(&self) -> ScrollbarVisibilityModeParseErrorOwned {
1007        match self {
1008            Self::InvalidValue(s) => ScrollbarVisibilityModeParseErrorOwned::InvalidValue(s.to_string().into()),
1009        }
1010    }
1011}
1012impl ScrollbarVisibilityModeParseErrorOwned {
1013    pub fn to_shared<'a>(&'a self) -> ScrollbarVisibilityModeParseError<'a> {
1014        match self {
1015            Self::InvalidValue(s) => ScrollbarVisibilityModeParseError::InvalidValue(s.as_str()),
1016        }
1017    }
1018}
1019
1020#[cfg(feature = "parser")]
1021pub fn parse_scrollbar_visibility_mode<'a>(
1022    input: &'a str,
1023) -> Result<ScrollbarVisibilityMode, ScrollbarVisibilityModeParseError<'a>> {
1024    match input.trim() {
1025        "always" => Ok(ScrollbarVisibilityMode::Always),
1026        "when-scrolling" => Ok(ScrollbarVisibilityMode::WhenScrolling),
1027        "auto" => Ok(ScrollbarVisibilityMode::Auto),
1028        _ => Err(ScrollbarVisibilityModeParseError::InvalidValue(input)),
1029    }
1030}
1031
1032// --- Scrollbar Fade Delay Parser ---
1033
1034#[derive(Clone, PartialEq)]
1035pub enum ScrollbarFadeDelayParseError<'a> {
1036    InvalidValue(&'a str),
1037}
1038impl_debug_as_display!(ScrollbarFadeDelayParseError<'a>);
1039impl_display! { ScrollbarFadeDelayParseError<'a>, {
1040    InvalidValue(v) => format!("Invalid scrollbar-fade-delay value: \"{}\"", v),
1041}}
1042
1043#[derive(Debug, Clone, PartialEq)]
1044#[repr(C, u8)]
1045pub enum ScrollbarFadeDelayParseErrorOwned {
1046    InvalidValue(AzString),
1047}
1048impl<'a> ScrollbarFadeDelayParseError<'a> {
1049    pub fn to_contained(&self) -> ScrollbarFadeDelayParseErrorOwned {
1050        match self {
1051            Self::InvalidValue(s) => ScrollbarFadeDelayParseErrorOwned::InvalidValue(s.to_string().into()),
1052        }
1053    }
1054}
1055impl ScrollbarFadeDelayParseErrorOwned {
1056    pub fn to_shared<'a>(&'a self) -> ScrollbarFadeDelayParseError<'a> {
1057        match self {
1058            Self::InvalidValue(s) => ScrollbarFadeDelayParseError::InvalidValue(s.as_str()),
1059        }
1060    }
1061}
1062
1063#[cfg(feature = "parser")]
1064fn parse_time_ms(input: &str) -> Option<u32> {
1065    crate::props::basic::time::parse_duration(input).ok().map(|d| d.inner)
1066}
1067
1068#[cfg(feature = "parser")]
1069pub fn parse_scrollbar_fade_delay<'a>(
1070    input: &'a str,
1071) -> Result<ScrollbarFadeDelay, ScrollbarFadeDelayParseError<'a>> {
1072    parse_time_ms(input)
1073        .map(ScrollbarFadeDelay::new)
1074        .ok_or(ScrollbarFadeDelayParseError::InvalidValue(input))
1075}
1076
1077// --- Scrollbar Fade Duration Parser ---
1078
1079#[derive(Clone, PartialEq)]
1080pub enum ScrollbarFadeDurationParseError<'a> {
1081    InvalidValue(&'a str),
1082}
1083impl_debug_as_display!(ScrollbarFadeDurationParseError<'a>);
1084impl_display! { ScrollbarFadeDurationParseError<'a>, {
1085    InvalidValue(v) => format!("Invalid scrollbar-fade-duration value: \"{}\"", v),
1086}}
1087
1088#[derive(Debug, Clone, PartialEq)]
1089#[repr(C, u8)]
1090pub enum ScrollbarFadeDurationParseErrorOwned {
1091    InvalidValue(AzString),
1092}
1093impl<'a> ScrollbarFadeDurationParseError<'a> {
1094    pub fn to_contained(&self) -> ScrollbarFadeDurationParseErrorOwned {
1095        match self {
1096            Self::InvalidValue(s) => ScrollbarFadeDurationParseErrorOwned::InvalidValue(s.to_string().into()),
1097        }
1098    }
1099}
1100impl ScrollbarFadeDurationParseErrorOwned {
1101    pub fn to_shared<'a>(&'a self) -> ScrollbarFadeDurationParseError<'a> {
1102        match self {
1103            Self::InvalidValue(s) => ScrollbarFadeDurationParseError::InvalidValue(s.as_str()),
1104        }
1105    }
1106}
1107
1108#[cfg(feature = "parser")]
1109pub fn parse_scrollbar_fade_duration<'a>(
1110    input: &'a str,
1111) -> Result<ScrollbarFadeDuration, ScrollbarFadeDurationParseError<'a>> {
1112    parse_time_ms(input)
1113        .map(ScrollbarFadeDuration::new)
1114        .ok_or(ScrollbarFadeDurationParseError::InvalidValue(input))
1115}
1116
1117#[cfg(all(test, feature = "parser"))]
1118mod tests {
1119    use super::*;
1120    use crate::props::basic::color::ColorU;
1121
1122    #[test]
1123    fn test_parse_scrollbar_width() {
1124        assert_eq!(
1125            parse_layout_scrollbar_width("auto").unwrap(),
1126            LayoutScrollbarWidth::Auto
1127        );
1128        assert_eq!(
1129            parse_layout_scrollbar_width("thin").unwrap(),
1130            LayoutScrollbarWidth::Thin
1131        );
1132        assert_eq!(
1133            parse_layout_scrollbar_width("none").unwrap(),
1134            LayoutScrollbarWidth::None
1135        );
1136        assert!(parse_layout_scrollbar_width("thick").is_err());
1137    }
1138
1139    #[test]
1140    fn test_parse_scrollbar_color() {
1141        assert_eq!(
1142            parse_style_scrollbar_color("auto").unwrap(),
1143            StyleScrollbarColor::Auto
1144        );
1145
1146        let custom = parse_style_scrollbar_color("red blue").unwrap();
1147        assert_eq!(
1148            custom,
1149            StyleScrollbarColor::Custom(ScrollbarColorCustom {
1150                thumb: ColorU::RED,
1151                track: ColorU::BLUE
1152            })
1153        );
1154
1155        let custom_hex = parse_style_scrollbar_color("#ff0000 #0000ff").unwrap();
1156        assert_eq!(
1157            custom_hex,
1158            StyleScrollbarColor::Custom(ScrollbarColorCustom {
1159                thumb: ColorU::RED,
1160                track: ColorU::BLUE
1161            })
1162        );
1163
1164        assert!(parse_style_scrollbar_color("red").is_err());
1165        assert!(parse_style_scrollbar_color("red blue green").is_err());
1166    }
1167}