Skip to main content

azul_css/props/style/
scrollbar.rs

1//! CSS properties for styling scrollbars.
2
3use alloc::string::{String, ToString};
4
5use crate::props::{
6    basic::{
7        color::{parse_css_color, ColorU, CssColorParseError, CssColorParseErrorOwned},
8        pixel::PixelValue,
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/// Combined overscroll behavior for X and Y axes
68#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
69#[repr(C)]
70pub struct OverscrollBehaviorXY {
71    pub x: OverscrollBehavior,
72    pub y: OverscrollBehavior,
73}
74
75// ============================================================================
76// Extended Scroll Configuration (Azul-specific)
77// ============================================================================
78
79/// Scroll physics configuration for momentum scrolling
80///
81/// This controls how scrolling feels - the "weight" and "friction" of the scroll.
82/// Different platforms have different scroll physics (iOS vs Android vs Windows).
83#[derive(Debug, Clone, PartialEq, PartialOrd)]
84#[repr(C)]
85pub struct ScrollPhysics {
86    /// Smooth scroll animation duration in milliseconds (default: 300ms)
87    /// Only used when scroll-behavior: smooth
88    pub smooth_scroll_duration_ms: u32,
89
90    /// Deceleration rate for momentum scrolling (0.0 = instant stop, 1.0 = never stops)
91    /// Typical values: 0.95 (fast deceleration) to 0.998 (slow, iOS-like)
92    /// Default: 0.95
93    pub deceleration_rate: f32,
94
95    /// Minimum velocity threshold to start momentum scrolling (pixels/second)
96    /// Below this, scrolling stops immediately. Default: 50.0
97    pub min_velocity_threshold: f32,
98
99    /// Maximum scroll velocity (pixels/second). Default: 8000.0
100    pub max_velocity: f32,
101
102    /// Scroll wheel multiplier. Default: 1.0
103    /// Values > 1.0 make scrolling faster, < 1.0 slower
104    pub wheel_multiplier: f32,
105
106    /// Whether to invert scroll direction (natural scrolling). Default: false
107    pub invert_direction: bool,
108
109    /// Overscroll elasticity (0.0 = no bounce, 1.0 = full bounce like iOS)
110    /// Only applies when overscroll-behavior: auto. Default: 0.0 (no bounce)
111    pub overscroll_elasticity: f32,
112
113    /// Maximum overscroll distance in pixels before rubber-banding stops
114    /// Default: 100.0
115    pub max_overscroll_distance: f32,
116
117    /// Bounce-back duration when releasing overscroll (milliseconds)
118    /// Default: 400
119    pub bounce_back_duration_ms: u32,
120
121    /// Timer tick interval in milliseconds for the scroll physics timer.
122    /// Should match the monitor refresh rate (e.g. 16ms for 60Hz, 8ms for 120Hz).
123    /// Default: 16 (60 Hz)
124    pub timer_interval_ms: u32,
125}
126
127impl Default for ScrollPhysics {
128    fn default() -> Self {
129        Self {
130            smooth_scroll_duration_ms: 300,
131            deceleration_rate: 0.95,
132            min_velocity_threshold: 50.0,
133            max_velocity: 8000.0,
134            wheel_multiplier: 1.0,
135            invert_direction: false,
136            overscroll_elasticity: 0.0, // No bounce by default (Windows-like)
137            max_overscroll_distance: 100.0,
138            bounce_back_duration_ms: 400,
139            timer_interval_ms: 16,
140        }
141    }
142}
143
144impl ScrollPhysics {
145    /// iOS-like scroll physics with momentum and bounce
146    pub const fn ios() -> Self {
147        Self {
148            smooth_scroll_duration_ms: 300,
149            deceleration_rate: 0.998,
150            min_velocity_threshold: 20.0,
151            max_velocity: 8000.0,
152            wheel_multiplier: 1.0,
153            invert_direction: true, // Natural scrolling
154            overscroll_elasticity: 0.5,
155            max_overscroll_distance: 120.0,
156            bounce_back_duration_ms: 500,
157            timer_interval_ms: 16,
158        }
159    }
160
161    /// macOS-like scroll physics
162    pub const fn macos() -> Self {
163        Self {
164            smooth_scroll_duration_ms: 250,
165            deceleration_rate: 0.997,
166            min_velocity_threshold: 30.0,
167            max_velocity: 6000.0,
168            wheel_multiplier: 1.0,
169            invert_direction: true, // Natural scrolling by default
170            overscroll_elasticity: 0.3,
171            max_overscroll_distance: 80.0,
172            bounce_back_duration_ms: 400,
173            timer_interval_ms: 16,
174        }
175    }
176
177    /// Windows-like scroll physics (no momentum, no bounce)
178    pub const fn windows() -> Self {
179        Self {
180            smooth_scroll_duration_ms: 200,
181            deceleration_rate: 0.9,
182            min_velocity_threshold: 100.0,
183            max_velocity: 4000.0,
184            wheel_multiplier: 1.0,
185            invert_direction: false,
186            overscroll_elasticity: 0.0,
187            max_overscroll_distance: 0.0,
188            bounce_back_duration_ms: 200,
189            timer_interval_ms: 16,
190        }
191    }
192
193    /// Android-like scroll physics
194    pub const fn android() -> Self {
195        Self {
196            smooth_scroll_duration_ms: 250,
197            deceleration_rate: 0.996,
198            min_velocity_threshold: 40.0,
199            max_velocity: 8000.0,
200            wheel_multiplier: 1.0,
201            invert_direction: false,
202            overscroll_elasticity: 0.2, // Subtle glow effect
203            max_overscroll_distance: 60.0,
204            bounce_back_duration_ms: 300,
205            timer_interval_ms: 16,
206        }
207    }
208}
209
210// ============================================================================
211// Per-node Overflow Scrolling Mode (CSS: -azul-overflow-scrolling)
212// ============================================================================
213
214/// Controls per-node rubber-banding / momentum scrolling behavior.
215///
216/// Analogous to `-webkit-overflow-scrolling` on iOS Safari.
217///
218/// - `Auto`: Use the global `ScrollPhysics` from `SystemStyle`. On platforms
219///   with `overscroll_elasticity == 0.0` (e.g. Windows), this means no rubber-banding.
220/// - `Touch`: Force momentum scrolling with rubber-banding on this node,
221///   regardless of the global `ScrollPhysics` setting. Uses iOS-like elasticity
222///   if the global elasticity is zero.
223#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
224#[repr(C)]
225pub enum OverflowScrolling {
226    /// Use the global scroll physics (platform default). No rubber-banding on Windows.
227    #[default]
228    Auto,
229    /// Force rubber-banding / momentum scrolling on this node (like iOS/macOS).
230    Touch,
231}
232
233impl PrintAsCssValue for OverflowScrolling {
234    fn print_as_css_value(&self) -> String {
235        match self {
236            Self::Auto => "auto".to_string(),
237            Self::Touch => "touch".to_string(),
238        }
239    }
240}
241
242// ============================================================================
243// Standard Properties
244// ============================================================================
245
246/// Represents the standard `scrollbar-width` property.
247#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
248#[repr(C)]
249pub enum LayoutScrollbarWidth {
250    Auto,
251    Thin,
252    None,
253}
254
255impl Default for LayoutScrollbarWidth {
256    fn default() -> Self {
257        Self::Auto
258    }
259}
260
261impl PrintAsCssValue for LayoutScrollbarWidth {
262    fn print_as_css_value(&self) -> String {
263        match self {
264            Self::Auto => "auto".to_string(),
265            Self::Thin => "thin".to_string(),
266            Self::None => "none".to_string(),
267        }
268    }
269}
270
271/// Wrapper struct for custom scrollbar colors (thumb and track)
272#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
273#[repr(C)]
274pub struct ScrollbarColorCustom {
275    pub thumb: ColorU,
276    pub track: ColorU,
277}
278
279/// Represents the standard `scrollbar-color` property.
280#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
281#[repr(C, u8)]
282pub enum StyleScrollbarColor {
283    Auto,
284    Custom(ScrollbarColorCustom),
285}
286
287impl Default for StyleScrollbarColor {
288    fn default() -> Self {
289        Self::Auto
290    }
291}
292
293impl PrintAsCssValue for StyleScrollbarColor {
294    fn print_as_css_value(&self) -> String {
295        match self {
296            Self::Auto => "auto".to_string(),
297            Self::Custom(c) => format!("{} {}", c.thumb.to_hash(), c.track.to_hash()),
298        }
299    }
300}
301
302// -- -webkit-prefixed Properties --
303
304/// Holds info necessary for layouting / styling -webkit-scrollbar properties.
305#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
306#[repr(C)]
307pub struct ScrollbarInfo {
308    /// Total width (or height for vertical scrollbars) of the scrollbar in pixels
309    pub width: LayoutWidth,
310    /// Padding of the scrollbar tracker, in pixels. The inner bar is `width - padding` pixels
311    /// wide.
312    pub padding_left: LayoutPaddingLeft,
313    /// Padding of the scrollbar (right)
314    pub padding_right: LayoutPaddingRight,
315    /// Style of the scrollbar background
316    /// (`-webkit-scrollbar` / `-webkit-scrollbar-track` / `-webkit-scrollbar-track-piece`
317    /// combined)
318    pub track: StyleBackgroundContent,
319    /// Style of the scrollbar thumbs (the "up" / "down" arrows), (`-webkit-scrollbar-thumb`)
320    pub thumb: StyleBackgroundContent,
321    /// Styles the directional buttons on the scrollbar (`-webkit-scrollbar-button`)
322    pub button: StyleBackgroundContent,
323    /// If two scrollbars are present, addresses the (usually) bottom corner
324    /// of the scrollable element, where two scrollbars might meet (`-webkit-scrollbar-corner`)
325    pub corner: StyleBackgroundContent,
326    /// Addresses the draggable resizing handle that appears above the
327    /// `corner` at the bottom corner of some elements (`-webkit-resizer`)
328    pub resizer: StyleBackgroundContent,
329    /// Whether to clip the scrollbar to the container's border-radius.
330    /// When true, if the container has rounded corners, the scrollbar will be
331    /// clipped to those rounded corners instead of having rectangular edges.
332    /// Default is false for classic scrollbars, true for overlay scrollbars.
333    pub clip_to_container_border: bool,
334    /// Scroll behavior for this scrollbar's container (auto or smooth)
335    pub scroll_behavior: ScrollBehavior,
336    /// Overscroll behavior for the X axis
337    pub overscroll_behavior_x: OverscrollBehavior,
338    /// Overscroll behavior for the Y axis  
339    pub overscroll_behavior_y: OverscrollBehavior,
340    /// Per-node overflow scrolling mode (`-azul-overflow-scrolling: auto | touch`)
341    /// `Touch` forces rubber-banding on this node even when the global physics has no bounce.
342    pub overflow_scrolling: OverflowScrolling,
343}
344
345impl Default for ScrollbarInfo {
346    fn default() -> Self {
347        SCROLLBAR_CLASSIC_LIGHT
348    }
349}
350
351impl PrintAsCssValue for ScrollbarInfo {
352    fn print_as_css_value(&self) -> String {
353        // This is a custom format, not standard CSS
354        format!(
355            "width: {}; padding-left: {}; padding-right: {}; track: {}; thumb: {}; button: {}; \
356             corner: {}; resizer: {}",
357            self.width.print_as_css_value(),
358            self.padding_left.print_as_css_value(),
359            self.padding_right.print_as_css_value(),
360            self.track.print_as_css_value(),
361            self.thumb.print_as_css_value(),
362            self.button.print_as_css_value(),
363            self.corner.print_as_css_value(),
364            self.resizer.print_as_css_value(),
365        )
366    }
367}
368
369/// Scrollbar style for both horizontal and vertical scrollbars.
370#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
371#[repr(C)]
372pub struct ScrollbarStyle {
373    /// Horizontal scrollbar style, if any
374    pub horizontal: ScrollbarInfo,
375    /// Vertical scrollbar style, if any
376    pub vertical: ScrollbarInfo,
377}
378
379impl PrintAsCssValue for ScrollbarStyle {
380    fn print_as_css_value(&self) -> String {
381        // This is a custom format, not standard CSS
382        format!(
383            "horz({}), vert({})",
384            self.horizontal.print_as_css_value(),
385            self.vertical.print_as_css_value()
386        )
387    }
388}
389
390// Formatting to Rust code
391impl crate::format_rust_code::FormatAsRustCode for ScrollbarStyle {
392    fn format_as_rust_code(&self, tabs: usize) -> String {
393        let t = String::from("    ").repeat(tabs);
394        let t1 = String::from("    ").repeat(tabs + 1);
395        format!(
396            "ScrollbarStyle {{\r\n{}horizontal: {},\r\n{}vertical: {},\r\n{}}}",
397            t1,
398            crate::format_rust_code::format_scrollbar_info(&self.horizontal, tabs + 1),
399            t1,
400            crate::format_rust_code::format_scrollbar_info(&self.vertical, tabs + 1),
401            t,
402        )
403    }
404}
405
406impl crate::format_rust_code::FormatAsRustCode for LayoutScrollbarWidth {
407    fn format_as_rust_code(&self, _tabs: usize) -> String {
408        match self {
409            LayoutScrollbarWidth::Auto => String::from("LayoutScrollbarWidth::Auto"),
410            LayoutScrollbarWidth::Thin => String::from("LayoutScrollbarWidth::Thin"),
411            LayoutScrollbarWidth::None => String::from("LayoutScrollbarWidth::None"),
412        }
413    }
414}
415
416impl crate::format_rust_code::FormatAsRustCode for StyleScrollbarColor {
417    fn format_as_rust_code(&self, _tabs: usize) -> String {
418        match self {
419            StyleScrollbarColor::Auto => String::from("StyleScrollbarColor::Auto"),
420            StyleScrollbarColor::Custom(c) => format!(
421                "StyleScrollbarColor::Custom(ScrollbarColorCustom {{ thumb: {}, track: {} }})",
422                crate::format_rust_code::format_color_value(&c.thumb),
423                crate::format_rust_code::format_color_value(&c.track)
424            ),
425        }
426    }
427}
428
429// --- Final Computed Style ---
430
431/// The final, resolved style for a scrollbar, after considering both
432/// standard and -webkit- properties. This struct is intended for use by the layout engine.
433#[derive(Debug, Clone, PartialEq)]
434pub struct ComputedScrollbarStyle {
435    /// The width of the scrollbar. `None` signifies `scrollbar-width: none`.
436    pub width: Option<LayoutWidth>,
437    /// The color of the scrollbar thumb. `None` means use UA default.
438    pub thumb_color: Option<ColorU>,
439    /// The color of the scrollbar track. `None` means use UA default.
440    pub track_color: Option<ColorU>,
441}
442
443impl Default for ComputedScrollbarStyle {
444    fn default() -> Self {
445        let default_info = ScrollbarInfo::default();
446        Self {
447            width: Some(default_info.width), // Default width from UA/platform
448            thumb_color: match default_info.thumb {
449                StyleBackgroundContent::Color(c) => Some(c),
450                _ => None,
451            },
452            track_color: match default_info.track {
453                StyleBackgroundContent::Color(c) => Some(c),
454                _ => None,
455            },
456        }
457    }
458}
459
460// --- Default Style Constants ---
461
462/// A classic light-themed scrollbar, similar to older Windows versions.
463pub const SCROLLBAR_CLASSIC_LIGHT: ScrollbarInfo = ScrollbarInfo {
464    width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(17)),
465    padding_left: LayoutPaddingLeft {
466        inner: crate::props::basic::pixel::PixelValue::const_px(2),
467    },
468    padding_right: LayoutPaddingRight {
469        inner: crate::props::basic::pixel::PixelValue::const_px(2),
470    },
471    track: StyleBackgroundContent::Color(ColorU {
472        r: 241,
473        g: 241,
474        b: 241,
475        a: 255,
476    }),
477    thumb: StyleBackgroundContent::Color(ColorU {
478        r: 193,
479        g: 193,
480        b: 193,
481        a: 255,
482    }),
483    button: StyleBackgroundContent::Color(ColorU {
484        r: 163,
485        g: 163,
486        b: 163,
487        a: 255,
488    }),
489    corner: StyleBackgroundContent::Color(ColorU {
490        r: 241,
491        g: 241,
492        b: 241,
493        a: 255,
494    }),
495    resizer: StyleBackgroundContent::Color(ColorU {
496        r: 241,
497        g: 241,
498        b: 241,
499        a: 255,
500    }),
501    clip_to_container_border: false,
502    scroll_behavior: ScrollBehavior::Auto,
503    overscroll_behavior_x: OverscrollBehavior::Auto,
504    overscroll_behavior_y: OverscrollBehavior::Auto,
505    overflow_scrolling: OverflowScrolling::Auto,
506};
507
508/// A classic dark-themed scrollbar.
509pub const SCROLLBAR_CLASSIC_DARK: ScrollbarInfo = ScrollbarInfo {
510    width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(17)),
511    padding_left: LayoutPaddingLeft {
512        inner: crate::props::basic::pixel::PixelValue::const_px(2),
513    },
514    padding_right: LayoutPaddingRight {
515        inner: crate::props::basic::pixel::PixelValue::const_px(2),
516    },
517    track: StyleBackgroundContent::Color(ColorU {
518        r: 45,
519        g: 45,
520        b: 45,
521        a: 255,
522    }),
523    thumb: StyleBackgroundContent::Color(ColorU {
524        r: 100,
525        g: 100,
526        b: 100,
527        a: 255,
528    }),
529    button: StyleBackgroundContent::Color(ColorU {
530        r: 120,
531        g: 120,
532        b: 120,
533        a: 255,
534    }),
535    corner: StyleBackgroundContent::Color(ColorU {
536        r: 45,
537        g: 45,
538        b: 45,
539        a: 255,
540    }),
541    resizer: StyleBackgroundContent::Color(ColorU {
542        r: 45,
543        g: 45,
544        b: 45,
545        a: 255,
546    }),
547    clip_to_container_border: false,
548    scroll_behavior: ScrollBehavior::Auto,
549    overscroll_behavior_x: OverscrollBehavior::Auto,
550    overscroll_behavior_y: OverscrollBehavior::Auto,
551    overflow_scrolling: OverflowScrolling::Auto,
552};
553
554/// A modern, thin, overlay scrollbar inspired by macOS (Light Theme).
555pub const SCROLLBAR_MACOS_LIGHT: ScrollbarInfo = ScrollbarInfo {
556    width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(8)),
557    padding_left: LayoutPaddingLeft {
558        inner: crate::props::basic::pixel::PixelValue::const_px(0),
559    },
560    padding_right: LayoutPaddingRight {
561        inner: crate::props::basic::pixel::PixelValue::const_px(0),
562    },
563    track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
564    thumb: StyleBackgroundContent::Color(ColorU {
565        r: 0,
566        g: 0,
567        b: 0,
568        a: 100,
569    }), // semi-transparent black
570    button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
571    corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
572    resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
573    clip_to_container_border: true, // Overlay scrollbars should clip to rounded borders
574    scroll_behavior: ScrollBehavior::Smooth,
575    overscroll_behavior_x: OverscrollBehavior::Auto,
576    overscroll_behavior_y: OverscrollBehavior::Auto,
577    overflow_scrolling: OverflowScrolling::Auto,
578};
579
580/// A modern, thin, overlay scrollbar inspired by macOS (Dark Theme).
581pub const SCROLLBAR_MACOS_DARK: ScrollbarInfo = ScrollbarInfo {
582    width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(8)),
583    padding_left: LayoutPaddingLeft {
584        inner: crate::props::basic::pixel::PixelValue::const_px(0),
585    },
586    padding_right: LayoutPaddingRight {
587        inner: crate::props::basic::pixel::PixelValue::const_px(0),
588    },
589    track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
590    thumb: StyleBackgroundContent::Color(ColorU {
591        r: 255,
592        g: 255,
593        b: 255,
594        a: 100,
595    }), // semi-transparent white
596    button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
597    corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
598    resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
599    clip_to_container_border: true, // Overlay scrollbars should clip to rounded borders
600    scroll_behavior: ScrollBehavior::Smooth,
601    overscroll_behavior_x: OverscrollBehavior::Auto,
602    overscroll_behavior_y: OverscrollBehavior::Auto,
603    overflow_scrolling: OverflowScrolling::Auto,
604};
605
606/// A modern scrollbar inspired by Windows 11 (Light Theme).
607pub const SCROLLBAR_WINDOWS_LIGHT: ScrollbarInfo = ScrollbarInfo {
608    width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(12)),
609    padding_left: LayoutPaddingLeft {
610        inner: crate::props::basic::pixel::PixelValue::const_px(0),
611    },
612    padding_right: LayoutPaddingRight {
613        inner: crate::props::basic::pixel::PixelValue::const_px(0),
614    },
615    track: StyleBackgroundContent::Color(ColorU {
616        r: 241,
617        g: 241,
618        b: 241,
619        a: 255,
620    }),
621    thumb: StyleBackgroundContent::Color(ColorU {
622        r: 130,
623        g: 130,
624        b: 130,
625        a: 255,
626    }),
627    button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
628    corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
629    resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
630    clip_to_container_border: false,
631    scroll_behavior: ScrollBehavior::Auto,
632    overscroll_behavior_x: OverscrollBehavior::None,
633    overscroll_behavior_y: OverscrollBehavior::None,
634    overflow_scrolling: OverflowScrolling::Auto,
635};
636
637/// A modern scrollbar inspired by Windows 11 (Dark Theme).
638pub const SCROLLBAR_WINDOWS_DARK: ScrollbarInfo = ScrollbarInfo {
639    width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(12)),
640    padding_left: LayoutPaddingLeft {
641        inner: crate::props::basic::pixel::PixelValue::const_px(0),
642    },
643    padding_right: LayoutPaddingRight {
644        inner: crate::props::basic::pixel::PixelValue::const_px(0),
645    },
646    track: StyleBackgroundContent::Color(ColorU {
647        r: 32,
648        g: 32,
649        b: 32,
650        a: 255,
651    }),
652    thumb: StyleBackgroundContent::Color(ColorU {
653        r: 110,
654        g: 110,
655        b: 110,
656        a: 255,
657    }),
658    button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
659    corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
660    resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
661    clip_to_container_border: false,
662    scroll_behavior: ScrollBehavior::Auto,
663    overscroll_behavior_x: OverscrollBehavior::None,
664    overscroll_behavior_y: OverscrollBehavior::None,
665    overflow_scrolling: OverflowScrolling::Auto,
666};
667
668/// A modern, thin, overlay scrollbar inspired by iOS (Light Theme).
669pub const SCROLLBAR_IOS_LIGHT: ScrollbarInfo = ScrollbarInfo {
670    width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(7)),
671    padding_left: LayoutPaddingLeft {
672        inner: crate::props::basic::pixel::PixelValue::const_px(0),
673    },
674    padding_right: LayoutPaddingRight {
675        inner: crate::props::basic::pixel::PixelValue::const_px(0),
676    },
677    track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
678    thumb: StyleBackgroundContent::Color(ColorU {
679        r: 0,
680        g: 0,
681        b: 0,
682        a: 102,
683    }), // rgba(0,0,0,0.4)
684    button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
685    corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
686    resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
687    clip_to_container_border: true, // Overlay scrollbars should clip to rounded borders
688    scroll_behavior: ScrollBehavior::Smooth,
689    overscroll_behavior_x: OverscrollBehavior::Auto,
690    overscroll_behavior_y: OverscrollBehavior::Auto,
691    overflow_scrolling: OverflowScrolling::Auto,
692};
693
694/// A modern, thin, overlay scrollbar inspired by iOS (Dark Theme).
695pub const SCROLLBAR_IOS_DARK: ScrollbarInfo = ScrollbarInfo {
696    width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(7)),
697    padding_left: LayoutPaddingLeft {
698        inner: crate::props::basic::pixel::PixelValue::const_px(0),
699    },
700    padding_right: LayoutPaddingRight {
701        inner: crate::props::basic::pixel::PixelValue::const_px(0),
702    },
703    track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
704    thumb: StyleBackgroundContent::Color(ColorU {
705        r: 255,
706        g: 255,
707        b: 255,
708        a: 102,
709    }), // rgba(255,255,255,0.4)
710    button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
711    corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
712    resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
713    clip_to_container_border: true, // Overlay scrollbars should clip to rounded borders
714    scroll_behavior: ScrollBehavior::Smooth,
715    overscroll_behavior_x: OverscrollBehavior::Auto,
716    overscroll_behavior_y: OverscrollBehavior::Auto,
717    overflow_scrolling: OverflowScrolling::Auto,
718};
719
720/// A modern, thin, overlay scrollbar inspired by Android (Light Theme).
721pub const SCROLLBAR_ANDROID_LIGHT: ScrollbarInfo = ScrollbarInfo {
722    width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(6)),
723    padding_left: LayoutPaddingLeft {
724        inner: crate::props::basic::pixel::PixelValue::const_px(0),
725    },
726    padding_right: LayoutPaddingRight {
727        inner: crate::props::basic::pixel::PixelValue::const_px(0),
728    },
729    track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
730    thumb: StyleBackgroundContent::Color(ColorU {
731        r: 0,
732        g: 0,
733        b: 0,
734        a: 102,
735    }), // rgba(0,0,0,0.4)
736    button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
737    corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
738    resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
739    clip_to_container_border: true, // Overlay scrollbars should clip to rounded borders
740    scroll_behavior: ScrollBehavior::Smooth,
741    overscroll_behavior_x: OverscrollBehavior::Contain,
742    overscroll_behavior_y: OverscrollBehavior::Auto,
743    overflow_scrolling: OverflowScrolling::Auto,
744};
745
746/// A modern, thin, overlay scrollbar inspired by Android (Dark Theme).
747pub const SCROLLBAR_ANDROID_DARK: ScrollbarInfo = ScrollbarInfo {
748    width: LayoutWidth::Px(crate::props::basic::pixel::PixelValue::const_px(6)),
749    padding_left: LayoutPaddingLeft {
750        inner: crate::props::basic::pixel::PixelValue::const_px(0),
751    },
752    padding_right: LayoutPaddingRight {
753        inner: crate::props::basic::pixel::PixelValue::const_px(0),
754    },
755    track: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
756    thumb: StyleBackgroundContent::Color(ColorU {
757        r: 255,
758        g: 255,
759        b: 255,
760        a: 102,
761    }), // rgba(255,255,255,0.4)
762    button: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
763    corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
764    resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
765    clip_to_container_border: true, // Overlay scrollbars should clip to rounded borders
766    scroll_behavior: ScrollBehavior::Smooth,
767    overscroll_behavior_x: OverscrollBehavior::Contain,
768    overscroll_behavior_y: OverscrollBehavior::Auto,
769    overflow_scrolling: OverflowScrolling::Auto,
770};
771
772// --- PARSERS ---
773
774#[derive(Clone, PartialEq)]
775pub enum LayoutScrollbarWidthParseError<'a> {
776    InvalidValue(&'a str),
777}
778impl_debug_as_display!(LayoutScrollbarWidthParseError<'a>);
779impl_display! { LayoutScrollbarWidthParseError<'a>, {
780    InvalidValue(v) => format!("Invalid scrollbar-width value: \"{}\"", v),
781}}
782
783#[derive(Debug, Clone, PartialEq)]
784pub enum LayoutScrollbarWidthParseErrorOwned {
785    InvalidValue(String),
786}
787impl<'a> LayoutScrollbarWidthParseError<'a> {
788    pub fn to_contained(&self) -> LayoutScrollbarWidthParseErrorOwned {
789        match self {
790            Self::InvalidValue(s) => {
791                LayoutScrollbarWidthParseErrorOwned::InvalidValue(s.to_string())
792            }
793        }
794    }
795}
796impl LayoutScrollbarWidthParseErrorOwned {
797    pub fn to_shared<'a>(&'a self) -> LayoutScrollbarWidthParseError<'a> {
798        match self {
799            Self::InvalidValue(s) => LayoutScrollbarWidthParseError::InvalidValue(s.as_str()),
800        }
801    }
802}
803
804#[cfg(feature = "parser")]
805pub fn parse_layout_scrollbar_width<'a>(
806    input: &'a str,
807) -> Result<LayoutScrollbarWidth, LayoutScrollbarWidthParseError<'a>> {
808    match input.trim() {
809        "auto" => Ok(LayoutScrollbarWidth::Auto),
810        "thin" => Ok(LayoutScrollbarWidth::Thin),
811        "none" => Ok(LayoutScrollbarWidth::None),
812        _ => Err(LayoutScrollbarWidthParseError::InvalidValue(input)),
813    }
814}
815
816#[derive(Clone, PartialEq)]
817pub enum StyleScrollbarColorParseError<'a> {
818    InvalidValue(&'a str),
819    Color(CssColorParseError<'a>),
820}
821impl_debug_as_display!(StyleScrollbarColorParseError<'a>);
822impl_display! { StyleScrollbarColorParseError<'a>, {
823    InvalidValue(v) => format!("Invalid scrollbar-color value: \"{}\"", v),
824    Color(e) => format!("Invalid color in scrollbar-color: {}", e),
825}}
826impl_from!(CssColorParseError<'a>, StyleScrollbarColorParseError::Color);
827
828#[derive(Debug, Clone, PartialEq)]
829pub enum StyleScrollbarColorParseErrorOwned {
830    InvalidValue(String),
831    Color(CssColorParseErrorOwned),
832}
833impl<'a> StyleScrollbarColorParseError<'a> {
834    pub fn to_contained(&self) -> StyleScrollbarColorParseErrorOwned {
835        match self {
836            Self::InvalidValue(s) => {
837                StyleScrollbarColorParseErrorOwned::InvalidValue(s.to_string())
838            }
839            Self::Color(e) => StyleScrollbarColorParseErrorOwned::Color(e.to_contained()),
840        }
841    }
842}
843impl StyleScrollbarColorParseErrorOwned {
844    pub fn to_shared<'a>(&'a self) -> StyleScrollbarColorParseError<'a> {
845        match self {
846            Self::InvalidValue(s) => StyleScrollbarColorParseError::InvalidValue(s.as_str()),
847            Self::Color(e) => StyleScrollbarColorParseError::Color(e.to_shared()),
848        }
849    }
850}
851
852#[cfg(feature = "parser")]
853pub fn parse_style_scrollbar_color<'a>(
854    input: &'a str,
855) -> Result<StyleScrollbarColor, StyleScrollbarColorParseError<'a>> {
856    let input = input.trim();
857    if input == "auto" {
858        return Ok(StyleScrollbarColor::Auto);
859    }
860
861    let mut parts = input.split_whitespace();
862    let thumb_str = parts
863        .next()
864        .ok_or(StyleScrollbarColorParseError::InvalidValue(input))?;
865    let track_str = parts
866        .next()
867        .ok_or(StyleScrollbarColorParseError::InvalidValue(input))?;
868
869    if parts.next().is_some() {
870        return Err(StyleScrollbarColorParseError::InvalidValue(input));
871    }
872
873    let thumb = parse_css_color(thumb_str)?;
874    let track = parse_css_color(track_str)?;
875
876    Ok(StyleScrollbarColor::Custom(ScrollbarColorCustom {
877        thumb,
878        track,
879    }))
880}
881
882#[derive(Clone, PartialEq)]
883pub enum CssScrollbarStyleParseError<'a> {
884    Invalid(&'a str),
885}
886
887impl_debug_as_display!(CssScrollbarStyleParseError<'a>);
888impl_display! { CssScrollbarStyleParseError<'a>, {
889    Invalid(e) => format!("Invalid scrollbar style: \"{}\"", e),
890}}
891
892#[derive(Debug, Clone, PartialEq)]
893pub enum CssScrollbarStyleParseErrorOwned {
894    Invalid(String),
895}
896
897impl<'a> CssScrollbarStyleParseError<'a> {
898    pub fn to_contained(&self) -> CssScrollbarStyleParseErrorOwned {
899        match self {
900            CssScrollbarStyleParseError::Invalid(s) => {
901                CssScrollbarStyleParseErrorOwned::Invalid(s.to_string())
902            }
903        }
904    }
905}
906
907impl CssScrollbarStyleParseErrorOwned {
908    pub fn to_shared<'a>(&'a self) -> CssScrollbarStyleParseError<'a> {
909        match self {
910            CssScrollbarStyleParseErrorOwned::Invalid(s) => {
911                CssScrollbarStyleParseError::Invalid(s.as_str())
912            }
913        }
914    }
915}
916
917#[cfg(feature = "parser")]
918pub fn parse_scrollbar_style<'a>(
919    _input: &'a str,
920) -> Result<ScrollbarStyle, CssScrollbarStyleParseError<'a>> {
921    // A real implementation would parse the custom format used for -webkit-scrollbar.
922    // For now, it returns the default style.
923    Ok(ScrollbarStyle::default())
924}
925
926// --- STYLE RESOLUTION ---
927
928/// Resolves the final scrollbar style for a node based on standard and
929/// non-standard CSS properties.
930///
931/// This function implements the specified override behavior: if `scrollbar-width`
932/// or `scrollbar-color` are set to anything other than `auto`, they take
933/// precedence over any `::-webkit-scrollbar` styling.
934pub fn resolve_scrollbar_style(
935    scrollbar_width: Option<&LayoutScrollbarWidth>,
936    scrollbar_color: Option<&StyleScrollbarColor>,
937    webkit_scrollbar_style: Option<&ScrollbarStyle>,
938) -> ComputedScrollbarStyle {
939    let final_width = scrollbar_width
940        .cloned()
941        .unwrap_or(LayoutScrollbarWidth::Auto);
942    let final_color = scrollbar_color
943        .cloned()
944        .unwrap_or(StyleScrollbarColor::Auto);
945
946    // If standard properties are used (not 'auto'), they win.
947    if final_width != LayoutScrollbarWidth::Auto || final_color != StyleScrollbarColor::Auto {
948        let width = match final_width {
949            LayoutScrollbarWidth::None => None,
950            // Use a reasonable default for "thin"
951            LayoutScrollbarWidth::Thin => Some(LayoutWidth::Px(PixelValue::px(8.0))),
952            // If auto, fall back to -webkit- width or the UA default
953            LayoutScrollbarWidth::Auto => Some(
954                webkit_scrollbar_style
955                    .map_or_else(|| ScrollbarInfo::default().width, |s| s.vertical.width.clone()),
956            ),
957        };
958
959        let (thumb_color, track_color) = match final_color {
960            StyleScrollbarColor::Custom(c) => (Some(c.thumb), Some(c.track)),
961            StyleScrollbarColor::Auto => (None, None), // UA default
962        };
963
964        return ComputedScrollbarStyle {
965            width: width.clone(),
966            thumb_color,
967            track_color,
968        };
969    }
970
971    // Otherwise, fall back to -webkit-scrollbar properties if they exist.
972    if let Some(webkit_style) = webkit_scrollbar_style {
973        // For simplicity, we'll use the vertical scrollbar's info.
974        let info = &webkit_style.vertical;
975
976        // The -webkit-scrollbar `display: none;` is often implemented by setting width to 0.
977        let width_pixels = match info.width {
978            LayoutWidth::Px(px) => {
979                use crate::props::basic::pixel::DEFAULT_FONT_SIZE;
980                px.to_pixels_internal(0.0, DEFAULT_FONT_SIZE)
981            }
982            _ => 8.0, // Default for min-content/max-content
983        };
984        if width_pixels <= 0.0 {
985            return ComputedScrollbarStyle {
986                width: None,
987                thumb_color: None,
988                track_color: None,
989            };
990        }
991
992        let thumb = match &info.thumb {
993            StyleBackgroundContent::Color(c) => Some(*c),
994            _ => None, // Gradients, images are not directly mapped to a single color
995        };
996
997        let track = match &info.track {
998            StyleBackgroundContent::Color(c) => Some(*c),
999            _ => None,
1000        };
1001
1002        return ComputedScrollbarStyle {
1003            width: Some(info.width.clone()),
1004            thumb_color: thumb,
1005            track_color: track,
1006        };
1007    }
1008
1009    // If no styling is provided at all, use UA defaults.
1010    ComputedScrollbarStyle::default()
1011}
1012
1013#[cfg(all(test, feature = "parser"))]
1014mod tests {
1015    use super::*;
1016    use crate::props::{basic::color::ColorU, layout::dimensions::LayoutWidth};
1017
1018    // --- Parser Tests ---
1019
1020    #[test]
1021    fn test_parse_scrollbar_width() {
1022        assert_eq!(
1023            parse_layout_scrollbar_width("auto").unwrap(),
1024            LayoutScrollbarWidth::Auto
1025        );
1026        assert_eq!(
1027            parse_layout_scrollbar_width("thin").unwrap(),
1028            LayoutScrollbarWidth::Thin
1029        );
1030        assert_eq!(
1031            parse_layout_scrollbar_width("none").unwrap(),
1032            LayoutScrollbarWidth::None
1033        );
1034        assert!(parse_layout_scrollbar_width("thick").is_err());
1035    }
1036
1037    #[test]
1038    fn test_parse_scrollbar_color() {
1039        assert_eq!(
1040            parse_style_scrollbar_color("auto").unwrap(),
1041            StyleScrollbarColor::Auto
1042        );
1043
1044        let custom = parse_style_scrollbar_color("red blue").unwrap();
1045        assert_eq!(
1046            custom,
1047            StyleScrollbarColor::Custom(ScrollbarColorCustom {
1048                thumb: ColorU::RED,
1049                track: ColorU::BLUE
1050            })
1051        );
1052
1053        let custom_hex = parse_style_scrollbar_color("#ff0000 #0000ff").unwrap();
1054        assert_eq!(
1055            custom_hex,
1056            StyleScrollbarColor::Custom(ScrollbarColorCustom {
1057                thumb: ColorU::RED,
1058                track: ColorU::BLUE
1059            })
1060        );
1061
1062        assert!(parse_style_scrollbar_color("red").is_err());
1063        assert!(parse_style_scrollbar_color("red blue green").is_err());
1064    }
1065
1066    // --- Resolution Logic Tests ---
1067
1068    // Helper to create a default -webkit- style for testing
1069    fn get_webkit_style() -> ScrollbarStyle {
1070        let mut info = ScrollbarInfo::default();
1071        info.width = LayoutWidth::px(15.0);
1072        info.thumb = StyleBackgroundContent::Color(ColorU::GREEN);
1073        info.track = StyleBackgroundContent::Color(ColorU::new_rgb(100, 100, 100));
1074        ScrollbarStyle {
1075            horizontal: info.clone(),
1076            vertical: info,
1077        }
1078    }
1079
1080    #[test]
1081    fn test_resolve_standard_overrides_webkit() {
1082        let width = LayoutScrollbarWidth::Thin;
1083        let color = StyleScrollbarColor::Custom(ScrollbarColorCustom {
1084            thumb: ColorU::RED,
1085            track: ColorU::BLUE,
1086        });
1087        let webkit_style = get_webkit_style();
1088
1089        let resolved = resolve_scrollbar_style(Some(&width), Some(&color), Some(&webkit_style));
1090
1091        // "thin" resolves to a specific px value (e.g., 8px)
1092        assert_eq!(resolved.width, Some(LayoutWidth::px(8.0)));
1093        assert_eq!(resolved.thumb_color, Some(ColorU::RED));
1094        assert_eq!(resolved.track_color, Some(ColorU::BLUE));
1095    }
1096
1097    #[test]
1098    fn test_resolve_standard_auto_falls_back_to_webkit() {
1099        let width = LayoutScrollbarWidth::Auto;
1100        let color = StyleScrollbarColor::Auto;
1101        let webkit_style = get_webkit_style();
1102
1103        let resolved = resolve_scrollbar_style(Some(&width), Some(&color), Some(&webkit_style));
1104
1105        assert_eq!(resolved.width, Some(LayoutWidth::px(15.0)));
1106        assert_eq!(resolved.thumb_color, Some(ColorU::GREEN));
1107        assert_eq!(resolved.track_color, Some(ColorU::new_rgb(100, 100, 100)));
1108    }
1109
1110    #[test]
1111    fn test_resolve_no_styles_uses_default() {
1112        let resolved = resolve_scrollbar_style(None, None, None);
1113        assert_eq!(resolved, ComputedScrollbarStyle::default());
1114    }
1115
1116    #[test]
1117    fn test_resolve_scrollbar_width_none() {
1118        let width = LayoutScrollbarWidth::None;
1119        let webkit_style = get_webkit_style();
1120
1121        let resolved = resolve_scrollbar_style(Some(&width), None, Some(&webkit_style));
1122        assert_eq!(resolved.width, None);
1123    }
1124
1125    #[test]
1126    fn test_resolve_webkit_display_none_equivalent() {
1127        let mut webkit_style = get_webkit_style();
1128        webkit_style.vertical.width = LayoutWidth::px(0.0);
1129
1130        let resolved = resolve_scrollbar_style(None, None, Some(&webkit_style));
1131        assert_eq!(resolved.width, None);
1132    }
1133
1134    #[test]
1135    fn test_resolve_only_color_is_set() {
1136        let color = StyleScrollbarColor::Custom(ScrollbarColorCustom {
1137            thumb: ColorU::RED,
1138            track: ColorU::BLUE,
1139        });
1140        let webkit_style = get_webkit_style();
1141
1142        let resolved = resolve_scrollbar_style(None, Some(&color), Some(&webkit_style));
1143
1144        // Width should fall back to webkit, but colors should be standard
1145        assert_eq!(resolved.width, Some(LayoutWidth::px(15.0)));
1146        assert_eq!(resolved.thumb_color, Some(ColorU::RED));
1147        assert_eq!(resolved.track_color, Some(ColorU::BLUE));
1148    }
1149
1150    #[test]
1151    fn test_resolve_only_width_is_set() {
1152        let width = LayoutScrollbarWidth::Thin;
1153        let webkit_style = get_webkit_style();
1154
1155        let resolved = resolve_scrollbar_style(Some(&width), None, Some(&webkit_style));
1156
1157        // Width should be from standard, colors should be UA default (since standard color is auto)
1158        assert_eq!(resolved.width, Some(LayoutWidth::px(8.0)));
1159        assert_eq!(resolved.thumb_color, None);
1160        assert_eq!(resolved.track_color, None);
1161    }
1162}