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