Skip to main content

iced_widget/
scrollable.rs

1//! Scrollables let users navigate an endless amount of content with a scrollbar.
2//!
3//! # Example
4//! ```no_run
5//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
6//! # use iced::widget::Widget;
7//! # pub type State = ();
8//! use iced::widget::{column, scrollable, space};
9//!
10//! enum Message {
11//!     // ...
12//! }
13//!
14//! fn view(state: &State) -> impl Widget<Message> {
15//!     scrollable(column![
16//!         "Scroll me!",
17//!         space().height(3000),
18//!         "You did it!",
19//!     ])
20//! }
21//! ```
22use crate::container;
23use crate::core::alignment;
24use crate::core::border::{self, Border};
25use crate::core::keyboard;
26use crate::core::layout;
27use crate::core::mouse;
28use crate::core::overlay;
29use crate::core::renderer;
30use crate::core::text;
31use crate::core::time::{Duration, Instant};
32use crate::core::touch;
33use crate::core::widget;
34use crate::core::widget::operation::{self, Animation, Operation};
35use crate::core::widget::tree::{self, Tree};
36use crate::core::window;
37use crate::core::{
38    self, Background, Color, Event, InputMethod, Layout, Length, Padding, Pixels, Point, Rectangle,
39    Shadow, Shell, Size, Theme, Vector, Widget,
40};
41
42pub use operation::scrollable::{AbsoluteOffset, RelativeOffset};
43
44/// A widget that can vertically display an infinite amount of content with a
45/// scrollbar.
46///
47/// # Example
48/// ```no_run
49/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
50/// # use iced::widget::Widget;
51/// # pub type State = ();
52/// use iced::widget::{column, scrollable, space};
53///
54/// enum Message {
55///     // ...
56/// }
57///
58/// fn view(state: &State) -> impl Widget<Message> {
59///     scrollable(column![
60///         "Scroll me!",
61///         space().height(3000),
62///         "You did it!",
63///     ])
64/// }
65/// ```
66///
67/// # Scrollbars
68///
69/// A scrollbar has two interactive parts: the *scroller* (the draggable
70/// thumb) and the *rail* (the track it slides on).
71///
72/// * Pressing and dragging the **scroller** moves it 1:1 with the pointer.
73/// * Pressing the **rail** scrolls one page (animated like a wheel scroll
74///   when [`Self::smooth_scroll`] is enabled). While the button is held, the
75///   scroller then slides toward the pointer at a constant speed, stopping
76///   when its edge reaches the pointer; moving the pointer moves the target
77///   with it. This mirrors the track autoscroll of most toolkits.
78/// * `Shift`-clicking the **rail** instead jumps the scroller so that its
79///   center is under the pointer, and then drags it from there.
80///
81/// With [`Self::click_to_scroll`] enabled, the behavior of the rail press
82/// and the `Shift`-click is inverted.
83pub struct Scrollable<'a, Message, W = crate::Element<'a, Message>, Theme = crate::Theme>
84where
85    Theme: Catalog,
86{
87    id: Option<widget::Id>,
88    width: Length,
89    height: Length,
90    direction: Direction,
91    auto_scroll: bool,
92    smooth_scroll: bool,
93    click_to_scroll: bool,
94    content: W,
95    on_scroll: Option<Box<dyn Fn(Scroll) -> Action<Message> + 'a>>,
96    class: Theme::Class<'a>,
97}
98
99impl<'a, Message, W, Theme> Scrollable<'a, Message, W, Theme>
100where
101    Theme: Catalog,
102{
103    /// Creates a new vertical [`Scrollable`].
104    pub fn new(content: W) -> Self {
105        Self::with_direction(content, Direction::default())
106    }
107
108    /// Creates a new [`Scrollable`] with the given [`Direction`].
109    pub fn with_direction(content: W, direction: impl Into<Direction>) -> Self {
110        Scrollable {
111            id: None,
112            width: Length::Fit,
113            height: Length::Fit,
114            direction: direction.into(),
115            auto_scroll: false,
116            smooth_scroll: true,
117            click_to_scroll: false,
118            content,
119            on_scroll: None,
120            class: Theme::default(),
121        }
122    }
123
124    /// Makes the [`Scrollable`] scroll horizontally, with default [`Scrollbar`] settings.
125    pub fn horizontal(self) -> Self {
126        self.direction(Direction::Horizontal(Scrollbar::default()))
127    }
128
129    /// Sets the [`Direction`] of the [`Scrollable`].
130    pub fn direction(mut self, direction: impl Into<Direction>) -> Self {
131        self.direction = direction.into();
132        self
133    }
134
135    /// Sets the [`widget::Id`] of the [`Scrollable`].
136    pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
137        self.id = Some(id.into());
138        self
139    }
140
141    /// Sets the width of the [`Scrollable`].
142    pub fn width(mut self, width: impl Into<Length>) -> Self {
143        self.width = width.into();
144        self
145    }
146
147    /// Sets the height of the [`Scrollable`].
148    pub fn height(mut self, height: impl Into<Length>) -> Self {
149        self.height = height.into();
150        self
151    }
152
153    /// Sets a handler to call when the [`Scrollable`] is scrolled.
154    ///
155    /// The function takes a [`Scroll`], which contains the [`Viewport`] of
156    /// the [`Scrollable`] and the [`Source`] of the scroll that caused the
157    /// notification.
158    pub fn on_scroll<T>(mut self, f: impl Fn(Scroll) -> T + 'a) -> Self
159    where
160        T: Into<Action<Message>>,
161    {
162        self.on_scroll = Some(Box::new(move |scroll| f(scroll).into()));
163        self
164    }
165
166    /// Anchors the vertical [`Scrollable`] direction to the top.
167    pub fn anchor_top(self) -> Self {
168        self.anchor_y(Anchor::Start)
169    }
170
171    /// Anchors the vertical [`Scrollable`] direction to the bottom.
172    pub fn anchor_bottom(self) -> Self {
173        self.anchor_y(Anchor::End)
174    }
175
176    /// Anchors the horizontal [`Scrollable`] direction to the left.
177    pub fn anchor_left(self) -> Self {
178        self.anchor_x(Anchor::Start)
179    }
180
181    /// Anchors the horizontal [`Scrollable`] direction to the right.
182    pub fn anchor_right(self) -> Self {
183        self.anchor_x(Anchor::End)
184    }
185
186    /// Sets the [`Anchor`] of the horizontal direction of the [`Scrollable`], if applicable.
187    pub fn anchor_x(mut self, alignment: Anchor) -> Self {
188        match &mut self.direction {
189            Direction::Horizontal(horizontal) | Direction::Both { horizontal, .. } => {
190                horizontal.alignment = alignment;
191            }
192            Direction::Vertical { .. } => {}
193        }
194
195        self
196    }
197
198    /// Sets the [`Anchor`] of the vertical direction of the [`Scrollable`], if applicable.
199    pub fn anchor_y(mut self, alignment: Anchor) -> Self {
200        match &mut self.direction {
201            Direction::Vertical(vertical) | Direction::Both { vertical, .. } => {
202                vertical.alignment = alignment;
203            }
204            Direction::Horizontal { .. } => {}
205        }
206
207        self
208    }
209
210    /// Embeds the [`Scrollbar`] into the [`Scrollable`], instead of floating on top of the
211    /// content.
212    ///
213    /// The `spacing` provided will be used as space between the [`Scrollbar`] and the contents
214    /// of the [`Scrollable`].
215    pub fn spacing(mut self, new_spacing: impl Into<Pixels>) -> Self {
216        match &mut self.direction {
217            Direction::Horizontal(scrollbar) | Direction::Vertical(scrollbar) => {
218                scrollbar.spacing = Some(new_spacing.into().0);
219            }
220            Direction::Both { .. } => {}
221        }
222
223        self
224    }
225
226    /// Adds padding at the ends of the [`Scrollbar`]s of the [`Scrollable`].
227    ///
228    /// The `padding` provided will be used as space at the top and bottom of a
229    /// vertical [`Scrollbar`], and at the left and right ends of a horizontal
230    /// [`Scrollbar`], when they are visible.
231    ///
232    /// Unlike [`Self::spacing`], the padding does not affect the layout of the
233    /// [`Scrollable`].
234    pub fn padding(mut self, new_padding: impl Into<Pixels>) -> Self {
235        let padding = new_padding.into().0;
236
237        match &mut self.direction {
238            Direction::Horizontal(scrollbar) | Direction::Vertical(scrollbar) => {
239                scrollbar.padding = padding;
240            }
241            Direction::Both {
242                horizontal,
243                vertical,
244            } => {
245                horizontal.padding = padding;
246                vertical.padding = padding;
247            }
248        }
249
250        self
251    }
252
253    /// Sets whether the user should be allowed to auto-scroll the [`Scrollable`]
254    /// with the middle mouse button.
255    ///
256    /// By default, it is disabled.
257    pub fn auto_scroll(mut self, auto_scroll: bool) -> Self {
258        self.auto_scroll = auto_scroll;
259        self
260    }
261
262    /// Sets whether wheel scrolling should be smoothed out over time, instead of
263    /// moving the [`Scrollable`] immediately.
264    ///
265    /// When enabled, discrete scrolls (e.g. from a mouse wheel) move a target
266    /// scroll offset — carrying the momentum of the wheel notches — and the
267    /// [`Scrollable`] eases towards it over a few frames. High-precision
268    /// scrolls (e.g. from a touchpad), which are already smooth, are always
269    /// applied immediately.
270    ///
271    /// The setting also acts as the default behavior of the scroll operations
272    /// (`snap_to`, `scroll_to`, `scroll_by`): operations with
273    /// [`Animation::Auto`] scroll smoothly if and only if it is enabled.
274    ///
275    /// By default, it is enabled.
276    pub fn smooth_scroll(mut self, smooth_scroll: bool) -> Self {
277        self.smooth_scroll = smooth_scroll;
278        self
279    }
280
281    /// Sets whether a plain click on the scrollbar rail should jump the
282    /// scroller to the click position, instead of scrolling one page.
283    ///
284    /// By default, a press on the rail scrolls one page (and autoscrolls
285    /// while the button is held), while a `Shift`-click jumps the scroller
286    /// so that its center is under the pointer and drags it from there.
287    ///
288    /// When enabled, the behavior is inverted: a plain click jumps the
289    /// scroller so that its center is under the pointer (and drags it from
290    /// there), while a `Shift`-click scrolls one page (and autoscrolls
291    /// while the button is held).
292    ///
293    /// By default, it is disabled.
294    pub fn click_to_scroll(mut self, click_to_scroll: bool) -> Self {
295        self.click_to_scroll = click_to_scroll;
296        self
297    }
298
299    /// Sets the style of this [`Scrollable`].
300    #[must_use]
301    pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
302    where
303        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
304    {
305        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
306        self
307    }
308
309    /// Sets the style class of the [`Scrollable`].
310    #[cfg(feature = "advanced")]
311    #[must_use]
312    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
313        self.class = class.into();
314        self
315    }
316}
317
318/// The direction of [`Scrollable`].
319#[derive(Debug, Clone, Copy, PartialEq)]
320pub enum Direction {
321    /// Vertical scrolling
322    Vertical(Scrollbar),
323    /// Horizontal scrolling
324    Horizontal(Scrollbar),
325    /// Both vertical and horizontal scrolling
326    Both {
327        /// The properties of the vertical scrollbar.
328        vertical: Scrollbar,
329        /// The properties of the horizontal scrollbar.
330        horizontal: Scrollbar,
331    },
332}
333
334impl Direction {
335    /// Returns the horizontal [`Scrollbar`], if any.
336    pub fn horizontal(&self) -> Option<&Scrollbar> {
337        match self {
338            Self::Horizontal(scrollbar) => Some(scrollbar),
339            Self::Both { horizontal, .. } => Some(horizontal),
340            Self::Vertical(_) => None,
341        }
342    }
343
344    /// Returns the vertical [`Scrollbar`], if any.
345    pub fn vertical(&self) -> Option<&Scrollbar> {
346        match self {
347            Self::Vertical(scrollbar) => Some(scrollbar),
348            Self::Both { vertical, .. } => Some(vertical),
349            Self::Horizontal(_) => None,
350        }
351    }
352
353    fn align(&self, delta: Vector) -> Vector {
354        let horizontal_alignment = self.horizontal().map(|p| p.alignment).unwrap_or_default();
355
356        let vertical_alignment = self.vertical().map(|p| p.alignment).unwrap_or_default();
357
358        let align = |alignment: Anchor, delta: f32| match alignment {
359            Anchor::Start => delta,
360            Anchor::End => -delta,
361        };
362
363        Vector::new(
364            align(horizontal_alignment, delta.x),
365            align(vertical_alignment, delta.y),
366        )
367    }
368}
369
370impl Default for Direction {
371    fn default() -> Self {
372        Self::Vertical(Scrollbar::default())
373    }
374}
375
376/// A scrollbar within a [`Scrollable`].
377#[derive(Debug, Clone, Copy, PartialEq)]
378pub struct Scrollbar {
379    width: f32,
380    margin: f32,
381    scroller_width: f32,
382    alignment: Anchor,
383    spacing: Option<f32>,
384    padding: f32,
385}
386
387impl Default for Scrollbar {
388    fn default() -> Self {
389        Self {
390            width: 10.0,
391            margin: 0.0,
392            scroller_width: 10.0,
393            alignment: Anchor::Start,
394            spacing: None,
395            padding: 0.0,
396        }
397    }
398}
399
400impl Scrollbar {
401    /// Creates new [`Scrollbar`] for use in a [`Scrollable`].
402    pub fn new() -> Self {
403        Self::default()
404    }
405
406    /// Create a [`Scrollbar`] with zero width to allow a [`Scrollable`] to scroll without a visible
407    /// scroller.
408    pub fn hidden() -> Self {
409        Self::default().width(0).scroller_width(0)
410    }
411
412    /// Sets the scrollbar width of the [`Scrollbar`] .
413    pub fn width(mut self, width: impl Into<Pixels>) -> Self {
414        self.width = width.into().0.max(0.0);
415        self
416    }
417
418    /// Sets the scrollbar margin of the [`Scrollbar`] .
419    pub fn margin(mut self, margin: impl Into<Pixels>) -> Self {
420        self.margin = margin.into().0;
421        self
422    }
423
424    /// Sets the scroller width of the [`Scrollbar`] .
425    pub fn scroller_width(mut self, scroller_width: impl Into<Pixels>) -> Self {
426        self.scroller_width = scroller_width.into().0.max(0.0);
427        self
428    }
429
430    /// Sets the [`Anchor`] of the [`Scrollbar`] .
431    pub fn anchor(mut self, alignment: Anchor) -> Self {
432        self.alignment = alignment;
433        self
434    }
435
436    /// Sets whether the [`Scrollbar`] should be embedded in the [`Scrollable`], using
437    /// the given spacing between itself and the contents.
438    ///
439    /// An embedded [`Scrollbar`] will always be displayed, will take layout space,
440    /// and will not float over the contents.
441    pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
442        self.spacing = Some(spacing.into().0);
443        self
444    }
445
446    /// Sets the padding of the [`Scrollbar`].
447    ///
448    /// The padding is added at the top and bottom of the scrollbar (or at the
449    /// left and right ends for a horizontal [`Scrollbar`]) when it is visible.
450    ///
451    /// Unlike [`Self::margin`] and [`Self::spacing`], the padding does not
452    /// affect the layout of the [`Scrollable`].
453    pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
454        self.padding = padding.into().0;
455        self
456    }
457}
458
459/// The anchor of the scroller of the [`Scrollable`] relative to its [`Viewport`]
460/// on a given axis.
461#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
462pub enum Anchor {
463    /// Scroller is anchored to the start of the [`Viewport`].
464    #[default]
465    Start,
466    /// Content is aligned to the end of the [`Viewport`].
467    End,
468}
469
470impl<Message, W, Theme> widget::Meta for Scrollable<'_, Message, W, Theme> where Theme: Catalog {}
471
472impl<Message, W, Theme, Renderer> Widget<Message, Theme, Renderer>
473    for Scrollable<'_, Message, W, Theme>
474where
475    Theme: Catalog,
476    Renderer: text::Renderer,
477    W: Widget<Message, Theme, Renderer>,
478{
479    fn tag(&self) -> tree::Tag {
480        tree::Tag::of::<State>()
481    }
482
483    fn state(&self) -> tree::State {
484        tree::State::new(State::new())
485    }
486
487    fn diff(&mut self, tree: &mut Tree) {
488        tree.diff_children(std::slice::from_mut(&mut self.content));
489
490        let state = tree.state.downcast_mut::<State>();
491
492        if state.last_id != self.id {
493            *state = State {
494                last_id: self.id.clone(),
495                ..State::default()
496            };
497        }
498
499        let size = self.content.size();
500
501        self.width = self.width.stack(size.width);
502        self.height = self.height.stack(size.height);
503    }
504
505    fn size(&self) -> Size<Length> {
506        Size {
507            width: self.width,
508            height: self.height,
509        }
510    }
511
512    fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits) {
513        let mut layout = |tree: &mut Tree, right_padding: f32, bottom_padding: f32| {
514            let is_horizontal = self.direction.horizontal().is_some();
515            let is_vertical = self.direction.vertical().is_some();
516
517            layout::padded(
518                tree,
519                limits,
520                self.width,
521                self.height,
522                Padding {
523                    right: right_padding,
524                    bottom: bottom_padding,
525                    ..Padding::ZERO
526                },
527                |tree, limits| {
528                    let child_limits = layout::Limits::with_flags(
529                        limits.min,
530                        limits.max,
531                        limits.compression,
532                        Size::new(
533                            limits.infinite.width || is_horizontal,
534                            limits.infinite.height || is_vertical,
535                        ),
536                    );
537
538                    self.content.layout(tree, renderer, &child_limits);
539
540                    tree.size
541                },
542            );
543        };
544
545        match self.direction {
546            Direction::Vertical(Scrollbar {
547                width,
548                margin,
549                spacing: Some(spacing),
550                ..
551            })
552            | Direction::Horizontal(Scrollbar {
553                width,
554                margin,
555                spacing: Some(spacing),
556                ..
557            }) => {
558                let is_vertical = matches!(self.direction, Direction::Vertical(_));
559
560                let padding = width + margin * 2.0 + spacing;
561                let was_scrollbar_visible = tree.state.downcast_mut::<State>().is_scrollbar_visible;
562
563                layout(
564                    tree,
565                    if is_vertical && was_scrollbar_visible {
566                        padding
567                    } else {
568                        0.0
569                    },
570                    if !is_vertical && was_scrollbar_visible {
571                        padding
572                    } else {
573                        0.0
574                    },
575                );
576
577                let is_scrollbar_visible = if is_vertical {
578                    tree.children[0].size.height > tree.size.height
579                } else {
580                    tree.children[0].size.width > tree.size.width
581                };
582
583                if was_scrollbar_visible != is_scrollbar_visible {
584                    log::trace!("Scrollbar status quo has changed");
585                    tree.state.downcast_mut::<State>().is_scrollbar_visible = is_scrollbar_visible;
586
587                    layout(
588                        tree,
589                        if is_vertical && is_scrollbar_visible {
590                            padding
591                        } else {
592                            0.0
593                        },
594                        if !is_vertical && is_scrollbar_visible {
595                            padding
596                        } else {
597                            0.0
598                        },
599                    );
600                }
601            }
602            _ => layout(tree, 0.0, 0.0),
603        }
604    }
605
606    fn operate(
607        &mut self,
608        tree: &mut Tree,
609        layout: Layout,
610        viewport: &Rectangle,
611        renderer: &Renderer,
612        operation: &mut dyn Operation,
613    ) {
614        let bounds = layout.bounds();
615        let (content_layout, content_tree) = layout.iter_mut(&mut tree.children).next().unwrap();
616        let content_bounds = content_layout.bounds();
617        let content = content_layout.size();
618
619        let state = tree.state.downcast_mut::<State>();
620
621        // `Animation::Auto` scroll operations resolve against this
622        state.smooth_scroll = self.smooth_scroll;
623
624        let translation = state.last_translation;
625        let viewport = viewport.intersection(&bounds).unwrap_or_default() + translation;
626
627        operation.scrollable(self.id.as_ref(), bounds, content, translation, state);
628        operation.container(self.id.as_ref(), content_bounds, &viewport);
629
630        operation.traverse(&mut |operation| {
631            self.content
632                .operate(content_tree, content_layout, &viewport, renderer, operation);
633        });
634    }
635
636    fn update(
637        &mut self,
638        tree: &mut Tree,
639        event: &Event,
640        layout: Layout,
641        cursor: mouse::Cursor,
642        renderer: &Renderer,
643        shell: &mut Shell<'_, Message>,
644        viewport: &Rectangle,
645    ) {
646        let bounds = layout.bounds();
647        let cursor_over_scrollable = cursor.position_over(bounds);
648
649        let (content_layout, content_tree) = layout.iter_mut(&mut tree.children).next().unwrap();
650        let content = content_layout.size();
651
652        let state = tree.state.downcast_mut::<State>();
653        let scrollbars = Scrollbars::new(state.last_translation, self.direction, bounds, content);
654        let mouse_over_scrollbar = scrollbars.is_mouse_over(cursor);
655        let last_offsets = (state.offset_x, state.offset_y);
656
657        let interact = state.interact(
658            event,
659            bounds,
660            content,
661            cursor,
662            cursor_over_scrollable,
663            &scrollbars,
664            mouse_over_scrollbar,
665            self.direction,
666            self.smooth_scroll,
667            self.click_to_scroll,
668        );
669
670        if let Some(scroll) = interact.scroll
671            && let Some(on_scroll) = &self.on_scroll
672        {
673            on_scroll(scroll).perform(state, bounds, content, shell);
674
675            if let Event::Window(window::Event::RedrawRequested(_now)) = event {
676                state.last_translation = state.translation(self.direction, bounds, content);
677            }
678        }
679
680        if interact.capture {
681            shell.capture_event();
682        }
683
684        if interact.invalidate_layout {
685            shell.invalidate_layout();
686        }
687
688        if interact.request_redraw {
689            shell.request_redraw();
690        }
691
692        if !interact.stop {
693            if let Some(Content { cursor }) = interact.content {
694                let had_input_method = shell.input_method().is_enabled();
695
696                let viewport =
697                    bounds.intersection(viewport).unwrap_or_default() + state.last_translation;
698
699                self.content.update(
700                    content_tree,
701                    event,
702                    content_layout,
703                    cursor,
704                    renderer,
705                    shell,
706                    &viewport,
707                );
708
709                if !had_input_method
710                    && let InputMethod::Enabled { cursor, .. } = shell.input_method_mut()
711                {
712                    *cursor -= state.last_translation;
713                }
714            }
715
716            let update = state.update(
717                event,
718                bounds,
719                content,
720                cursor,
721                cursor_over_scrollable,
722                mouse_over_scrollbar,
723                self.direction,
724                self.smooth_scroll,
725                self.auto_scroll,
726                shell.is_event_captured(),
727            );
728
729            if let Some(scroll) = update.scroll
730                && let Some(on_scroll) = &self.on_scroll
731            {
732                on_scroll(scroll).perform(state, bounds, content, shell);
733            }
734
735            if update.capture {
736                shell.capture_event();
737            }
738
739            if update.invalidate_layout {
740                shell.invalidate_layout();
741            }
742
743            if update.request_redraw {
744                shell.request_redraw();
745            }
746        }
747
748        let status = if let Some(axis) = state.interaction.axis() {
749            Status::Dragged {
750                is_horizontal_scrollbar_dragged: axis == Axis::X,
751                is_vertical_scrollbar_dragged: axis == Axis::Y,
752                is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
753                is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
754            }
755        } else if cursor_over_scrollable.is_some() {
756            Status::Hovered {
757                is_horizontal_scrollbar_hovered: mouse_over_scrollbar == Some(Axis::X),
758                is_vertical_scrollbar_hovered: mouse_over_scrollbar == Some(Axis::Y),
759                is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
760                is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
761            }
762        } else {
763            Status::Active {
764                is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
765                is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
766            }
767        };
768
769        if let Event::Window(window::Event::RedrawRequested(_now)) = event {
770            state.last_status = Some(status);
771        }
772
773        if last_offsets != (state.offset_x, state.offset_y)
774            || state
775                .last_status
776                .is_some_and(|last_status| last_status != status)
777        {
778            shell.request_redraw();
779        }
780    }
781
782    fn draw(
783        &self,
784        tree: &Tree,
785        renderer: &mut Renderer,
786        theme: &Theme,
787        defaults: &renderer::Style,
788        layout: Layout,
789        cursor: mouse::Cursor,
790        viewport: &Rectangle,
791    ) {
792        let state = tree.state.downcast_ref::<State>();
793        let bounds = layout.bounds();
794
795        let Some(viewport) = viewport.intersection(&bounds) else {
796            return;
797        };
798
799        let (content_layout, content_tree) = layout.iter(&tree.children).next().unwrap();
800        let content = content_layout.size();
801
802        let translation = state.last_translation;
803        let scrollbars = Scrollbars::new(translation, self.direction, bounds, content);
804        let cursor_over_scrollable = cursor.position_over(bounds);
805        let mouse_over_scrollbar = scrollbars.is_mouse_over(cursor);
806
807        let cursor = match cursor_over_scrollable {
808            Some(cursor_position) if mouse_over_scrollbar.is_none() => {
809                mouse::Cursor::Available(cursor_position + translation)
810            }
811            _ => cursor.obstruct() + translation,
812        };
813
814        let style = theme.style(
815            &self.class,
816            state.last_status.unwrap_or(Status::Active {
817                is_horizontal_scrollbar_disabled: false,
818                is_vertical_scrollbar_disabled: false,
819            }),
820        );
821
822        container::draw_background(renderer, &style.container, layout.bounds());
823
824        // Draw inner content
825        if scrollbars.active() {
826            renderer.with_layer(viewport, |renderer| {
827                renderer.with_translation(
828                    -translation.hint(renderer.hint_factor().unwrap_or(1.0)),
829                    |renderer| {
830                        self.content.draw(
831                            content_tree,
832                            renderer,
833                            theme,
834                            defaults,
835                            content_layout,
836                            cursor,
837                            &(viewport + translation),
838                        );
839                    },
840                );
841            });
842
843            let draw_scrollbar =
844                |renderer: &mut Renderer, style: Rail, scrollbar: &internals::Scrollbar| {
845                    if scrollbar.bounds.width > 0.0
846                        && scrollbar.bounds.height > 0.0
847                        && (style.background.is_some()
848                            || (style.border.color != Color::TRANSPARENT
849                                && style.border.width > 0.0))
850                    {
851                        renderer.fill_quad(
852                            renderer::Quad {
853                                bounds: scrollbar.bounds,
854                                border: style.border,
855                                ..renderer::Quad::default()
856                            },
857                            style
858                                .background
859                                .unwrap_or(Background::Color(Color::TRANSPARENT)),
860                        );
861                    }
862
863                    if let Some(scroller) = scrollbar.scroller
864                        && scroller.bounds.width > 0.0
865                        && scroller.bounds.height > 0.0
866                        && (style.scroller.background != Background::Color(Color::TRANSPARENT)
867                            || (style.scroller.border.color != Color::TRANSPARENT
868                                && style.scroller.border.width > 0.0))
869                    {
870                        renderer.fill_quad(
871                            renderer::Quad {
872                                bounds: scroller.bounds,
873                                border: style.scroller.border,
874                                ..renderer::Quad::default()
875                            },
876                            style.scroller.background,
877                        );
878                    }
879                };
880
881            let has_floating_scrollbar = scrollbars.is_any_floating();
882
883            if has_floating_scrollbar {
884                renderer.start_layer(viewport);
885            }
886
887            if let Some(scrollbar) = scrollbars.y {
888                draw_scrollbar(renderer, style.vertical_rail, &scrollbar);
889            }
890
891            if let Some(scrollbar) = scrollbars.x {
892                draw_scrollbar(renderer, style.horizontal_rail, &scrollbar);
893            }
894
895            if let (Some(x), Some(y)) = (scrollbars.x, scrollbars.y) {
896                let background = style.gap.or(style.container.background);
897
898                if let Some(background) = background {
899                    renderer.fill_quad(
900                        renderer::Quad {
901                            bounds: Rectangle {
902                                x: y.bounds.x,
903                                y: x.bounds.y,
904                                width: y.bounds.width,
905                                height: x.bounds.height,
906                            },
907                            ..renderer::Quad::default()
908                        },
909                        background,
910                    );
911                }
912            }
913
914            if has_floating_scrollbar {
915                renderer.end_layer();
916            }
917        } else {
918            self.content.draw(
919                content_tree,
920                renderer,
921                theme,
922                defaults,
923                content_layout,
924                cursor,
925                &(viewport + translation),
926            );
927        }
928    }
929
930    fn mouse_interaction(
931        &self,
932        tree: &Tree,
933        layout: Layout,
934        cursor: mouse::Cursor,
935        viewport: &Rectangle,
936        renderer: &Renderer,
937    ) -> mouse::Interaction {
938        let bounds = layout.bounds();
939        let state = tree.state.downcast_ref::<State>();
940
941        if state.interaction.scrollers_grabbed() {
942            return mouse::Interaction::Idle;
943        }
944
945        let Some(viewport) = viewport.intersection(&bounds) else {
946            return mouse::Interaction::None;
947        };
948
949        let cursor_over_scrollable = cursor.position_over(bounds);
950        let (content_layout, content_tree) = layout.iter(&tree.children).next().unwrap();
951        let content = content_layout.size();
952
953        let translation = state.last_translation;
954        let scrollbars = Scrollbars::new(translation, self.direction, bounds, content);
955        let mouse_over_scrollbar = scrollbars.is_mouse_over(cursor);
956
957        let cursor = match cursor_over_scrollable {
958            Some(cursor_position) if mouse_over_scrollbar.is_none() => {
959                mouse::Cursor::Available(cursor_position + translation)
960            }
961            _ => cursor.obstruct() + translation,
962        };
963
964        self.content.mouse_interaction(
965            content_tree,
966            content_layout,
967            cursor,
968            &(viewport + translation),
969            renderer,
970        )
971    }
972
973    fn overlay<'b>(
974        &'b mut self,
975        tree: &'b mut Tree,
976        layout: Layout,
977        renderer: &Renderer,
978        viewport: &Rectangle,
979        translation: Vector,
980        window: Size,
981    ) -> Vec<overlay::Element<'b, Message, Theme, Renderer>> {
982        let state = tree.state.downcast_ref::<State>();
983        let bounds = layout.bounds();
984        let (content_layout, content_tree) = layout.iter_mut(&mut tree.children).next().unwrap();
985        let content = content_layout.size();
986        let viewport = viewport.intersection(&bounds).unwrap_or(*viewport);
987
988        let offset = state.last_translation;
989
990        let overlay = self.content.overlay(
991            content_tree,
992            content_layout,
993            renderer,
994            &(viewport + offset),
995            translation - offset,
996            window,
997        );
998
999        let icon = if let Interaction::AutoScrolling { origin, .. } = state.interaction {
1000            let scrollbars = Scrollbars::new(offset, self.direction, bounds, content);
1001
1002            Some(overlay::Element::new(Box::new(AutoScrollIcon {
1003                origin,
1004                vertical: scrollbars.y.is_some(),
1005                horizontal: scrollbars.x.is_some(),
1006                class: &self.class,
1007            })))
1008        } else {
1009            None
1010        };
1011
1012        overlay.into_iter().chain(icon).collect()
1013    }
1014}
1015
1016struct AutoScrollIcon<'a, Class> {
1017    origin: Point,
1018    vertical: bool,
1019    horizontal: bool,
1020    class: &'a Class,
1021}
1022
1023impl<Class> AutoScrollIcon<'_, Class> {
1024    const SIZE: f32 = 40.0;
1025    const DOT: f32 = Self::SIZE / 10.0;
1026    const PADDING: f32 = Self::SIZE / 10.0;
1027}
1028
1029impl<Message, Theme, Renderer> core::Overlay<Message, Theme, Renderer>
1030    for AutoScrollIcon<'_, Theme::Class<'_>>
1031where
1032    Renderer: text::Renderer,
1033    Theme: Catalog,
1034{
1035    fn draw(
1036        &self,
1037        renderer: &mut Renderer,
1038        theme: &Theme,
1039        _style: &renderer::Style,
1040        _cursor: mouse::Cursor,
1041    ) {
1042        let bounds = Rectangle::new(
1043            self.origin - Vector::new(Self::SIZE, Self::SIZE) / 2.0,
1044            Size::new(Self::SIZE, Self::SIZE),
1045        );
1046
1047        let style = theme
1048            .style(
1049                self.class,
1050                Status::Active {
1051                    is_horizontal_scrollbar_disabled: false,
1052                    is_vertical_scrollbar_disabled: false,
1053                },
1054            )
1055            .auto_scroll;
1056
1057        renderer.with_layer(bounds, |renderer| {
1058            renderer.fill_quad(
1059                renderer::Quad {
1060                    bounds,
1061                    border: style.border,
1062                    shadow: style.shadow,
1063                    snap: false,
1064                },
1065                style.background,
1066            );
1067
1068            renderer.fill_quad(
1069                renderer::Quad {
1070                    bounds: Rectangle::new(
1071                        bounds.center() - Vector::new(Self::DOT, Self::DOT) / 2.0,
1072                        Size::new(Self::DOT, Self::DOT),
1073                    ),
1074                    border: border::rounded(bounds.width),
1075                    snap: false,
1076                    ..renderer::Quad::default()
1077                },
1078                style.icon,
1079            );
1080
1081            let arrow = core::Text {
1082                content: String::new(),
1083                bounds: bounds.size(),
1084                size: Pixels::from(12),
1085                line_height: text::LineHeight::from(1.0),
1086                font: Renderer::ICON_FONT,
1087                align_x: text::Alignment::Center,
1088                align_y: alignment::Vertical::Center,
1089                shaping: text::Shaping::Basic,
1090                wrapping: text::Wrapping::None,
1091                ellipsis: text::Ellipsis::None,
1092                hint_factor: None,
1093            };
1094
1095            if self.vertical {
1096                renderer.fill_text(
1097                    core::Text {
1098                        content: Renderer::SCROLL_UP_ICON.to_string(),
1099                        align_y: alignment::Vertical::Top,
1100                        ..arrow
1101                    },
1102                    Point::new(bounds.center_x(), bounds.y + Self::PADDING),
1103                    style.icon,
1104                    bounds,
1105                );
1106
1107                renderer.fill_text(
1108                    core::Text {
1109                        content: Renderer::SCROLL_DOWN_ICON.to_string(),
1110                        align_y: alignment::Vertical::Bottom,
1111                        ..arrow
1112                    },
1113                    Point::new(
1114                        bounds.center_x(),
1115                        bounds.y + bounds.height - Self::PADDING - 0.5,
1116                    ),
1117                    style.icon,
1118                    bounds,
1119                );
1120            }
1121
1122            if self.horizontal {
1123                renderer.fill_text(
1124                    core::Text {
1125                        content: Renderer::SCROLL_LEFT_ICON.to_string(),
1126                        align_x: text::Alignment::Left,
1127                        ..arrow
1128                    },
1129                    Point::new(bounds.x + Self::PADDING + 1.0, bounds.center_y() + 1.0),
1130                    style.icon,
1131                    bounds,
1132                );
1133
1134                renderer.fill_text(
1135                    core::Text {
1136                        content: Renderer::SCROLL_RIGHT_ICON.to_string(),
1137                        align_x: text::Alignment::Right,
1138                        ..arrow
1139                    },
1140                    Point::new(
1141                        bounds.x + bounds.width - Self::PADDING - 1.0,
1142                        bounds.center_y() + 1.0,
1143                    ),
1144                    style.icon,
1145                    bounds,
1146                );
1147            }
1148        });
1149    }
1150
1151    fn index(&self) -> f32 {
1152        f32::MAX
1153    }
1154}
1155
1156/// The new position of the pointer of a held rail, if it moved.
1157///
1158/// A moved pointer re-evaluates the autoscroll's pause and stop conditions,
1159/// so it wakes the animation with a redraw.
1160fn rail_moved(event: &Event, cursor: mouse::Cursor, last: Point) -> Option<Point> {
1161    if matches!(
1162        event,
1163        Event::Mouse(mouse::Event::CursorMoved { .. })
1164            | Event::Touch(touch::Event::FingerMoved { .. })
1165    ) {
1166        cursor
1167            .observe()
1168            .position()
1169            .filter(|position| *position != last)
1170    } else {
1171        None
1172    }
1173}
1174
1175/// The content update that [`State::interact`] requests to be delegated to the
1176/// child widget.
1177#[derive(Debug)]
1178struct Content {
1179    /// The [`mouse::Cursor`] to pass to the child widget
1180    cursor: mouse::Cursor,
1181}
1182
1183/// The outcome of [`State::interact`]: the [`Shell`] effects to materialize and
1184/// whether the event must be delegated to the content.
1185#[derive(Debug, Default)]
1186struct Interact {
1187    /// The scroll notification to publish, if the scroll changed: the final
1188    /// state after the event, coalescing any intermediate changes
1189    scroll: Option<Scroll>,
1190
1191    /// Whether the event must be captured
1192    capture: bool,
1193
1194    /// Whether the layout must be invalidated
1195    invalidate_layout: bool,
1196
1197    /// Whether a redraw must be requested
1198    request_redraw: bool,
1199
1200    /// Whether the interaction stopped early: the event must not be delegated
1201    /// to the content, nor processed by [`State::update`]
1202    stop: bool,
1203
1204    /// If `Some`, the event must be delegated to the content with the given
1205    /// cursor and viewport
1206    content: Option<Content>,
1207}
1208
1209/// The outcome of [`State::update`]: the [`Shell`] effects to materialize
1210#[derive(Debug, Default)]
1211struct Update {
1212    /// The scroll notification to publish, if the scroll changed: the final
1213    /// state after the event, coalescing any intermediate changes
1214    scroll: Option<Scroll>,
1215
1216    /// Whether the event must be captured
1217    capture: bool,
1218
1219    /// Whether the layout must be invalidated
1220    invalidate_layout: bool,
1221
1222    /// Whether a redraw must be requested
1223    request_redraw: bool,
1224}
1225
1226#[derive(Debug, Clone)]
1227struct State {
1228    offset_y: Offset,
1229    offset_x: Offset,
1230    smooth_scroll: bool,
1231    target: Option<Target>,
1232    source: Option<Source>,
1233    segment: Segment,
1234    last_frame: Option<Instant>,
1235    last_translation: Vector,
1236    interaction: Interaction,
1237    keyboard_modifiers: keyboard::Modifiers,
1238    last_notified: Option<Viewport>,
1239    last_scrolled: Option<Instant>,
1240    is_scrollbar_visible: bool,
1241    last_status: Option<Status>,
1242    last_id: Option<widget::Id>,
1243}
1244
1245#[derive(Debug, Clone, Copy)]
1246struct Segment {
1247    start: Point,
1248    started: Instant,
1249    duration: f32,
1250    slope: f32,
1251}
1252
1253#[derive(Debug, Clone, Copy)]
1254enum Interaction {
1255    None,
1256    ScrollerGrabbed(Axis, f32),
1257    TouchScrolling(Point),
1258    AutoScrolling {
1259        origin: Point,
1260        current: Point,
1261        last_frame: Option<Instant>,
1262    },
1263    RailHeld(RailHeld),
1264}
1265
1266impl Interaction {
1267    /// The axis of the active scrollbar interaction — a scroller drag or a
1268    /// held rail — if any.
1269    fn axis(&self) -> Option<Axis> {
1270        match self {
1271            Interaction::ScrollerGrabbed(axis, _) => Some(*axis),
1272            Interaction::RailHeld(rail) => Some(rail.axis),
1273            _ => None,
1274        }
1275    }
1276
1277    /// Whether the user is interacting with a scrollbar: dragging a
1278    /// scroller or holding a rail pressed.
1279    fn scrollers_grabbed(&self) -> bool {
1280        self.axis().is_some()
1281    }
1282
1283    /// The axis and the fraction of the scroller grabbed, if a scroller is
1284    /// being dragged.
1285    fn scroller_grabbed(&self) -> Option<(Axis, f32)> {
1286        let Interaction::ScrollerGrabbed(axis, grabbed_at) = self else {
1287            return None;
1288        };
1289
1290        Some((*axis, *grabbed_at))
1291    }
1292}
1293
1294/// The state of a [`Scrollable`] whose rail is being held pressed.
1295///
1296/// A plain rail press scrolls one page (smoothly) and, after
1297/// [`State::RAIL_AUTOSCROLL_DELAY`], autoscrolls at a constant velocity
1298/// until the scroller reaches the pointer, mirroring Chromium's track
1299/// autoscroll (`cc::ScrollbarController`).
1300#[derive(Debug, Clone, Copy)]
1301struct RailHeld {
1302    /// The axis the rail was pressed on
1303    axis: Axis,
1304
1305    /// The direction of the autoscroll, in offset units (`+1.0` or
1306    /// `-1.0`), fixed when the rail was pressed
1307    direction: f32,
1308
1309    /// Which side of the scroller the rail was pressed on, in cursor
1310    /// units: `+1.0` below it, `-1.0` above it.
1311    ///
1312    /// This is independent of the anchor's mirroring, and selects the
1313    /// scroller edge the autoscroll stops at (its leading edge) and the
1314    /// track part that pauses it.
1315    cursor_direction: f32,
1316
1317    /// The autoscroll velocity, in pixels per second
1318    velocity: f32,
1319
1320    /// When the rail was pressed
1321    pressed_at: Instant,
1322
1323    /// The latest pointer position
1324    pointer: Point,
1325
1326    /// The last frame the autoscroll was stepped for, guarding against
1327    /// stepping twice for the same instant
1328    last_frame: Option<Instant>,
1329}
1330
1331/// The outcome of stepping a held rail's autoscroll.
1332#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1333enum RailStep {
1334    /// The offset moved; the scroll should be notified and redrawn
1335    Moved,
1336
1337    /// The autoscroll has not started yet (within the press delay); frames
1338    /// must keep coming
1339    Waiting,
1340
1341    /// Nothing can move until the pointer moves again
1342    Idle,
1343}
1344
1345/// The axis of the [`Scrollable`] content.
1346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1347pub(super) enum Axis {
1348    /// The horizontal axis.
1349    X,
1350
1351    /// The vertical axis.
1352    Y,
1353}
1354
1355impl Axis {
1356    /// The length of the given size along this axis.
1357    fn length(self, size: impl Into<Size>) -> f32 {
1358        let size = size.into();
1359        match self {
1360            Axis::X => size.width,
1361            Axis::Y => size.height,
1362        }
1363    }
1364
1365    /// The coordinate of the given `point` along this axis.
1366    fn coordinate(self, point: Point) -> f32 {
1367        match self {
1368            Axis::X => point.x,
1369            Axis::Y => point.y,
1370        }
1371    }
1372
1373    /// A [`Vector`] with the given `value` on this axis and `0.0` on the
1374    /// other.
1375    fn vector(self, value: f32) -> Vector {
1376        match self {
1377            Axis::X => Vector::new(value, 0.0),
1378            Axis::Y => Vector::new(0.0, value),
1379        }
1380    }
1381}
1382
1383impl Default for State {
1384    fn default() -> Self {
1385        Self {
1386            offset_y: Offset::Absolute(0.0),
1387            offset_x: Offset::Absolute(0.0),
1388            smooth_scroll: true,
1389            target: None,
1390            source: None,
1391            segment: Segment {
1392                start: Point::ORIGIN,
1393                started: Instant::now(),
1394                duration: 0.0,
1395                slope: 0.0,
1396            },
1397            last_frame: None,
1398            last_translation: Vector::ZERO,
1399            interaction: Interaction::None,
1400            keyboard_modifiers: keyboard::Modifiers::default(),
1401            last_notified: None,
1402            last_scrolled: None,
1403            is_scrollbar_visible: true,
1404            last_status: None,
1405            last_id: None,
1406        }
1407    }
1408}
1409
1410impl operation::Scrollable for State {
1411    fn snap_to(
1412        &mut self,
1413        offset: RelativeOffset<Option<f32>>,
1414        animation: Animation,
1415        bounds: Rectangle,
1416        content: Size,
1417    ) {
1418        State::snap_to(self, offset, animation, bounds, content, Source::Operation);
1419    }
1420
1421    fn scroll_to(
1422        &mut self,
1423        offset: AbsoluteOffset<Option<f32>>,
1424        animation: Animation,
1425        bounds: Rectangle,
1426        content: Size,
1427    ) {
1428        State::scroll_to(self, offset, animation, bounds, content, Source::Operation);
1429    }
1430
1431    fn scroll_by(
1432        &mut self,
1433        offset: AbsoluteOffset,
1434        animation: Animation,
1435        bounds: Rectangle,
1436        content: Size,
1437    ) {
1438        State::scroll_by(self, offset, animation, bounds, content);
1439    }
1440}
1441
1442/// An offset of a [`Scrollable`], in either absolute or relative units.
1443#[derive(Debug, Clone, Copy, PartialEq)]
1444pub enum Offset {
1445    /// An absolute offset, in pixels.
1446    Absolute(f32),
1447
1448    /// A relative offset, as a fraction of the scrollable range.
1449    Relative(f32),
1450}
1451
1452impl Offset {
1453    /// Returns whether this offset is snapped, i.e. relative.
1454    pub fn is_snapped(self) -> bool {
1455        matches!(self, Offset::Relative(_))
1456    }
1457
1458    fn absolute(self, viewport: f32, content: f32) -> f32 {
1459        match self {
1460            Offset::Absolute(absolute) => absolute.min((content - viewport).max(0.0)),
1461            Offset::Relative(percentage) => ((content - viewport) * percentage).max(0.0),
1462        }
1463    }
1464
1465    fn translation(self, viewport: f32, content: f32, alignment: Anchor) -> f32 {
1466        let offset = self.absolute(viewport, content);
1467
1468        match alignment {
1469            Anchor::Start => offset,
1470            Anchor::End => ((content - viewport).max(0.0) - offset).max(0.0),
1471        }
1472    }
1473}
1474
1475/// The target of an in-progress smooth scroll.
1476///
1477/// The offsets are what the scroll settles at, so relative (snapped)
1478/// offsets stay snapped.
1479#[derive(Debug, Clone, Copy)]
1480pub struct Target {
1481    /// The X offset of the target, in [`Offset`] units.
1482    pub x: Offset,
1483
1484    /// The Y offset of the target, in [`Offset`] units.
1485    pub y: Offset,
1486
1487    /// The resolution of the offsets at the current bounds, the point the
1488    /// running segment eases towards.
1489    pub destination: Vector,
1490}
1491
1492impl Target {
1493    /// Resolves the given offsets into a [`Target`].
1494    fn new(x: Offset, y: Offset, bounds: Rectangle, content: Size) -> Self {
1495        let destination = Vector::new(
1496            x.absolute(bounds.width, content.width),
1497            y.absolute(bounds.height, content.height),
1498        );
1499
1500        Self { x, y, destination }
1501    }
1502
1503    /// A [`Target`] that settles at the given absolute `destination`.
1504    fn absolute(destination: Vector) -> Self {
1505        Self {
1506            x: Offset::Absolute(destination.x),
1507            y: Offset::Absolute(destination.y),
1508            destination,
1509        }
1510    }
1511}
1512
1513/// The source of a scroll notification.
1514#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1515pub enum Source {
1516    /// The user scrolled with a mouse wheel or touchpad.
1517    Wheel,
1518
1519    /// The user scrolled by swiping with a touch.
1520    Touch,
1521
1522    /// The user scrolled by dragging the scrollbar's scroller.
1523    Scrollbar,
1524
1525    /// The user scrolled by auto-scrolling with the middle mouse button.
1526    AutoScroll,
1527
1528    /// A scroll operation (`scroll_to`, `snap_to` or `scroll_by`) scrolled.
1529    Operation,
1530
1531    /// A scroll [`Action`] scrolled.
1532    Action,
1533
1534    /// The size of the content changed.
1535    Content,
1536
1537    /// The size of the [`Scrollable`] changed.
1538    Resize,
1539}
1540
1541/// An event describing a scroll of a [`Scrollable`].
1542#[derive(Debug, Clone, Copy)]
1543pub struct Scroll {
1544    /// The [`Viewport`] of the [`Scrollable`].
1545    pub viewport: Viewport,
1546
1547    /// The original [`Viewport`] before the scroll event, if any.
1548    pub origin: Option<Viewport>,
1549
1550    /// The [`Source`] of the scroll.
1551    pub source: Source,
1552
1553    /// The [`Target`] of the scroll.
1554    pub target: Option<Target>,
1555}
1556
1557impl Scroll {
1558    /// Returns the [`Viewport`] that this scroll settles at.
1559    ///
1560    /// While a smooth scrolling animation is in progress, the returned
1561    /// [`Viewport`] carries the [`Target`]'s offsets — the offsets the
1562    /// scroll settles at — instead of the current ones.
1563    pub fn destination(&self) -> Viewport {
1564        Viewport {
1565            x: self
1566                .target
1567                .map(|target| target.x)
1568                .unwrap_or(self.viewport.x),
1569            y: self
1570                .target
1571                .map(|target| target.y)
1572                .unwrap_or(self.viewport.y),
1573            ..self.viewport
1574        }
1575    }
1576}
1577
1578/// The current [`Viewport`] of the [`Scrollable`].
1579#[derive(Debug, Clone, Copy)]
1580pub struct Viewport {
1581    /// The current X offset, in [`Offset`] units.
1582    pub x: Offset,
1583
1584    /// The current Y offset, in [`Offset`] units.
1585    pub y: Offset,
1586
1587    /// The bounds of the [`Scrollable`].
1588    pub bounds: Rectangle,
1589
1590    /// The size of the content of the [`Scrollable`].
1591    pub content: Size,
1592}
1593
1594impl Viewport {
1595    /// Returns the end of the [`Viewport`].
1596    pub fn end(&self) -> AbsoluteOffset {
1597        self.absolute_offset() + self.distance_to_end()
1598    }
1599
1600    /// Returns a new [`Viewport`] aligned with the coordinates of the
1601    /// given one.
1602    pub fn slide(self, other: Self) -> Self {
1603        Self {
1604            x: other.x,
1605            y: other.y,
1606            ..self
1607        }
1608    }
1609
1610    /// Returns the distance from the current scroll position to the end of
1611    /// the content, in pixels, per axis.
1612    ///
1613    /// This is the amount of content that can still be scrolled into view:
1614    /// it is zero when the content fits, or when the scroll is at the end.
1615    pub fn distance_to_end(&self) -> Vector {
1616        let AbsoluteOffset { x, y } = self.absolute_offset();
1617
1618        Vector::new(
1619            (self.content.width - self.bounds.width - x).max(0.0),
1620            (self.content.height - self.bounds.height - y).max(0.0),
1621        )
1622    }
1623
1624    /// Returns the [`AbsoluteOffset`] of the current [`Viewport`].
1625    pub fn absolute_offset(&self) -> AbsoluteOffset {
1626        let x = self.x.absolute(self.bounds.width, self.content.width);
1627        let y = self.y.absolute(self.bounds.height, self.content.height);
1628
1629        AbsoluteOffset { x, y }
1630    }
1631
1632    /// Returns the [`AbsoluteOffset`] of the current [`Viewport`], but with its
1633    /// alignment reversed.
1634    ///
1635    /// This method can be useful to switch the alignment of a [`Scrollable`]
1636    /// while maintaining its scrolling position.
1637    pub fn absolute_offset_reversed(&self) -> AbsoluteOffset {
1638        let AbsoluteOffset { x, y } = self.absolute_offset();
1639
1640        AbsoluteOffset {
1641            x: (self.content.width - self.bounds.width).max(0.0) - x,
1642            y: (self.content.height - self.bounds.height).max(0.0) - y,
1643        }
1644    }
1645
1646    /// Returns the [`RelativeOffset`] of the current [`Viewport`].
1647    pub fn relative_offset(&self) -> RelativeOffset {
1648        let AbsoluteOffset { x, y } = self.absolute_offset();
1649
1650        let x = x / (self.content.width - self.bounds.width);
1651        let y = y / (self.content.height - self.bounds.height);
1652
1653        RelativeOffset { x, y }
1654    }
1655}
1656
1657/// An action to perform in response to a [`Scroll`].
1658///
1659/// This is the return type of the [`Scrollable::on_scroll`] handler. It lets
1660/// the handler react to a scroll notification by driving the [`Scrollable`]
1661/// further, or by publishing a message.
1662#[derive(Debug)]
1663pub enum Action<Message> {
1664    /// Do nothing.
1665    None,
1666
1667    /// Scroll to the given [`AbsoluteOffset`], with the given [`Animation`].
1668    ///
1669    /// An axis set to `None` keeps its current position.
1670    ScrollTo(AbsoluteOffset<Option<f32>>, Animation),
1671
1672    /// Snap to the given [`RelativeOffset`], with the given [`Animation`].
1673    ///
1674    /// An axis set to `None` keeps its current position.
1675    SnapTo(RelativeOffset<Option<f32>>, Animation),
1676
1677    /// Publish the given message.
1678    Custom(Message),
1679}
1680
1681impl<Message> Action<Message> {
1682    fn perform(
1683        self,
1684        state: &mut State,
1685        bounds: Rectangle,
1686        content: Size,
1687        shell: &mut Shell<'_, Message>,
1688    ) {
1689        match self {
1690            Action::None => {}
1691            Action::ScrollTo(absolute_offset, animation) => {
1692                state.scroll_to(absolute_offset, animation, bounds, content, Source::Action);
1693            }
1694            Action::SnapTo(relative_offset, animation) => {
1695                state.snap_to(relative_offset, animation, bounds, content, Source::Action);
1696            }
1697            Action::Custom(message) => {
1698                shell.publish(message);
1699            }
1700        }
1701    }
1702}
1703
1704impl<Message> From<Message> for Action<Message> {
1705    fn from(message: Message) -> Self {
1706        Self::Custom(message)
1707    }
1708}
1709
1710impl<Message> From<Option<Message>> for Action<Message> {
1711    fn from(message: Option<Message>) -> Self {
1712        match message {
1713            Some(message) => Self::Custom(message),
1714            None => Self::None,
1715        }
1716    }
1717}
1718
1719/// The distance (in logical pixels) scrolled per wheel line.
1720///
1721/// Chromium scrolls a fixed 120 CSS pixels per classic wheel notch,
1722/// independent of the page's line height: the OS delta is normalized to
1723/// 120 units (`ui::MouseWheelEvent::kWheelDelta`) and passed through 1:1.
1724///
1725/// This value assumes the platform reports one line per notch (e.g. X11).
1726/// On platforms that report three lines per notch, `40.0` (Chromium's
1727/// `cc::kPixelsPerLineStep`) is the equivalent value.
1728const WHEEL_PX_PER_LINE: f32 = 120.0;
1729
1730// The smooth scrolling behavior below (the easing curve, the animation
1731// durations, and the velocity-preserving retargeting) is derived from
1732// the Chromium project's wheel scroll animation
1733// (`cc/animation/scroll_offset_animation_curve.{h,cc}`), which is
1734// licensed under the BSD 3-Clause license:
1735// <https://chromium.googlesource.com/chromium/src/+/main/LICENSE>
1736
1737/// The control points of the smooth scrolling easing curve.
1738///
1739/// This is the standard ease-in-out cubic bezier, as used by Chromium for
1740/// wheel scrolling: the scroll starts from rest, peaks mid-way, and
1741/// settles on the target at rest.
1742const SMOOTH_SCROLL_BEZIER_X1: f32 = 0.42;
1743const SMOOTH_SCROLL_BEZIER_X2: f32 = 0.58;
1744
1745/// The divisor of the smooth scrolling animation duration, matching
1746/// Chromium's.
1747const SMOOTH_SCROLL_DURATION_DIVISOR: f32 = 60.0;
1748
1749/// The delay (in seconds) between a wheel event and the next drawn frame.
1750///
1751/// Wheel events are processed between frames, and the smooth scrolling
1752/// animation is only stepped when the next frame is drawn. Starting the
1753/// animation one nominal frame *before* the event, where a nominal frame
1754/// is one unit of `SMOOTH_SCROLL_DURATION_DIVISOR`, ensures that the
1755/// first drawn frame already shows some progress, instead of repeating
1756/// the previous one.
1757///
1758/// A full frame is used rather than the mean delay of half a frame: it
1759/// guarantees that the first drawn frame visibly moves, even when the
1760/// event arrives right after a frame. The animation then settles one
1761/// frame early, which is imperceptible since the curve ends at rest.
1762const SMOOTH_SCROLL_FRAME_DELAY: f32 = 1.0 / SMOOTH_SCROLL_DURATION_DIVISOR;
1763
1764/// The distances (in pixels) at which the smooth scrolling duration ramp
1765/// starts and ends.
1766const SMOOTH_SCROLL_DURATION_RAMP_START: f32 = WHEEL_PX_PER_LINE;
1767const SMOOTH_SCROLL_DURATION_RAMP_END: f32 = 480.0;
1768
1769/// The shortest and longest smooth scrolling animation durations, in
1770/// `SMOOTH_SCROLL_DURATION_DIVISOR` units, matching Chromium's.
1771///
1772/// The duration is *inversely* proportional to the distance within these
1773/// bounds: short scrolls get a longer (softer) animation, while long
1774/// scrolls get a shorter (snappier) one.
1775const SMOOTH_SCROLL_DURATION_MIN: f32 = 6.0;
1776const SMOOTH_SCROLL_DURATION_MAX: f32 = 12.0;
1777
1778/// The factor applied to the time it would take to cover the new distance
1779/// at the current velocity when retargeting a running animation, matching
1780/// Chromium's.
1781///
1782/// Bounding the new duration by this keeps a fast scroll from "rubber
1783/// banding" when a small new delta is added at high velocity.
1784const SMOOTH_SCROLL_RETARGET_VELOCITY_BOUND: f32 = 2.5;
1785
1786/// The clamp for the initial slope of a retargeted animation, matching
1787/// Chromium's.
1788const SMOOTH_SCROLL_SLOPE_CLAMP: f32 = 1000.0;
1789
1790// The held-rail behavior below (the press delay, the autoscroll
1791// velocity, and the page step) is derived from the Chromium
1792// project's compositor scrollbar controller (`cc/input/
1793// scrollbar_controller.{h,cc}` and `cc/input/scrollbar.h`), which is
1794// licensed under the BSD 3-Clause license:
1795// <https://chromium.googlesource.com/chromium/src/+/main/LICENSE>
1796
1797/// The delay between a rail press and the start of the held-rail
1798/// autoscroll, matching Chromium's `cc::kInitialAutoscrollTimerDelay`.
1799///
1800/// During the delay, only the initial page step (animated like a wheel
1801/// scroll) is applied; a quick click therefore scrolls exactly one page.
1802const RAIL_AUTOSCROLL_DELAY: Duration = Duration::from_millis(250);
1803
1804/// The factor converting a rail page step into the held-rail autoscroll
1805/// velocity, matching Chromium's `cc::kAutoscrollMultiplier`.
1806///
1807/// Chromium's main thread autoscroll applies the page step every 50 ms;
1808/// the equivalent constant velocity is the step times 20.
1809const RAIL_AUTOSCROLL_MULTIPLIER: f32 = 20.0;
1810
1811/// The fraction of the viewport covered by a rail click's initial page
1812/// step, matching Chromium's `cc::kMinFractionToStepWhenPaging` (the
1813/// non-Mac page step).
1814const RAIL_PAGE_STEP_FRACTION: f32 = 0.875;
1815
1816/// The distance (in pixels) within which the auto-scroll (middle mouse
1817/// button) movement is ignored.
1818const AUTOSCROLL_DEADZONE: f32 = 20.0;
1819
1820/// The exponent of the auto-scroll velocity curve, which makes the
1821/// auto-scroll accelerate with the distance from the origin.
1822const AUTOSCROLL_SMOOTHNESS: f32 = 1.5;
1823
1824impl State {
1825    /// The distance (in pixels) of a rail click's initial page step, given
1826    /// the length of the scrollable viewport.
1827    fn rail_page_step(viewport: f32) -> f32 {
1828        (viewport * RAIL_PAGE_STEP_FRACTION).max(1.0)
1829    }
1830
1831    fn new() -> Self {
1832        State::default()
1833    }
1834
1835    fn scroll(&mut self, delta: Vector<f32>, bounds: Rectangle, content: Size) {
1836        self.cancel();
1837
1838        if bounds.height < content.height {
1839            self.offset_y = Offset::Absolute(
1840                (self.offset_y.absolute(bounds.height, content.height) + delta.y)
1841                    .clamp(0.0, content.height - bounds.height),
1842            );
1843        }
1844
1845        if bounds.width < content.width {
1846            self.offset_x = Offset::Absolute(
1847                (self.offset_x.absolute(bounds.width, content.width) + delta.x)
1848                    .clamp(0.0, content.width - bounds.width),
1849            );
1850        }
1851    }
1852
1853    /// Moves the *target* scroll offset by `delta`, for smooth scrolling.
1854    ///
1855    /// The delta is accumulated onto the pending target, if any, so that
1856    /// quick wheel movements do not lose their (not yet scrolled) distance;
1857    /// the target is then animated via [`State::scroll_smoothly_to`].
1858    fn scroll_smoothly(
1859        &mut self,
1860        delta: Vector<f32>,
1861        bounds: Rectangle,
1862        content: Size,
1863        now: Instant,
1864    ) {
1865        let current = Point::new(
1866            self.offset_x.absolute(bounds.width, content.width),
1867            self.offset_y.absolute(bounds.height, content.height),
1868        );
1869
1870        // Accumulate onto the pending target, if any, so that quick wheel
1871        // movements do not lose their (not yet scrolled) distance
1872        let target = match self.target {
1873            Some(target) => Vector::new(
1874                Self::clamp_offset(target.destination.x + delta.x, bounds.width, content.width),
1875                Self::clamp_offset(
1876                    target.destination.y + delta.y,
1877                    bounds.height,
1878                    content.height,
1879                ),
1880            ),
1881            None => Vector::new(
1882                Self::clamp_offset(current.x + delta.x, bounds.width, content.width),
1883                Self::clamp_offset(current.y + delta.y, bounds.height, content.height),
1884            ),
1885        };
1886
1887        self.scroll_smoothly_to(Target::absolute(target), bounds, content, now);
1888    }
1889
1890    /// Scrolls smoothly to the given `target`.
1891    ///
1892    /// The target replaces any pending one. If a segment is already running,
1893    /// it is retargeted from the current position and velocity, so that
1894    /// scrolling flows instead of restarting.
1895    ///
1896    /// The target is then eased towards on each frame, via [`State::step`],
1897    /// with an ease-in-out animation whose duration depends on the distance:
1898    /// short scrolls get a longer (softer) animation, while long scrolls get a
1899    /// shorter (snappier) one.
1900    fn scroll_smoothly_to(
1901        &mut self,
1902        target: Target,
1903        bounds: Rectangle,
1904        content: Size,
1905        now: Instant,
1906    ) {
1907        // The scroll is requested between frames; start the animation one
1908        // nominal frame early so that the first drawn frame already shows
1909        // progress
1910        let now = now - Duration::from_secs_f32(SMOOTH_SCROLL_FRAME_DELAY);
1911
1912        let current = Point::new(
1913            self.offset_x.absolute(bounds.width, content.width),
1914            self.offset_y.absolute(bounds.height, content.height),
1915        );
1916
1917        // Nothing to animate: the content fits, or we're already at the target
1918        if target.destination.x == current.x && target.destination.y == current.y {
1919            self.offset_x = target.x;
1920            self.offset_y = target.y;
1921            self.target = None;
1922            self.last_frame = None;
1923            return;
1924        }
1925
1926        let Some(ongoing) = self.target else {
1927            // A new scroll run: start a fresh segment from rest at the
1928            // current position
1929            let distance = (target.destination.x - current.x)
1930                .abs()
1931                .max((target.destination.y - current.y).abs());
1932
1933            self.target = Some(target);
1934            self.segment = Segment {
1935                start: current,
1936                started: now,
1937                duration: Self::smooth_scroll_duration(distance),
1938                slope: 0.0,
1939            };
1940            self.last_frame = Some(now);
1941            return;
1942        };
1943
1944        // The target is unchanged: keep the running segment as is
1945        if ongoing.destination == target.destination {
1946            return;
1947        }
1948
1949        self.retarget(target, now);
1950    }
1951
1952    /// Retargets the running segment towards `target`, from the current
1953    /// position and velocity, so that scrolling flows instead of restarting.
1954    fn retarget(&mut self, target: Target, now: Instant) {
1955        // Retarget the running segment from the current position, preserving
1956        // the current velocity
1957        let start = self.animated_position(now);
1958        let velocity = self.animated_velocity(now);
1959        let new = Vector::new(
1960            target.destination.x - start.x,
1961            target.destination.y - start.y,
1962        );
1963
1964        // The signed dimension with the largest magnitude, like Chromium's
1965        let max_dimension = if new.x.abs() > new.y.abs() {
1966            new.x
1967        } else {
1968            new.y
1969        };
1970
1971        // The new duration, bounded so that a small delta added at high
1972        // velocity does not "rubber band"; a bound with the wrong sign means
1973        // the new delta is against the current motion, and does not apply
1974        let mut duration = Self::smooth_scroll_duration(max_dimension.abs());
1975        if velocity.abs() > 0.01 {
1976            let bound = SMOOTH_SCROLL_RETARGET_VELOCITY_BOUND * max_dimension / velocity;
1977
1978            if bound > 0.0 {
1979                duration = duration.min(bound);
1980            }
1981        }
1982
1983        if max_dimension.abs() < 0.01 || duration < 0.01 {
1984            // The new target is right on top of us: end the animation now
1985            self.offset_x = target.x;
1986            self.offset_y = target.y;
1987            self.target = None;
1988            self.last_frame = None;
1989            return;
1990        }
1991
1992        // Adjust the initial slope of the new segment so that it starts with
1993        // the current velocity
1994        let slope = (velocity * (duration / max_dimension))
1995            .clamp(-SMOOTH_SCROLL_SLOPE_CLAMP, SMOOTH_SCROLL_SLOPE_CLAMP);
1996
1997        self.target = Some(target);
1998        self.segment = Segment {
1999            start,
2000            started: now,
2001            duration,
2002            slope,
2003        };
2004        self.last_frame = Some(now);
2005    }
2006
2007    /// Steps the smooth scrolling animation forward, towards the target
2008    /// offset.
2009    ///
2010    /// Returns `true` if the animation is still in progress.
2011    fn step(&mut self, now: Instant, bounds: Rectangle, content: Size) -> bool {
2012        let Some(target) = self.target else {
2013            return false;
2014        };
2015
2016        // The bounds and content may have changed while the animation is
2017        // running; re-resolve the target, and retarget if it moved
2018        let resolved = Target::new(target.x, target.y, bounds, content);
2019        if resolved.destination != target.destination {
2020            self.retarget(resolved, now);
2021        }
2022
2023        let Some(target) = self.target else {
2024            return false;
2025        };
2026        let started = self.segment.started;
2027
2028        let t = (now - started).as_secs_f32();
2029        let progress = (t / self.segment.duration).clamp(0.0, 1.0);
2030
2031        if progress >= 1.0 {
2032            // Settled exactly on the target, keeping relative (snapped)
2033            // offsets
2034            self.offset_x = target.x;
2035            self.offset_y = target.y;
2036            self.target = None;
2037            self.last_frame = None;
2038            return false;
2039        }
2040
2041        let bez = Self::smooth_scroll_progress(progress, self.segment.slope);
2042        self.offset_x = Offset::Absolute(
2043            self.segment.start.x + (target.destination.x - self.segment.start.x) * bez,
2044        );
2045        self.offset_y = Offset::Absolute(
2046            self.segment.start.y + (target.destination.y - self.segment.start.y) * bez,
2047        );
2048
2049        self.last_frame = Some(now);
2050        true
2051    }
2052
2053    /// Cancels any in-progress smooth scrolling animation, keeping the current
2054    /// offsets.
2055    ///
2056    /// This ensures that direct manipulation (scrollbar drags, touch scrolling,
2057    /// auto-scrolling, programmatic scrolling) always takes priority over a
2058    /// pending wheel animation.
2059    fn cancel(&mut self) {
2060        self.target = None;
2061        self.last_frame = None;
2062    }
2063
2064    /// Clamps an absolute scroll offset to the range in which the given content
2065    /// can be scrolled inside the given viewport.
2066    fn clamp_offset(offset: f32, viewport: f32, content: f32) -> f32 {
2067        if content > viewport {
2068            offset.clamp(0.0, content - viewport)
2069        } else {
2070            0.0
2071        }
2072    }
2073
2074    /// The animated position at `now`, or [`Point::ORIGIN`] if a segment is
2075    /// not running.
2076    fn animated_position(&self, now: Instant) -> Point {
2077        let Some(target) = self.target else {
2078            return Point::ORIGIN;
2079        };
2080
2081        let started = self.segment.started;
2082
2083        let t = (now - started).as_secs_f32();
2084        let progress = (t / self.segment.duration).clamp(0.0, 1.0);
2085        let bez = Self::smooth_scroll_progress(progress, self.segment.slope);
2086
2087        Point::new(
2088            self.segment.start.x + (target.destination.x - self.segment.start.x) * bez,
2089            self.segment.start.y + (target.destination.y - self.segment.start.y) * bez,
2090        )
2091    }
2092
2093    /// The animated velocity at `now`, in pixels per second along the
2094    /// segment's largest dimension, or `0.0` if a segment is not running.
2095    fn animated_velocity(&self, now: Instant) -> f32 {
2096        let Some(target) = self.target else {
2097            return 0.0;
2098        };
2099
2100        let started = self.segment.started;
2101
2102        let t = (now - started).as_secs_f32();
2103        let progress = (t / self.segment.duration).clamp(0.0, 1.0);
2104
2105        if progress >= 1.0 {
2106            return 0.0;
2107        }
2108
2109        let dx = target.destination.x - self.segment.start.x;
2110        let dy = target.destination.y - self.segment.start.y;
2111        let max_dimension = if dx.abs() > dy.abs() { dx } else { dy };
2112
2113        Self::smooth_scroll_curve_slope(progress, self.segment.slope) * max_dimension
2114            / self.segment.duration
2115    }
2116
2117    /// The duration (in seconds) of a smooth scrolling segment covering the
2118    /// given distance (in pixels).
2119    ///
2120    /// The duration is inversely proportional to the distance within a ramp:
2121    /// short scrolls get a longer (softer) animation, while long scrolls get a
2122    /// shorter (snappier) one.
2123    fn smooth_scroll_duration(distance: f32) -> f32 {
2124        let slope = (SMOOTH_SCROLL_DURATION_MIN - SMOOTH_SCROLL_DURATION_MAX)
2125            / (SMOOTH_SCROLL_DURATION_RAMP_END - SMOOTH_SCROLL_DURATION_RAMP_START);
2126        let offset = SMOOTH_SCROLL_DURATION_MAX - SMOOTH_SCROLL_DURATION_RAMP_START * slope;
2127
2128        (offset + distance * slope).clamp(SMOOTH_SCROLL_DURATION_MIN, SMOOTH_SCROLL_DURATION_MAX)
2129            / SMOOTH_SCROLL_DURATION_DIVISOR
2130    }
2131
2132    /// The progress of the smooth scrolling easing curve at the given time
2133    /// progress (in `[0, 1]`), with the given initial slope.
2134    ///
2135    /// The curve is an ease-in-out cubic bezier whose initial control point is
2136    /// scaled by `slope` (a slope of `0.0` is the plain ease-in-out curve).
2137    fn smooth_scroll_progress(time: f32, slope: f32) -> f32 {
2138        if time <= 0.0 {
2139            return 0.0;
2140        }
2141
2142        if time >= 1.0 {
2143            return 1.0;
2144        }
2145
2146        let s = Self::bezier_solve(time);
2147        let y1 = SMOOTH_SCROLL_BEZIER_X1 * slope;
2148        let os = 1.0 - s;
2149
2150        // y(s), with y2 = 1
2151        3.0 * y1 * s * os * os + 3.0 * s * s * os + s * s * s
2152    }
2153
2154    /// The slope of the smooth scrolling easing curve (dy/dx) at the given
2155    /// time progress (in `[0, 1]`), with the given initial slope.
2156    fn smooth_scroll_curve_slope(time: f32, slope: f32) -> f32 {
2157        let s = if time <= 0.0 {
2158            0.0
2159        } else if time >= 1.0 {
2160            1.0
2161        } else {
2162            Self::bezier_solve(time)
2163        };
2164
2165        let x1 = SMOOTH_SCROLL_BEZIER_X1;
2166        let x2 = SMOOTH_SCROLL_BEZIER_X2;
2167        let y1 = x1 * slope;
2168        let os = 1.0 - s;
2169
2170        // x'(s) and y'(s), with y2 = 1
2171        let x_prime =
2172            3.0 * x1 * os * (1.0 - 3.0 * s) + 3.0 * x2 * s * (2.0 - 3.0 * s) + 3.0 * s * s;
2173        let y_prime = 3.0 * y1 * os * (1.0 - 3.0 * s) + 3.0 * s * (2.0 - 3.0 * s) + 3.0 * s * s;
2174
2175        y_prime / x_prime
2176    }
2177
2178    /// Solves `x(s) = time` for `s` in `[0, 1]`, for the smooth scrolling
2179    /// bezier's x-axis.
2180    ///
2181    /// The x-axis is strictly increasing for the control points used here, so
2182    /// bisection converges unconditionally.
2183    fn bezier_solve(time: f32) -> f32 {
2184        let x1 = SMOOTH_SCROLL_BEZIER_X1;
2185        let x2 = SMOOTH_SCROLL_BEZIER_X2;
2186
2187        let x = |s: f32| {
2188            let os = 1.0 - s;
2189            3.0 * x1 * s * os * os + 3.0 * x2 * s * s * os + s * s * s
2190        };
2191
2192        let mut low = 0.0;
2193        let mut high = 1.0;
2194
2195        for _ in 0..40 {
2196            let mid = (low + high) / 2.0;
2197
2198            if x(mid) < time {
2199                low = mid;
2200            } else {
2201                high = mid;
2202            }
2203        }
2204
2205        (low + high) / 2.0
2206    }
2207
2208    /// Builds a [`Scroll`] notification for the current viewport, if it changed
2209    /// since the last one, and records that the user scrolled.
2210    fn notify_scroll(&mut self, bounds: Rectangle, content: Size) -> Option<Scroll> {
2211        let notification = self.notify_viewport(bounds, content);
2212
2213        if notification.is_some() {
2214            self.last_scrolled = Some(Instant::now());
2215        }
2216
2217        notification
2218    }
2219
2220    /// Builds a [`Scroll`] notification for the current viewport, if it changed
2221    /// since the last one, recording it so that it is not reported again.
2222    fn notify_viewport(&mut self, bounds: Rectangle, content: Size) -> Option<Scroll> {
2223        if content.width <= bounds.width && content.height <= bounds.height {
2224            return None;
2225        }
2226
2227        let viewport = Viewport {
2228            x: self.offset_x,
2229            y: self.offset_y,
2230            bounds,
2231            content,
2232        };
2233
2234        // Don't publish redundant viewports to shell
2235        if let Some(last_notified) = self.last_notified {
2236            let last_absolute_offset = last_notified.absolute_offset();
2237            let current_absolute_offset = viewport.absolute_offset();
2238
2239            let unchanged =
2240                |a: f32, b: f32| (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan());
2241
2242            if last_notified.bounds == bounds
2243                && last_notified.content == content
2244                && unchanged(last_absolute_offset.x, current_absolute_offset.x)
2245                && unchanged(last_absolute_offset.y, current_absolute_offset.y)
2246            {
2247                return None;
2248            }
2249        }
2250
2251        // The notification's source: the scroll in progress or pending, or,
2252        // when the scroll position changed on its own, what changed in the
2253        // layout
2254        let source = if self.target.is_some() {
2255            // An in-flight animation: report the segment's source, keeping it
2256            // so that the settle notification reports it too
2257            self.source
2258        } else {
2259            // A settled or pending scroll: consume the source
2260            self.source.take()
2261        }
2262        .unwrap_or_else(|| {
2263            if let Some(last_notified) = self.last_notified {
2264                if last_notified.content != content {
2265                    Source::Content
2266                } else {
2267                    Source::Resize
2268                }
2269            } else {
2270                // The first notification: the content was laid out
2271                Source::Content
2272            }
2273        });
2274
2275        let origin = self.last_notified;
2276        self.last_notified = Some(viewport);
2277
2278        Some(Scroll {
2279            viewport,
2280            origin,
2281            source,
2282            target: self.target,
2283        })
2284    }
2285
2286    /// Jumps the scroll to the given `percentage` along the given axis,
2287    /// snapping it to the nearest logical position.
2288    fn scroll_to_percentage(
2289        &mut self,
2290        axis: Axis,
2291        percentage: f32,
2292        bounds: Rectangle,
2293        content: Size,
2294    ) {
2295        self.cancel();
2296
2297        match axis {
2298            Axis::X => self.offset_x = Offset::Relative(percentage.clamp(0.0, 1.0)),
2299            Axis::Y => self.offset_y = Offset::Relative(percentage.clamp(0.0, 1.0)),
2300        }
2301
2302        self.unsnap(axis, bounds, content);
2303    }
2304
2305    /// Presses the rail of the given axis at `cursor_position`, starting
2306    /// the held-rail autoscroll.
2307    ///
2308    /// This mirrors Chromium's track press (`cc::ScrollbarController::
2309    /// HandlePointerDown`): a page step is applied immediately, animated
2310    /// like a wheel scroll (or immediately when smooth scrolling is
2311    /// disabled), and, while the button is held, a constant-velocity
2312    /// autoscroll runs after a delay until the scroller reaches the
2313    /// pointer (stepped on each frame, via [`Self::step_rail`]).
2314    fn press_rail(
2315        &mut self,
2316        axis: Axis,
2317        scrollbar: &internals::Scrollbar,
2318        cursor_position: Point,
2319        smooth: bool,
2320        now: Instant,
2321        bounds: Rectangle,
2322        content: Size,
2323    ) {
2324        let Some(scroller) = scrollbar.scroller else {
2325            return;
2326        };
2327
2328        // Which side of the scroller was pressed, in cursor space: valid
2329        // for both anchors, as the scroller's bounds already account for
2330        // the `Anchor::End` mirroring
2331        let cursor_direction =
2332            if axis.coordinate(cursor_position) < axis.coordinate(scroller.bounds.position()) {
2333                -1.0
2334            } else {
2335                1.0
2336            };
2337
2338        // The offset at which the scroller's leading edge (toward the
2339        // pointer) reaches the pointer: a `grabbed_at` of `0.0` is the
2340        // scroller's start edge, `1.0` its end edge
2341        let grabbed_at = if cursor_direction < 0.0 { 0.0 } else { 1.0 };
2342        let stop = Offset::Relative(scrollbar.scroll_percentage(axis, grabbed_at, cursor_position))
2343            .absolute(axis.length(bounds), axis.length(content));
2344
2345        // The direction, in offset units: the offset-space sign of the stop
2346        // line, which accounts for the anchor's mirroring
2347        let direction = (stop - self.axis_offset(axis, bounds, content)).signum();
2348        if direction == 0.0 {
2349            // The pointer is on the stop line: there is nothing to scroll
2350            return;
2351        }
2352
2353        let page_step = Self::rail_page_step(axis.length(bounds));
2354
2355        // The initial page step: animated like a wheel scroll, or applied
2356        // immediately when smooth scrolling is disabled
2357        if smooth {
2358            self.scroll_smoothly(axis.vector(direction * page_step), bounds, content, now);
2359        } else {
2360            self.scroll(axis.vector(direction * page_step), bounds, content);
2361        }
2362
2363        self.interaction = Interaction::RailHeld(RailHeld {
2364            axis,
2365            direction,
2366            cursor_direction,
2367            velocity: page_step * RAIL_AUTOSCROLL_MULTIPLIER,
2368            pressed_at: now,
2369            pointer: cursor_position,
2370            last_frame: None,
2371        });
2372    }
2373
2374    /// Steps the held-rail autoscroll of the given axis on a frame.
2375    ///
2376    /// Returns `None` if the scrollbar disappeared while the rail was held,
2377    /// in which case the interaction must be dropped (like Chromium does
2378    /// when its scrollbar is unregistered); otherwise, the outcome of the
2379    /// step.
2380    fn step_rail(
2381        &mut self,
2382        rail: &mut RailHeld,
2383        now: Instant,
2384        scrollbar: Option<&internals::Scrollbar>,
2385        bounds: Rectangle,
2386        content: Size,
2387    ) -> Option<RailStep> {
2388        let axis = rail.axis;
2389
2390        let scrollbar = scrollbar?;
2391
2392        // The autoscroll starts after the press delay, while the initial
2393        // page step is still easing
2394        if now - rail.pressed_at < RAIL_AUTOSCROLL_DELAY {
2395            return Some(RailStep::Waiting);
2396        }
2397
2398        // `last_frame` guards against stepping twice for the same instant
2399        if rail.last_frame == Some(now) {
2400            return Some(RailStep::Waiting);
2401        }
2402
2403        // The time since the last step; the first step starts from the
2404        // nominal start of the autoscroll, not from the press
2405        let last_frame = rail.last_frame;
2406        rail.last_frame = Some(now);
2407        let time_delta = last_frame.map_or(
2408            (now - rail.pressed_at) - RAIL_AUTOSCROLL_DELAY,
2409            |last_frame| now - last_frame,
2410        );
2411        if time_delta.is_zero() {
2412            return Some(RailStep::Waiting);
2413        }
2414
2415        let scroller = scrollbar.scroller?;
2416
2417        let current = self.axis_offset(axis, bounds, content);
2418
2419        // The offset at which the scroller's leading edge (the edge on the
2420        // pressed side, in cursor space) reaches the pointer
2421        let grabbed_at = if rail.cursor_direction > 0.0 {
2422            1.0
2423        } else {
2424            0.0
2425        };
2426        let stop = Offset::Relative(scrollbar.scroll_percentage(axis, grabbed_at, rail.pointer))
2427            .absolute(axis.length(bounds), axis.length(content));
2428
2429        // The autoscroll pauses while the pointer is on the rail but no
2430        // longer on the pressed side of the (moving) scroller, and
2431        // continues while the pointer is off the rail, mirroring Chromium's
2432        // behavior when the pointer leaves the scrollbar layer
2433        let on_pressed_side = if rail.cursor_direction > 0.0 {
2434            axis.coordinate(rail.pointer)
2435                > axis.coordinate(scroller.bounds.position()) + axis.length(scroller.bounds)
2436        } else {
2437            axis.coordinate(rail.pointer) < axis.coordinate(scroller.bounds.position())
2438        };
2439        if scrollbar.total_bounds.contains(rail.pointer) && !on_pressed_side {
2440            return Some(RailStep::Idle);
2441        }
2442
2443        // ... and it stops while the scroller's leading edge has reached
2444        // the pointer
2445        let remaining = if rail.direction > 0.0 {
2446            stop - current
2447        } else {
2448            current - stop
2449        };
2450        if remaining <= 0.0 {
2451            return Some(RailStep::Idle);
2452        }
2453
2454        // A constant-velocity step, clamped so that the scroller does not
2455        // cross the pointer within the frame
2456        let delta = rail.direction * (rail.velocity * time_delta.as_secs_f32()).min(remaining);
2457
2458        self.scroll(axis.vector(delta), bounds, content);
2459
2460        Some(if self.axis_offset(axis, bounds, content) != current {
2461            RailStep::Moved
2462        } else {
2463            // The step was clamped away (e.g. at the end of the content):
2464            // nothing can move until the pointer moves again
2465            RailStep::Idle
2466        })
2467    }
2468
2469    /// Snaps the scroll to the given [`RelativeOffset`], with the given
2470    /// [`Animation`].
2471    fn snap_to(
2472        &mut self,
2473        offset: RelativeOffset<Option<f32>>,
2474        animation: Animation,
2475        bounds: Rectangle,
2476        content: Size,
2477        source: Source,
2478    ) {
2479        self.source = Some(source);
2480
2481        if !self.should_scroll_smoothly(animation) {
2482            self.cancel();
2483
2484            if let Some(x) = offset.x {
2485                self.offset_x = Offset::Relative(x.clamp(0.0, 1.0));
2486            }
2487
2488            if let Some(y) = offset.y {
2489                self.offset_y = Offset::Relative(y.clamp(0.0, 1.0));
2490            }
2491
2492            return;
2493        }
2494
2495        // Snap the targeted axes relative to the content, and keep the
2496        // current offsets of the axes the snap does not target
2497        let x = offset
2498            .x
2499            .map(|x| Offset::Relative(x.clamp(0.0, 1.0)))
2500            .unwrap_or(self.offset_x);
2501        let y = offset
2502            .y
2503            .map(|y| Offset::Relative(y.clamp(0.0, 1.0)))
2504            .unwrap_or(self.offset_y);
2505
2506        self.scroll_smoothly_to(
2507            Target::new(x, y, bounds, content),
2508            bounds,
2509            content,
2510            Instant::now(),
2511        );
2512    }
2513
2514    /// Scrolls to the given [`AbsoluteOffset`], with the given [`Animation`].
2515    fn scroll_to(
2516        &mut self,
2517        offset: AbsoluteOffset<Option<f32>>,
2518        animation: Animation,
2519        bounds: Rectangle,
2520        content: Size,
2521        source: Source,
2522    ) {
2523        self.source = Some(source);
2524
2525        if !self.should_scroll_smoothly(animation) {
2526            self.cancel();
2527
2528            if let Some(x) = offset.x {
2529                self.offset_x = Offset::Absolute(x.max(0.0));
2530            }
2531
2532            if let Some(y) = offset.y {
2533                self.offset_y = Offset::Absolute(y.max(0.0));
2534            }
2535
2536            return;
2537        }
2538
2539        // Scroll the targeted axes to their clamped absolute offsets, and
2540        // keep the current offsets of the axes the scroll does not target
2541        let x = offset
2542            .x
2543            .map(|x| Offset::Absolute(Self::clamp_offset(x, bounds.width, content.width)))
2544            .unwrap_or(self.offset_x);
2545        let y = offset
2546            .y
2547            .map(|y| Offset::Absolute(Self::clamp_offset(y, bounds.height, content.height)))
2548            .unwrap_or(self.offset_y);
2549
2550        self.scroll_smoothly_to(
2551            Target::new(x, y, bounds, content),
2552            bounds,
2553            content,
2554            Instant::now(),
2555        );
2556    }
2557
2558    /// Scrolls by the provided [`AbsoluteOffset`], with the given
2559    /// [`Animation`].
2560    fn scroll_by(
2561        &mut self,
2562        offset: AbsoluteOffset,
2563        animation: Animation,
2564        bounds: Rectangle,
2565        content: Size,
2566    ) {
2567        self.source = Some(Source::Operation);
2568
2569        let delta = Vector::new(offset.x, offset.y);
2570
2571        if self.should_scroll_smoothly(animation) {
2572            self.scroll_smoothly(delta, bounds, content, Instant::now());
2573        } else {
2574            self.scroll(delta, bounds, content);
2575        }
2576    }
2577
2578    /// Whether the given [`Animation`] requests a smooth scroll, given the
2579    /// widget's smooth scrolling setting.
2580    fn should_scroll_smoothly(&self, animation: Animation) -> bool {
2581        match animation {
2582            Animation::Auto => self.smooth_scroll,
2583            Animation::Instant => false,
2584            Animation::Smooth => true,
2585        }
2586    }
2587
2588    /// Handles the interaction of the user with the [`Scrollable`], up to (but
2589    /// not including) the delegation of the event to the content.
2590    ///
2591    /// Returns the [`Interact`] effects to materialize on the [`Shell`] and
2592    /// whether the event must be delegated to the content.
2593    fn interact(
2594        &mut self,
2595        event: &Event,
2596        bounds: Rectangle,
2597        content: Size,
2598        cursor: mouse::Cursor,
2599        cursor_over_scrollable: Option<Point>,
2600        scrollbars: &Scrollbars,
2601        mouse_over_scrollbar: Option<Axis>,
2602        direction: Direction,
2603        smooth_scroll: bool,
2604        click_to_scroll: bool,
2605    ) -> Interact {
2606        let mut interact = Interact::default();
2607
2608        // Clear the scroll transaction, if the user is no longer scrolling
2609        if let Some(last_scrolled) = self.last_scrolled {
2610            let clear_transaction = match event {
2611                Event::Mouse(
2612                    mouse::Event::ButtonPressed(_)
2613                    | mouse::Event::ButtonReleased(_)
2614                    | mouse::Event::CursorLeft,
2615                ) => true,
2616                Event::Mouse(mouse::Event::CursorMoved { .. }) => {
2617                    last_scrolled.elapsed() > Duration::from_millis(100)
2618                }
2619                _ => last_scrolled.elapsed() > Duration::from_millis(1500),
2620            };
2621
2622            if clear_transaction {
2623                self.last_scrolled = None;
2624            }
2625        }
2626
2627        let mut translation = self.last_translation;
2628
2629        if let Event::Window(window::Event::RedrawRequested(now)) = event {
2630            // Step the smooth scrolling animation, if any; `last_frame`
2631            // guards against stepping twice for the same instant
2632            if self.last_frame != Some(*now)
2633                && self.step(*now, bounds, content)
2634                && let Some(notification) = self.notify_scroll(bounds, content)
2635            {
2636                interact.scroll = Some(notification);
2637            }
2638
2639            translation = self.translation(direction, bounds, content);
2640            self.last_translation = translation;
2641
2642            // Step the held-rail autoscroll, if any; the geometry is rebuilt
2643            // from the current translation, so that the pause and stop
2644            // conditions are evaluated against the scroller as of this frame
2645            if let Interaction::RailHeld(mut rail) = self.interaction {
2646                let scrollbars = Scrollbars::new(translation, direction, bounds, content);
2647
2648                let scrollbar = match rail.axis {
2649                    Axis::X => scrollbars.x.as_ref(),
2650                    Axis::Y => scrollbars.y.as_ref(),
2651                };
2652
2653                let step = self
2654                    .step_rail(&mut rail, *now, scrollbar, bounds, content)
2655                    .map(|step| (Interaction::RailHeld(rail), step));
2656
2657                match step {
2658                    Some((interaction, step)) => {
2659                        self.interaction = interaction;
2660
2661                        match step {
2662                            RailStep::Moved => {
2663                                self.source = Some(Source::Scrollbar);
2664
2665                                if let Some(notification) = self.notify_scroll(bounds, content) {
2666                                    interact.scroll = Some(notification);
2667                                }
2668
2669                                interact.request_redraw = true;
2670                            }
2671                            RailStep::Waiting => interact.request_redraw = true,
2672                            RailStep::Idle => {}
2673                        }
2674                    }
2675                    // The scrollbar disappeared while the rail was held: drop
2676                    // the interaction, like Chromium does when its scrollbar
2677                    // is unregistered
2678                    None => self.interaction = Interaction::None,
2679                }
2680            }
2681
2682            if self.target.is_some() {
2683                interact.request_redraw = true;
2684            } else if let Interaction::AutoScrolling {
2685                origin,
2686                current,
2687                last_frame,
2688            } = self.interaction
2689            {
2690                if last_frame == Some(*now) {
2691                    interact.request_redraw = true;
2692                } else {
2693                    self.interaction = Interaction::AutoScrolling {
2694                        origin,
2695                        current,
2696                        last_frame: None,
2697                    };
2698
2699                    let mut delta = current - origin;
2700
2701                    if delta.x.abs() < AUTOSCROLL_DEADZONE {
2702                        delta.x = 0.0;
2703                    }
2704
2705                    if delta.y.abs() < AUTOSCROLL_DEADZONE {
2706                        delta.y = 0.0;
2707                    }
2708
2709                    if delta.x != 0.0 || delta.y != 0.0 {
2710                        let time_delta =
2711                            last_frame.map_or(Duration::ZERO, |last_frame| *now - last_frame);
2712
2713                        let scroll_factor = time_delta.as_secs_f32();
2714
2715                        self.scroll(
2716                            direction.align(Vector::new(
2717                                delta.x.signum()
2718                                    * delta.x.abs().powf(AUTOSCROLL_SMOOTHNESS)
2719                                    * scroll_factor,
2720                                delta.y.signum()
2721                                    * delta.y.abs().powf(AUTOSCROLL_SMOOTHNESS)
2722                                    * scroll_factor,
2723                            )),
2724                            bounds,
2725                            content,
2726                        );
2727
2728                        self.source = Some(Source::AutoScroll);
2729
2730                        let notification = self.notify_scroll(bounds, content);
2731                        let has_scrolled = notification.is_some();
2732
2733                        if let Some(notification) = notification {
2734                            interact.scroll = Some(notification);
2735                        }
2736
2737                        if has_scrolled || time_delta.is_zero() {
2738                            self.interaction = Interaction::AutoScrolling {
2739                                origin,
2740                                current,
2741                                last_frame: Some(*now),
2742                            };
2743
2744                            interact.request_redraw = true;
2745                        }
2746                    }
2747                }
2748            }
2749
2750            if interact.scroll.is_none() {
2751                interact.scroll = self.notify_viewport(bounds, content);
2752            }
2753
2754            // A source that did not produce a notification must not be
2755            // attributed to a later one
2756            if self.target.is_none() {
2757                self.source = None;
2758            }
2759        }
2760
2761        // A held rail: track the pointer for the autoscroll's pause and stop
2762        // conditions; a moved pointer re-evaluates them, waking the animation
2763        if let Interaction::RailHeld(mut rail) = self.interaction
2764            && let Some(position) = rail_moved(event, cursor, rail.pointer)
2765        {
2766            rail.pointer = position;
2767            self.interaction = Interaction::RailHeld(rail);
2768            interact.request_redraw = true;
2769        }
2770
2771        // A scroller being dragged follows the pointer 1:1, until the button
2772        // is released
2773        if let Some((axis, scroller_grabbed_at)) = self.interaction.scroller_grabbed() {
2774            match event {
2775                Event::Mouse(mouse::Event::CursorMoved { .. })
2776                | Event::Touch(touch::Event::FingerMoved { .. }) => {
2777                    if let Some(scrollbar) = scrollbars.scrollbar(axis) {
2778                        let Some(cursor_position) = cursor.observe().position() else {
2779                            interact.stop = true;
2780                            return interact;
2781                        };
2782
2783                        self.scroll_to_percentage(
2784                            axis,
2785                            scrollbar.scroll_percentage(axis, scroller_grabbed_at, cursor_position),
2786                            bounds,
2787                            content,
2788                        );
2789
2790                        self.source = Some(Source::Scrollbar);
2791
2792                        if let Some(notification) = self.notify_scroll(bounds, content) {
2793                            interact.scroll = Some(notification);
2794                        }
2795
2796                        interact.capture = true;
2797                    }
2798                }
2799                _ => {}
2800            }
2801        } else if let Some(axis) = mouse_over_scrollbar {
2802            // Otherwise, a press on a scrollbar under the cursor grabs its
2803            // scroller, jump-drags it (with `Shift`, or with a plain press
2804            // when `click_to_scroll` is enabled), or presses the rail
2805            match event {
2806                Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
2807                | Event::Touch(touch::Event::FingerPressed { .. }) => {
2808                    let Some(cursor_position) = cursor.position() else {
2809                        interact.stop = true;
2810                        return interact;
2811                    };
2812
2813                    if let Some((scrollbar, hit)) = scrollbars.hit(axis, cursor_position) {
2814                        match hit {
2815                            // The scroller: jump to the click position and
2816                            // drag it from there
2817                            Hit::Scroller { grabbed_at } => {
2818                                self.scroll_to_percentage(
2819                                    axis,
2820                                    scrollbar.scroll_percentage(axis, grabbed_at, cursor_position),
2821                                    bounds,
2822                                    content,
2823                                );
2824
2825                                self.interaction = Interaction::ScrollerGrabbed(axis, grabbed_at);
2826                            }
2827                            // A "jump click" on the rail — a `Shift`-click by
2828                            // default, or a plain click when `click_to_scroll`
2829                            // is enabled: jump the scroller to the click
2830                            // position and drag it from there, like
2831                            // Chromium's `Shift`+click on the track
2832                            Hit::Rail if self.keyboard_modifiers.shift() != click_to_scroll => {
2833                                self.scroll_to_percentage(
2834                                    axis,
2835                                    scrollbar.scroll_percentage(axis, 0.5, cursor_position),
2836                                    bounds,
2837                                    content,
2838                                );
2839
2840                                self.interaction = Interaction::ScrollerGrabbed(axis, 0.5);
2841                            }
2842                            // A paging rail press — a plain click by default,
2843                            // or a `Shift`-click when `click_to_scroll` is
2844                            // enabled: a page step (animated like a wheel
2845                            // scroll) and, while the button is held, a
2846                            // constant-velocity autoscroll until the scroller
2847                            // reaches the pointer, like Chromium's track
2848                            // autoscroll
2849                            Hit::Rail => {
2850                                self.press_rail(
2851                                    axis,
2852                                    scrollbar,
2853                                    cursor_position,
2854                                    smooth_scroll,
2855                                    Instant::now(),
2856                                    bounds,
2857                                    content,
2858                                );
2859
2860                                interact.request_redraw = true;
2861                            }
2862                        }
2863
2864                        self.source = Some(Source::Scrollbar);
2865
2866                        if let Some(notification) = self.notify_scroll(bounds, content) {
2867                            interact.scroll = Some(notification);
2868                        }
2869
2870                        interact.capture = true;
2871                    }
2872                }
2873                _ => {}
2874            }
2875        }
2876
2877        if matches!(self.interaction, Interaction::AutoScrolling { .. })
2878            && matches!(
2879                event,
2880                Event::Mouse(mouse::Event::ButtonPressed(_) | mouse::Event::WheelScrolled { .. })
2881                    | Event::Touch(_)
2882                    | Event::Keyboard(_)
2883            )
2884        {
2885            self.interaction = Interaction::None;
2886            interact.capture = true;
2887            interact.invalidate_layout = true;
2888            interact.request_redraw = true;
2889            interact.stop = true;
2890            return interact;
2891        }
2892
2893        if self.last_scrolled.is_none()
2894            || !matches!(event, Event::Mouse(mouse::Event::WheelScrolled { .. }))
2895        {
2896            let cursor = match cursor_over_scrollable {
2897                Some(cursor_position)
2898                    if mouse_over_scrollbar.is_none() && !self.interaction.scrollers_grabbed() =>
2899                {
2900                    mouse::Cursor::Available(cursor_position + translation)
2901                }
2902                _ => cursor.obstruct() + translation,
2903            };
2904
2905            interact.content = Some(Content { cursor });
2906        }
2907
2908        interact
2909    }
2910
2911    /// Handles the part of the interaction that happens after the event is
2912    /// delegated to the content.
2913    ///
2914    /// Returns the [`Update`] effects to materialize on the [`Shell`].
2915    fn update(
2916        &mut self,
2917        event: &Event,
2918        bounds: Rectangle,
2919        content: Size,
2920        cursor: mouse::Cursor,
2921        cursor_over_scrollable: Option<Point>,
2922        mouse_over_scrollbar: Option<Axis>,
2923        direction: Direction,
2924        smooth_scroll: bool,
2925        auto_scroll: bool,
2926        is_event_captured: bool,
2927    ) -> Update {
2928        let mut update = Update::default();
2929
2930        if matches!(
2931            event,
2932            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
2933                | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. })
2934        ) {
2935            self.interaction = Interaction::None;
2936            return update;
2937        }
2938
2939        if is_event_captured {
2940            return update;
2941        }
2942
2943        match event {
2944            Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
2945                if !cursor.land().is_over(bounds) {
2946                    return update;
2947                }
2948
2949                let (delta, is_significant) = match *delta {
2950                    mouse::ScrollDelta::Lines { x, y } => {
2951                        let is_shift_pressed = self.keyboard_modifiers.shift();
2952
2953                        // macOS automatically inverts the axes when Shift is
2954                        // pressed
2955                        let (x, y) = if cfg!(target_os = "macos") && is_shift_pressed {
2956                            (y, x)
2957                        } else {
2958                            (x, y)
2959                        };
2960
2961                        let movement = if !is_shift_pressed {
2962                            Vector::new(x, y)
2963                        } else {
2964                            Vector::new(y, x)
2965                        };
2966
2967                        (
2968                            -movement * WHEEL_PX_PER_LINE,
2969                            x.abs() >= 0.5 || y.abs() >= 0.5,
2970                        )
2971                    }
2972                    // Pixel deltas (e.g. from high-precision touchpads) are
2973                    // already smooth, so scrolling them immediately avoids
2974                    // double-smoothing them
2975                    mouse::ScrollDelta::Pixels { x, y } => (-Vector::new(x, y), false),
2976                };
2977
2978                let delta = direction.align(delta);
2979
2980                if smooth_scroll && is_significant {
2981                    self.scroll_smoothly(delta, bounds, content, Instant::now());
2982                } else {
2983                    self.scroll(delta, bounds, content);
2984                }
2985
2986                self.source = Some(Source::Wheel);
2987
2988                let notification = self.notify_scroll(bounds, content);
2989                let has_scrolled = notification.is_some();
2990
2991                if let Some(notification) = notification {
2992                    update.scroll = Some(notification);
2993                }
2994
2995                let in_transaction = self.last_scrolled.is_some() || self.target.is_some();
2996
2997                if has_scrolled || in_transaction {
2998                    update.capture = true;
2999                }
3000
3001                if self.target.is_some() {
3002                    update.request_redraw = true;
3003                }
3004            }
3005            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle))
3006                if auto_scroll && matches!(self.interaction, Interaction::None) =>
3007            {
3008                let Some(origin) = cursor_over_scrollable else {
3009                    return update;
3010                };
3011
3012                self.interaction = Interaction::AutoScrolling {
3013                    origin,
3014                    current: origin,
3015                    last_frame: None,
3016                };
3017
3018                update.capture = true;
3019                update.invalidate_layout = true;
3020                update.request_redraw = true;
3021            }
3022            Event::Touch(event)
3023                if matches!(self.interaction, Interaction::TouchScrolling(_))
3024                    || mouse_over_scrollbar.is_none() =>
3025            {
3026                match event {
3027                    touch::Event::FingerPressed { .. } => {
3028                        let Some(position) = cursor_over_scrollable else {
3029                            return update;
3030                        };
3031
3032                        self.interaction = Interaction::TouchScrolling(position);
3033                    }
3034                    touch::Event::FingerMoved { .. } => {
3035                        let Interaction::TouchScrolling(scroll_box_touched_at) = self.interaction
3036                        else {
3037                            return update;
3038                        };
3039
3040                        let Some(cursor_position) = cursor.position() else {
3041                            return update;
3042                        };
3043
3044                        let delta = Vector::new(
3045                            scroll_box_touched_at.x - cursor_position.x,
3046                            scroll_box_touched_at.y - cursor_position.y,
3047                        );
3048
3049                        self.scroll(direction.align(delta), bounds, content);
3050
3051                        self.interaction = Interaction::TouchScrolling(cursor_position);
3052                        self.source = Some(Source::Touch);
3053
3054                        if let Some(notification) = self.notify_scroll(bounds, content) {
3055                            update.scroll = Some(notification);
3056                        }
3057                    }
3058                    _ => {}
3059                }
3060
3061                update.capture = true;
3062            }
3063            Event::Mouse(mouse::Event::CursorMoved { position }) => {
3064                if let Interaction::AutoScrolling {
3065                    origin, last_frame, ..
3066                } = self.interaction
3067                {
3068                    let delta = *position - origin;
3069
3070                    self.interaction = Interaction::AutoScrolling {
3071                        origin,
3072                        current: *position,
3073                        last_frame,
3074                    };
3075
3076                    if (delta.x.abs() >= AUTOSCROLL_DEADZONE
3077                        || delta.y.abs() >= AUTOSCROLL_DEADZONE)
3078                        && last_frame.is_none()
3079                    {
3080                        update.request_redraw = true;
3081                    }
3082                }
3083            }
3084            Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
3085                self.keyboard_modifiers = *modifiers;
3086            }
3087            _ => {}
3088        }
3089
3090        update
3091    }
3092
3093    /// Materializes the given axis's offset into an absolute one, so that it
3094    /// is no longer snapped to a logical position.
3095    fn unsnap(&mut self, axis: Axis, bounds: Rectangle, content: Size) {
3096        match axis {
3097            Axis::X => {
3098                self.offset_x =
3099                    Offset::Absolute(self.offset_x.absolute(bounds.width, content.width));
3100            }
3101            Axis::Y => {
3102                self.offset_y =
3103                    Offset::Absolute(self.offset_y.absolute(bounds.height, content.height));
3104            }
3105        }
3106    }
3107
3108    /// The absolute offset of the given axis, in pixels.
3109    fn axis_offset(&self, axis: Axis, bounds: Rectangle, content: Size) -> f32 {
3110        match axis {
3111            Axis::X => self.offset_x.absolute(bounds.width, content.width),
3112            Axis::Y => self.offset_y.absolute(bounds.height, content.height),
3113        }
3114    }
3115
3116    /// Returns the scrolling translation of the [`State`], given a [`Direction`],
3117    /// the bounds of the [`Scrollable`] and its contents.
3118    fn translation(&self, direction: Direction, bounds: Rectangle, content: Size) -> Vector {
3119        Vector::new(
3120            if let Some(horizontal) = direction.horizontal() {
3121                self.offset_x
3122                    .translation(bounds.width, content.width, horizontal.alignment)
3123            } else {
3124                0.0
3125            },
3126            if let Some(vertical) = direction.vertical() {
3127                self.offset_y
3128                    .translation(bounds.height, content.height, vertical.alignment)
3129            } else {
3130                0.0
3131            },
3132        )
3133    }
3134}
3135
3136/// The part of a [`Scrollbar`] hit by a cursor position.
3137#[derive(Debug, Clone, Copy, PartialEq)]
3138enum Hit {
3139    /// The scroller (thumb) of the [`Scrollbar`], grabbed at the given
3140    /// fraction of its length.
3141    Scroller {
3142        /// The fraction of the scroller's length, from its start, at which
3143        /// it was grabbed.
3144        grabbed_at: f32,
3145    },
3146
3147    /// The rail (track) of the [`Scrollbar`], outside of the scroller.
3148    Rail,
3149}
3150
3151#[derive(Debug)]
3152/// State of both [`Scrollbar`]s.
3153struct Scrollbars {
3154    y: Option<internals::Scrollbar>,
3155    x: Option<internals::Scrollbar>,
3156}
3157
3158impl Scrollbars {
3159    /// Create y and/or x scrollbar(s) if content is overflowing the [`Scrollable`] bounds.
3160    fn new(translation: Vector, direction: Direction, bounds: Rectangle, content: Size) -> Self {
3161        let show_scrollbar_x = direction
3162            .horizontal()
3163            .filter(|_scrollbar| content.width > bounds.width);
3164
3165        let show_scrollbar_y = direction
3166            .vertical()
3167            .filter(|_scrollbar| content.height > bounds.height);
3168
3169        let y_scrollbar = if let Some(vertical) = show_scrollbar_y {
3170            let Scrollbar {
3171                width,
3172                margin,
3173                scroller_width,
3174                spacing,
3175                padding,
3176                ..
3177            } = *vertical;
3178
3179            // Adjust the height of the vertical scrollbar if the horizontal scrollbar
3180            // is present
3181            let x_scrollbar_height =
3182                show_scrollbar_x.map_or(0.0, |h| h.width.max(h.scroller_width) + h.margin);
3183
3184            let total_scrollbar_width = width.max(scroller_width) + 2.0 * margin;
3185
3186            // The padding is purely visual: it shrinks the top and bottom of the
3187            // scrollbar without affecting the layout
3188            let scrollbar_height = (bounds.height - x_scrollbar_height - 2.0 * padding).max(0.0);
3189
3190            // Total bounds of the scrollbar + margin + scroller width
3191            let total_scrollbar_bounds = Rectangle {
3192                x: bounds.x + bounds.width - total_scrollbar_width,
3193                y: bounds.y + padding,
3194                width: total_scrollbar_width,
3195                height: scrollbar_height,
3196            };
3197
3198            // Bounds of just the scrollbar
3199            let scrollbar_bounds = Rectangle {
3200                x: bounds.x + bounds.width - total_scrollbar_width / 2.0 - width / 2.0,
3201                y: bounds.y + padding,
3202                width,
3203                height: scrollbar_height,
3204            };
3205
3206            let ratio = bounds.height / content.height;
3207
3208            let scroller = if ratio >= 1.0 {
3209                None
3210            } else {
3211                // min height for easier grabbing with super tall content
3212                let scroller_height = (scrollbar_bounds.height * ratio).max(2.0);
3213                let scroller_offset =
3214                    translation.y * ratio * scrollbar_bounds.height / bounds.height;
3215
3216                let scroller_bounds = Rectangle {
3217                    x: bounds.x + bounds.width - total_scrollbar_width / 2.0 - scroller_width / 2.0,
3218                    y: (scrollbar_bounds.y + scroller_offset).max(0.0),
3219                    width: scroller_width,
3220                    height: scroller_height,
3221                };
3222
3223                Some(internals::Scroller {
3224                    bounds: scroller_bounds,
3225                })
3226            };
3227
3228            Some(internals::Scrollbar {
3229                total_bounds: total_scrollbar_bounds,
3230                bounds: scrollbar_bounds,
3231                scroller,
3232                alignment: vertical.alignment,
3233                disabled: content.height <= bounds.height,
3234                floating: spacing.is_none(),
3235            })
3236        } else {
3237            None
3238        };
3239
3240        let x_scrollbar = if let Some(horizontal) = show_scrollbar_x {
3241            let Scrollbar {
3242                width,
3243                margin,
3244                scroller_width,
3245                spacing,
3246                padding,
3247                ..
3248            } = *horizontal;
3249
3250            // Need to adjust the width of the horizontal scrollbar if the vertical scrollbar
3251            // is present
3252            let scrollbar_y_width =
3253                y_scrollbar.map_or(0.0, |scrollbar| scrollbar.total_bounds.width);
3254
3255            let total_scrollbar_height = width.max(scroller_width) + 2.0 * margin;
3256
3257            // The padding is purely visual: it shrinks the left and right ends of
3258            // the scrollbar without affecting the layout
3259            let scrollbar_width = (bounds.width - scrollbar_y_width - 2.0 * padding).max(0.0);
3260
3261            // Total bounds of the scrollbar + margin + scroller width
3262            let total_scrollbar_bounds = Rectangle {
3263                x: bounds.x + padding,
3264                y: bounds.y + bounds.height - total_scrollbar_height,
3265                width: scrollbar_width,
3266                height: total_scrollbar_height,
3267            };
3268
3269            // Bounds of just the scrollbar
3270            let scrollbar_bounds = Rectangle {
3271                x: bounds.x + padding,
3272                y: bounds.y + bounds.height - total_scrollbar_height / 2.0 - width / 2.0,
3273                width: scrollbar_width,
3274                height: width,
3275            };
3276
3277            let ratio = bounds.width / content.width;
3278
3279            let scroller = if ratio >= 1.0 {
3280                None
3281            } else {
3282                // min width for easier grabbing with extra wide content
3283                let scroller_length = (scrollbar_bounds.width * ratio).max(2.0);
3284                let scroller_offset = translation.x * ratio * scrollbar_bounds.width / bounds.width;
3285
3286                let scroller_bounds = Rectangle {
3287                    x: (scrollbar_bounds.x + scroller_offset).max(0.0),
3288                    y: bounds.y + bounds.height
3289                        - total_scrollbar_height / 2.0
3290                        - scroller_width / 2.0,
3291                    width: scroller_length,
3292                    height: scroller_width,
3293                };
3294
3295                Some(internals::Scroller {
3296                    bounds: scroller_bounds,
3297                })
3298            };
3299
3300            Some(internals::Scrollbar {
3301                total_bounds: total_scrollbar_bounds,
3302                bounds: scrollbar_bounds,
3303                scroller,
3304                alignment: horizontal.alignment,
3305                disabled: content.width <= bounds.width,
3306                floating: spacing.is_none(),
3307            })
3308        } else {
3309            None
3310        };
3311
3312        Self {
3313            y: y_scrollbar,
3314            x: x_scrollbar,
3315        }
3316    }
3317
3318    /// The [`internals::Scrollbar`] of the given axis, if it is shown.
3319    fn scrollbar(&self, axis: Axis) -> Option<&internals::Scrollbar> {
3320        match axis {
3321            Axis::X => self.x.as_ref(),
3322            Axis::Y => self.y.as_ref(),
3323        }
3324    }
3325
3326    /// The [`Axis`] of the scrollbar the given `cursor` is over, if any.
3327    ///
3328    /// The scrollbars' total bounds don't overlap, so the cursor is over at
3329    /// most one of them; the vertical one takes precedence.
3330    fn is_mouse_over(&self, cursor: mouse::Cursor) -> Option<Axis> {
3331        let cursor_position = cursor.position()?;
3332
3333        if self
3334            .y
3335            .as_ref()
3336            .is_some_and(|scrollbar| scrollbar.is_mouse_over(cursor_position))
3337        {
3338            Some(Axis::Y)
3339        } else if self
3340            .x
3341            .as_ref()
3342            .is_some_and(|scrollbar| scrollbar.is_mouse_over(cursor_position))
3343        {
3344            Some(Axis::X)
3345        } else {
3346            None
3347        }
3348    }
3349
3350    fn is_y_disabled(&self) -> bool {
3351        self.y.map(|y| y.disabled).unwrap_or(false)
3352    }
3353
3354    fn is_x_disabled(&self) -> bool {
3355        self.x.map(|x| x.disabled).unwrap_or(false)
3356    }
3357
3358    /// The [`internals::Scrollbar`] of the given axis and the part of it hit by
3359    /// the given cursor position, if any.
3360    fn hit(&self, axis: Axis, cursor_position: Point) -> Option<(&internals::Scrollbar, Hit)> {
3361        let scrollbar = match axis {
3362            Axis::X => self.x.as_ref(),
3363            Axis::Y => self.y.as_ref(),
3364        }?;
3365        let scroller = scrollbar.scroller?;
3366
3367        if !scrollbar.total_bounds.contains(cursor_position) {
3368            return None;
3369        }
3370
3371        Some((
3372            scrollbar,
3373            if scroller.bounds.contains(cursor_position) {
3374                Hit::Scroller {
3375                    grabbed_at: (axis.coordinate(cursor_position)
3376                        - axis.coordinate(scroller.bounds.position()))
3377                        / axis.length(scroller.bounds),
3378                }
3379            } else {
3380                Hit::Rail
3381            },
3382        ))
3383    }
3384
3385    fn is_any_floating(&self) -> bool {
3386        self.y.is_some_and(|scrollbar| scrollbar.floating)
3387            || self.x.is_some_and(|scrollbar| scrollbar.floating)
3388    }
3389
3390    fn active(&self) -> bool {
3391        self.y.is_some() || self.x.is_some()
3392    }
3393}
3394
3395pub(super) mod internals {
3396    use crate::core::{Point, Rectangle};
3397
3398    use super::Anchor;
3399
3400    #[derive(Debug, Copy, Clone)]
3401    pub struct Scrollbar {
3402        pub total_bounds: Rectangle,
3403        pub bounds: Rectangle,
3404        pub scroller: Option<Scroller>,
3405        pub alignment: Anchor,
3406        pub disabled: bool,
3407        pub floating: bool,
3408    }
3409
3410    impl Scrollbar {
3411        /// Returns whether the mouse is over the scrollbar or not.
3412        pub fn is_mouse_over(&self, cursor_position: Point) -> bool {
3413            self.total_bounds.contains(cursor_position)
3414        }
3415
3416        /// Returns the scrolled percentage from the cursor position, along
3417        /// the given axis.
3418        pub fn scroll_percentage(
3419            &self,
3420            axis: super::Axis,
3421            grabbed_at: f32,
3422            cursor_position: Point,
3423        ) -> f32 {
3424            let Some(scroller) = self.scroller else {
3425                return 0.0;
3426            };
3427
3428            let percentage = (axis.coordinate(cursor_position)
3429                - axis.coordinate(self.bounds.position())
3430                - axis.length(scroller.bounds) * grabbed_at)
3431                / (axis.length(self.bounds) - axis.length(scroller.bounds));
3432
3433            match self.alignment {
3434                Anchor::Start => percentage,
3435                Anchor::End => 1.0 - percentage,
3436            }
3437        }
3438    }
3439
3440    /// The handle of a [`Scrollbar`].
3441    #[derive(Debug, Clone, Copy)]
3442    pub struct Scroller {
3443        /// The bounds of the [`Scroller`].
3444        pub bounds: Rectangle,
3445    }
3446}
3447
3448/// The possible status of a [`Scrollable`].
3449#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3450pub enum Status {
3451    /// The [`Scrollable`] can be interacted with.
3452    Active {
3453        /// Whether or not the horizontal scrollbar is disabled meaning the content isn't overflowing.
3454        is_horizontal_scrollbar_disabled: bool,
3455        /// Whether or not the vertical scrollbar is disabled meaning the content isn't overflowing.
3456        is_vertical_scrollbar_disabled: bool,
3457    },
3458    /// The [`Scrollable`] is being hovered.
3459    Hovered {
3460        /// Indicates if the horizontal scrollbar is being hovered.
3461        is_horizontal_scrollbar_hovered: bool,
3462        /// Indicates if the vertical scrollbar is being hovered.
3463        is_vertical_scrollbar_hovered: bool,
3464        /// Whether or not the horizontal scrollbar is disabled meaning the content isn't overflowing.
3465        is_horizontal_scrollbar_disabled: bool,
3466        /// Whether or not the vertical scrollbar is disabled meaning the content isn't overflowing.
3467        is_vertical_scrollbar_disabled: bool,
3468    },
3469    /// The [`Scrollable`] is being dragged.
3470    Dragged {
3471        /// Indicates if the horizontal scrollbar is being dragged.
3472        is_horizontal_scrollbar_dragged: bool,
3473        /// Indicates if the vertical scrollbar is being dragged.
3474        is_vertical_scrollbar_dragged: bool,
3475        /// Whether or not the horizontal scrollbar is disabled meaning the content isn't overflowing.
3476        is_horizontal_scrollbar_disabled: bool,
3477        /// Whether or not the vertical scrollbar is disabled meaning the content isn't overflowing.
3478        is_vertical_scrollbar_disabled: bool,
3479    },
3480}
3481
3482/// The appearance of a scrollable.
3483#[derive(Debug, Clone, Copy, PartialEq)]
3484pub struct Style {
3485    /// The [`container::Style`] of a scrollable.
3486    pub container: container::Style,
3487    /// The vertical [`Rail`] appearance.
3488    pub vertical_rail: Rail,
3489    /// The horizontal [`Rail`] appearance.
3490    pub horizontal_rail: Rail,
3491    /// The [`Background`] of the gap between a horizontal and vertical scrollbar.
3492    pub gap: Option<Background>,
3493    /// The appearance of the [`AutoScroll`] overlay.
3494    pub auto_scroll: AutoScroll,
3495}
3496
3497/// The appearance of the scrollbar of a scrollable.
3498#[derive(Debug, Clone, Copy, PartialEq)]
3499pub struct Rail {
3500    /// The [`Background`] of a scrollbar.
3501    pub background: Option<Background>,
3502    /// The [`Border`] of a scrollbar.
3503    pub border: Border,
3504    /// The appearance of the [`Scroller`] of a scrollbar.
3505    pub scroller: Scroller,
3506}
3507
3508/// The appearance of the scroller of a scrollable.
3509#[derive(Debug, Clone, Copy, PartialEq)]
3510pub struct Scroller {
3511    /// The [`Background`] of the scroller.
3512    pub background: Background,
3513    /// The [`Border`] of the scroller.
3514    pub border: Border,
3515}
3516
3517/// The appearance of the autoscroll overlay of a scrollable.
3518#[derive(Debug, Clone, Copy, PartialEq)]
3519pub struct AutoScroll {
3520    /// The [`Background`] of the [`AutoScroll`] overlay.
3521    pub background: Background,
3522    /// The [`Border`] of the [`AutoScroll`] overlay.
3523    pub border: Border,
3524    /// Thje [`Shadow`] of the [`AutoScroll`] overlay.
3525    pub shadow: Shadow,
3526    /// The [`Color`] for the arrow icons of the [`AutoScroll`] overlay.
3527    pub icon: Color,
3528}
3529
3530/// The theme catalog of a [`Scrollable`].
3531pub trait Catalog {
3532    /// The item class of the [`Catalog`].
3533    type Class<'a>;
3534
3535    /// The default class produced by the [`Catalog`].
3536    fn default<'a>() -> Self::Class<'a>;
3537
3538    /// The [`Style`] of a class with the given status.
3539    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
3540}
3541
3542/// A styling function for a [`Scrollable`].
3543pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
3544
3545impl Catalog for Theme {
3546    type Class<'a> = StyleFn<'a, Self>;
3547
3548    fn default<'a>() -> Self::Class<'a> {
3549        Box::new(default)
3550    }
3551
3552    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
3553        class(self, status)
3554    }
3555}
3556
3557/// The default style of a [`Scrollable`].
3558pub fn default(theme: &Theme, status: Status) -> Style {
3559    let palette = theme.palette();
3560
3561    let scrollbar = Rail {
3562        background: Some(palette.background.weak.color.into()),
3563        border: border::rounded(2),
3564        scroller: Scroller {
3565            background: palette.background.strongest.color.into(),
3566            border: border::rounded(2),
3567        },
3568    };
3569
3570    let auto_scroll = AutoScroll {
3571        background: palette.background.base.color.scale_alpha(0.9).into(),
3572        border: border::rounded(u32::MAX)
3573            .width(1)
3574            .color(palette.background.base.text.scale_alpha(0.8)),
3575        shadow: Shadow {
3576            color: Color::BLACK.scale_alpha(0.7),
3577            offset: Vector::ZERO,
3578            blur_radius: 2.0,
3579        },
3580        icon: palette.background.base.text.scale_alpha(0.8),
3581    };
3582
3583    match status {
3584        Status::Active { .. } => Style {
3585            container: container::Style::default(),
3586            vertical_rail: scrollbar,
3587            horizontal_rail: scrollbar,
3588            gap: None,
3589            auto_scroll,
3590        },
3591        Status::Hovered {
3592            is_horizontal_scrollbar_hovered,
3593            is_vertical_scrollbar_hovered,
3594            ..
3595        } => {
3596            let hovered_scrollbar = Rail {
3597                scroller: Scroller {
3598                    background: palette.primary.strong.color.into(),
3599                    ..scrollbar.scroller
3600                },
3601                ..scrollbar
3602            };
3603
3604            Style {
3605                container: container::Style::default(),
3606                vertical_rail: if is_vertical_scrollbar_hovered {
3607                    hovered_scrollbar
3608                } else {
3609                    scrollbar
3610                },
3611                horizontal_rail: if is_horizontal_scrollbar_hovered {
3612                    hovered_scrollbar
3613                } else {
3614                    scrollbar
3615                },
3616                gap: None,
3617                auto_scroll,
3618            }
3619        }
3620        Status::Dragged {
3621            is_horizontal_scrollbar_dragged,
3622            is_vertical_scrollbar_dragged,
3623            ..
3624        } => {
3625            let dragged_scrollbar = Rail {
3626                scroller: Scroller {
3627                    background: palette.primary.base.color.into(),
3628                    ..scrollbar.scroller
3629                },
3630                ..scrollbar
3631            };
3632
3633            Style {
3634                container: container::Style::default(),
3635                vertical_rail: if is_vertical_scrollbar_dragged {
3636                    dragged_scrollbar
3637                } else {
3638                    scrollbar
3639                },
3640                horizontal_rail: if is_horizontal_scrollbar_dragged {
3641                    dragged_scrollbar
3642                } else {
3643                    scrollbar
3644                },
3645                gap: None,
3646                auto_scroll,
3647            }
3648        }
3649    }
3650}