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::*; } }
6//! # pub type State = ();
7//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
8//! use iced::widget::{column, scrollable, space};
9//!
10//! enum Message {
11//!     // ...
12//! }
13//!
14//! fn view(state: &State) -> Element<'_, Message> {
15//!     scrollable(column![
16//!         "Scroll me!",
17//!         space().height(3000),
18//!         "You did it!",
19//!     ]).into()
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, Operation};
35use crate::core::widget::tree::{self, Tree};
36use crate::core::window;
37use crate::core::{
38    self, Background, Clipboard, Color, Element, Event, InputMethod, Layout,
39    Length, Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme,
40    Vector, Widget,
41};
42
43pub use operation::scrollable::{AbsoluteOffset, RelativeOffset};
44
45/// A widget that can vertically display an infinite amount of content with a
46/// scrollbar.
47///
48/// # Example
49/// ```no_run
50/// # mod iced { pub mod widget { pub use iced_widget::*; } }
51/// # pub type State = ();
52/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
53/// use iced::widget::{column, scrollable, space};
54///
55/// enum Message {
56///     // ...
57/// }
58///
59/// fn view(state: &State) -> Element<'_, Message> {
60///     scrollable(column![
61///         "Scroll me!",
62///         space().height(3000),
63///         "You did it!",
64///     ]).into()
65/// }
66/// ```
67pub struct Scrollable<
68    'a,
69    Message,
70    Theme = crate::Theme,
71    Renderer = crate::Renderer,
72> where
73    Theme: Catalog,
74    Renderer: text::Renderer,
75{
76    id: Option<widget::Id>,
77    width: Length,
78    height: Length,
79    direction: Direction,
80    auto_scroll: bool,
81    content: Element<'a, Message, Theme, Renderer>,
82    on_scroll: Option<Box<dyn Fn(Viewport) -> Message + 'a>>,
83    class: Theme::Class<'a>,
84    last_status: Option<Status>,
85}
86
87impl<'a, Message, Theme, Renderer> Scrollable<'a, Message, Theme, Renderer>
88where
89    Theme: Catalog,
90    Renderer: text::Renderer,
91{
92    /// Creates a new vertical [`Scrollable`].
93    pub fn new(
94        content: impl Into<Element<'a, Message, Theme, Renderer>>,
95    ) -> Self {
96        Self::with_direction(content, Direction::default())
97    }
98
99    /// Creates a new [`Scrollable`] with the given [`Direction`].
100    pub fn with_direction(
101        content: impl Into<Element<'a, Message, Theme, Renderer>>,
102        direction: impl Into<Direction>,
103    ) -> Self {
104        Scrollable {
105            id: None,
106            width: Length::Shrink,
107            height: Length::Shrink,
108            direction: direction.into(),
109            auto_scroll: false,
110            content: content.into(),
111            on_scroll: None,
112            class: Theme::default(),
113            last_status: None,
114        }
115        .enclose()
116    }
117
118    fn enclose(mut self) -> Self {
119        let size_hint = self.content.as_widget().size_hint();
120
121        if self.direction.horizontal().is_none() {
122            self.width = self.width.enclose(size_hint.width);
123        }
124
125        if self.direction.vertical().is_none() {
126            self.height = self.height.enclose(size_hint.height);
127        }
128
129        self
130    }
131
132    /// Makes the [`Scrollable`] scroll horizontally, with default [`Scrollbar`] settings.
133    pub fn horizontal(self) -> Self {
134        self.direction(Direction::Horizontal(Scrollbar::default()))
135    }
136
137    /// Sets the [`Direction`] of the [`Scrollable`].
138    pub fn direction(mut self, direction: impl Into<Direction>) -> Self {
139        self.direction = direction.into();
140        self.enclose()
141    }
142
143    /// Sets the [`widget::Id`] of the [`Scrollable`].
144    pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
145        self.id = Some(id.into());
146        self
147    }
148
149    /// Sets the width of the [`Scrollable`].
150    pub fn width(mut self, width: impl Into<Length>) -> Self {
151        self.width = width.into();
152        self
153    }
154
155    /// Sets the height of the [`Scrollable`].
156    pub fn height(mut self, height: impl Into<Length>) -> Self {
157        self.height = height.into();
158        self
159    }
160
161    /// Sets a function to call when the [`Scrollable`] is scrolled.
162    ///
163    /// The function takes the [`Viewport`] of the [`Scrollable`]
164    pub fn on_scroll(mut self, f: impl Fn(Viewport) -> Message + 'a) -> Self {
165        self.on_scroll = Some(Box::new(f));
166        self
167    }
168
169    /// Anchors the vertical [`Scrollable`] direction to the top.
170    pub fn anchor_top(self) -> Self {
171        self.anchor_y(Anchor::Start)
172    }
173
174    /// Anchors the vertical [`Scrollable`] direction to the bottom.
175    pub fn anchor_bottom(self) -> Self {
176        self.anchor_y(Anchor::End)
177    }
178
179    /// Anchors the horizontal [`Scrollable`] direction to the left.
180    pub fn anchor_left(self) -> Self {
181        self.anchor_x(Anchor::Start)
182    }
183
184    /// Anchors the horizontal [`Scrollable`] direction to the right.
185    pub fn anchor_right(self) -> Self {
186        self.anchor_x(Anchor::End)
187    }
188
189    /// Sets the [`Anchor`] of the horizontal direction of the [`Scrollable`], if applicable.
190    pub fn anchor_x(mut self, alignment: Anchor) -> Self {
191        match &mut self.direction {
192            Direction::Horizontal(horizontal)
193            | Direction::Both { horizontal, .. } => {
194                horizontal.alignment = alignment;
195            }
196            Direction::Vertical { .. } => {}
197        }
198
199        self
200    }
201
202    /// Sets the [`Anchor`] of the vertical direction of the [`Scrollable`], if applicable.
203    pub fn anchor_y(mut self, alignment: Anchor) -> Self {
204        match &mut self.direction {
205            Direction::Vertical(vertical)
206            | Direction::Both { vertical, .. } => {
207                vertical.alignment = alignment;
208            }
209            Direction::Horizontal { .. } => {}
210        }
211
212        self
213    }
214
215    /// Embeds the [`Scrollbar`] into the [`Scrollable`], instead of floating on top of the
216    /// content.
217    ///
218    /// The `spacing` provided will be used as space between the [`Scrollbar`] and the contents
219    /// of the [`Scrollable`].
220    pub fn spacing(mut self, new_spacing: impl Into<Pixels>) -> Self {
221        match &mut self.direction {
222            Direction::Horizontal(scrollbar)
223            | Direction::Vertical(scrollbar) => {
224                scrollbar.spacing = Some(new_spacing.into().0);
225            }
226            Direction::Both { .. } => {}
227        }
228
229        self
230    }
231
232    /// Sets whether the user should be allowed to auto-scroll the [`Scrollable`]
233    /// with the middle mouse button.
234    ///
235    /// By default, it is disabled.
236    pub fn auto_scroll(mut self, auto_scroll: bool) -> Self {
237        self.auto_scroll = auto_scroll;
238        self
239    }
240
241    /// Sets the style of this [`Scrollable`].
242    #[must_use]
243    pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
244    where
245        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
246    {
247        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
248        self
249    }
250
251    /// Sets the style class of the [`Scrollable`].
252    #[cfg(feature = "advanced")]
253    #[must_use]
254    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
255        self.class = class.into();
256        self
257    }
258}
259
260/// The direction of [`Scrollable`].
261#[derive(Debug, Clone, Copy, PartialEq)]
262pub enum Direction {
263    /// Vertical scrolling
264    Vertical(Scrollbar),
265    /// Horizontal scrolling
266    Horizontal(Scrollbar),
267    /// Both vertical and horizontal scrolling
268    Both {
269        /// The properties of the vertical scrollbar.
270        vertical: Scrollbar,
271        /// The properties of the horizontal scrollbar.
272        horizontal: Scrollbar,
273    },
274}
275
276impl Direction {
277    /// Returns the horizontal [`Scrollbar`], if any.
278    pub fn horizontal(&self) -> Option<&Scrollbar> {
279        match self {
280            Self::Horizontal(scrollbar) => Some(scrollbar),
281            Self::Both { horizontal, .. } => Some(horizontal),
282            Self::Vertical(_) => None,
283        }
284    }
285
286    /// Returns the vertical [`Scrollbar`], if any.
287    pub fn vertical(&self) -> Option<&Scrollbar> {
288        match self {
289            Self::Vertical(scrollbar) => Some(scrollbar),
290            Self::Both { vertical, .. } => Some(vertical),
291            Self::Horizontal(_) => None,
292        }
293    }
294
295    fn align(&self, delta: Vector) -> Vector {
296        let horizontal_alignment =
297            self.horizontal().map(|p| p.alignment).unwrap_or_default();
298
299        let vertical_alignment =
300            self.vertical().map(|p| p.alignment).unwrap_or_default();
301
302        let align = |alignment: Anchor, delta: f32| match alignment {
303            Anchor::Start => delta,
304            Anchor::End => -delta,
305        };
306
307        Vector::new(
308            align(horizontal_alignment, delta.x),
309            align(vertical_alignment, delta.y),
310        )
311    }
312}
313
314impl Default for Direction {
315    fn default() -> Self {
316        Self::Vertical(Scrollbar::default())
317    }
318}
319
320/// A scrollbar within a [`Scrollable`].
321#[derive(Debug, Clone, Copy, PartialEq)]
322pub struct Scrollbar {
323    width: f32,
324    margin: f32,
325    scroller_width: f32,
326    alignment: Anchor,
327    spacing: Option<f32>,
328}
329
330impl Default for Scrollbar {
331    fn default() -> Self {
332        Self {
333            width: 10.0,
334            margin: 0.0,
335            scroller_width: 10.0,
336            alignment: Anchor::Start,
337            spacing: None,
338        }
339    }
340}
341
342impl Scrollbar {
343    /// Creates new [`Scrollbar`] for use in a [`Scrollable`].
344    pub fn new() -> Self {
345        Self::default()
346    }
347
348    /// Create a [`Scrollbar`] with zero width to allow a [`Scrollable`] to scroll without a visible
349    /// scroller.
350    pub fn hidden() -> Self {
351        Self::default().width(0).scroller_width(0)
352    }
353
354    /// Sets the scrollbar width of the [`Scrollbar`] .
355    pub fn width(mut self, width: impl Into<Pixels>) -> Self {
356        self.width = width.into().0.max(0.0);
357        self
358    }
359
360    /// Sets the scrollbar margin of the [`Scrollbar`] .
361    pub fn margin(mut self, margin: impl Into<Pixels>) -> Self {
362        self.margin = margin.into().0;
363        self
364    }
365
366    /// Sets the scroller width of the [`Scrollbar`] .
367    pub fn scroller_width(mut self, scroller_width: impl Into<Pixels>) -> Self {
368        self.scroller_width = scroller_width.into().0.max(0.0);
369        self
370    }
371
372    /// Sets the [`Anchor`] of the [`Scrollbar`] .
373    pub fn anchor(mut self, alignment: Anchor) -> Self {
374        self.alignment = alignment;
375        self
376    }
377
378    /// Sets whether the [`Scrollbar`] should be embedded in the [`Scrollable`], using
379    /// the given spacing between itself and the contents.
380    ///
381    /// An embedded [`Scrollbar`] will always be displayed, will take layout space,
382    /// and will not float over the contents.
383    pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
384        self.spacing = Some(spacing.into().0);
385        self
386    }
387}
388
389/// The anchor of the scroller of the [`Scrollable`] relative to its [`Viewport`]
390/// on a given axis.
391#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
392pub enum Anchor {
393    /// Scroller is anchoer to the start of the [`Viewport`].
394    #[default]
395    Start,
396    /// Content is aligned to the end of the [`Viewport`].
397    End,
398}
399
400impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
401    for Scrollable<'_, Message, Theme, Renderer>
402where
403    Theme: Catalog,
404    Renderer: text::Renderer,
405{
406    fn tag(&self) -> tree::Tag {
407        tree::Tag::of::<State>()
408    }
409
410    fn state(&self) -> tree::State {
411        tree::State::new(State::new())
412    }
413
414    fn children(&self) -> Vec<Tree> {
415        vec![Tree::new(&self.content)]
416    }
417
418    fn diff(&self, tree: &mut Tree) {
419        tree.diff_children(std::slice::from_ref(&self.content));
420    }
421
422    fn size(&self) -> Size<Length> {
423        Size {
424            width: self.width,
425            height: self.height,
426        }
427    }
428
429    fn layout(
430        &mut self,
431        tree: &mut Tree,
432        renderer: &Renderer,
433        limits: &layout::Limits,
434    ) -> layout::Node {
435        let mut layout = |right_padding, bottom_padding| {
436            layout::padded(
437                limits,
438                self.width,
439                self.height,
440                Padding {
441                    right: right_padding,
442                    bottom: bottom_padding,
443                    ..Padding::ZERO
444                },
445                |limits| {
446                    let is_horizontal = self.direction.horizontal().is_some();
447                    let is_vertical = self.direction.vertical().is_some();
448
449                    let child_limits = layout::Limits::with_compression(
450                        limits.min(),
451                        Size::new(
452                            if is_horizontal {
453                                f32::INFINITY
454                            } else {
455                                limits.max().width
456                            },
457                            if is_vertical {
458                                f32::INFINITY
459                            } else {
460                                limits.max().height
461                            },
462                        ),
463                        Size::new(is_horizontal, is_vertical),
464                    );
465
466                    self.content.as_widget_mut().layout(
467                        &mut tree.children[0],
468                        renderer,
469                        &child_limits,
470                    )
471                },
472            )
473        };
474
475        match self.direction {
476            Direction::Vertical(Scrollbar {
477                width,
478                margin,
479                spacing: Some(spacing),
480                ..
481            })
482            | Direction::Horizontal(Scrollbar {
483                width,
484                margin,
485                spacing: Some(spacing),
486                ..
487            }) => {
488                let is_vertical =
489                    matches!(self.direction, Direction::Vertical(_));
490
491                let padding = width + margin * 2.0 + spacing;
492                let state = tree.state.downcast_mut::<State>();
493
494                let status_quo = layout(
495                    if is_vertical && state.is_scrollbar_visible {
496                        padding
497                    } else {
498                        0.0
499                    },
500                    if !is_vertical && state.is_scrollbar_visible {
501                        padding
502                    } else {
503                        0.0
504                    },
505                );
506
507                let is_scrollbar_visible = if is_vertical {
508                    status_quo.children()[0].size().height
509                        > status_quo.size().height
510                } else {
511                    status_quo.children()[0].size().width
512                        > status_quo.size().width
513                };
514
515                if state.is_scrollbar_visible == is_scrollbar_visible {
516                    status_quo
517                } else {
518                    log::trace!("Scrollbar status quo has changed");
519                    state.is_scrollbar_visible = is_scrollbar_visible;
520
521                    layout(
522                        if is_vertical && state.is_scrollbar_visible {
523                            padding
524                        } else {
525                            0.0
526                        },
527                        if !is_vertical && state.is_scrollbar_visible {
528                            padding
529                        } else {
530                            0.0
531                        },
532                    )
533                }
534            }
535            _ => layout(0.0, 0.0),
536        }
537    }
538
539    fn operate(
540        &mut self,
541        tree: &mut Tree,
542        layout: Layout<'_>,
543        renderer: &Renderer,
544        operation: &mut dyn Operation,
545    ) {
546        let state = tree.state.downcast_mut::<State>();
547
548        let bounds = layout.bounds();
549        let content_layout = layout.children().next().unwrap();
550        let content_bounds = content_layout.bounds();
551        let translation =
552            state.translation(self.direction, bounds, content_bounds);
553
554        operation.scrollable(
555            self.id.as_ref(),
556            bounds,
557            content_bounds,
558            translation,
559            state,
560        );
561
562        operation.traverse(&mut |operation| {
563            self.content.as_widget_mut().operate(
564                &mut tree.children[0],
565                layout.children().next().unwrap(),
566                renderer,
567                operation,
568            );
569        });
570    }
571
572    fn update(
573        &mut self,
574        tree: &mut Tree,
575        event: &Event,
576        layout: Layout<'_>,
577        cursor: mouse::Cursor,
578        renderer: &Renderer,
579        clipboard: &mut dyn Clipboard,
580        shell: &mut Shell<'_, Message>,
581        _viewport: &Rectangle,
582    ) {
583        const AUTOSCROLL_DEADZONE: f32 = 20.0;
584        const AUTOSCROLL_SMOOTHNESS: f32 = 1.5;
585
586        let state = tree.state.downcast_mut::<State>();
587        let bounds = layout.bounds();
588        let cursor_over_scrollable = cursor.position_over(bounds);
589
590        let content = layout.children().next().unwrap();
591        let content_bounds = content.bounds();
592
593        let scrollbars =
594            Scrollbars::new(state, self.direction, bounds, content_bounds);
595
596        let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
597            scrollbars.is_mouse_over(cursor);
598
599        let last_offsets = (state.offset_x, state.offset_y);
600
601        if let Some(last_scrolled) = state.last_scrolled {
602            let clear_transaction = match event {
603                Event::Mouse(
604                    mouse::Event::ButtonPressed(_)
605                    | mouse::Event::ButtonReleased(_)
606                    | mouse::Event::CursorLeft,
607                ) => true,
608                Event::Mouse(mouse::Event::CursorMoved { .. }) => {
609                    last_scrolled.elapsed() > Duration::from_millis(100)
610                }
611                _ => last_scrolled.elapsed() > Duration::from_millis(1500),
612            };
613
614            if clear_transaction {
615                state.last_scrolled = None;
616            }
617        }
618
619        let mut update = || {
620            if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at() {
621                match event {
622                    Event::Mouse(mouse::Event::CursorMoved { .. })
623                    | Event::Touch(touch::Event::FingerMoved { .. }) => {
624                        if let Some(scrollbar) = scrollbars.y {
625                            let Some(cursor_position) =
626                                cursor.land().position()
627                            else {
628                                return;
629                            };
630
631                            state.scroll_y_to(
632                                scrollbar.scroll_percentage_y(
633                                    scroller_grabbed_at,
634                                    cursor_position,
635                                ),
636                                bounds,
637                                content_bounds,
638                            );
639
640                            let _ = notify_scroll(
641                                state,
642                                &self.on_scroll,
643                                bounds,
644                                content_bounds,
645                                shell,
646                            );
647
648                            shell.capture_event();
649                        }
650                    }
651                    _ => {}
652                }
653            } else if mouse_over_y_scrollbar {
654                match event {
655                    Event::Mouse(mouse::Event::ButtonPressed(
656                        mouse::Button::Left,
657                    ))
658                    | Event::Touch(touch::Event::FingerPressed { .. }) => {
659                        let Some(cursor_position) = cursor.position() else {
660                            return;
661                        };
662
663                        if let (Some(scroller_grabbed_at), Some(scrollbar)) = (
664                            scrollbars.grab_y_scroller(cursor_position),
665                            scrollbars.y,
666                        ) {
667                            state.scroll_y_to(
668                                scrollbar.scroll_percentage_y(
669                                    scroller_grabbed_at,
670                                    cursor_position,
671                                ),
672                                bounds,
673                                content_bounds,
674                            );
675
676                            state.interaction = Interaction::YScrollerGrabbed(
677                                scroller_grabbed_at,
678                            );
679
680                            let _ = notify_scroll(
681                                state,
682                                &self.on_scroll,
683                                bounds,
684                                content_bounds,
685                                shell,
686                            );
687                        }
688
689                        shell.capture_event();
690                    }
691                    _ => {}
692                }
693            }
694
695            if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at() {
696                match event {
697                    Event::Mouse(mouse::Event::CursorMoved { .. })
698                    | Event::Touch(touch::Event::FingerMoved { .. }) => {
699                        let Some(cursor_position) = cursor.land().position()
700                        else {
701                            return;
702                        };
703
704                        if let Some(scrollbar) = scrollbars.x {
705                            state.scroll_x_to(
706                                scrollbar.scroll_percentage_x(
707                                    scroller_grabbed_at,
708                                    cursor_position,
709                                ),
710                                bounds,
711                                content_bounds,
712                            );
713
714                            let _ = notify_scroll(
715                                state,
716                                &self.on_scroll,
717                                bounds,
718                                content_bounds,
719                                shell,
720                            );
721                        }
722
723                        shell.capture_event();
724                    }
725                    _ => {}
726                }
727            } else if mouse_over_x_scrollbar {
728                match event {
729                    Event::Mouse(mouse::Event::ButtonPressed(
730                        mouse::Button::Left,
731                    ))
732                    | Event::Touch(touch::Event::FingerPressed { .. }) => {
733                        let Some(cursor_position) = cursor.position() else {
734                            return;
735                        };
736
737                        if let (Some(scroller_grabbed_at), Some(scrollbar)) = (
738                            scrollbars.grab_x_scroller(cursor_position),
739                            scrollbars.x,
740                        ) {
741                            state.scroll_x_to(
742                                scrollbar.scroll_percentage_x(
743                                    scroller_grabbed_at,
744                                    cursor_position,
745                                ),
746                                bounds,
747                                content_bounds,
748                            );
749
750                            state.interaction = Interaction::XScrollerGrabbed(
751                                scroller_grabbed_at,
752                            );
753
754                            let _ = notify_scroll(
755                                state,
756                                &self.on_scroll,
757                                bounds,
758                                content_bounds,
759                                shell,
760                            );
761
762                            shell.capture_event();
763                        }
764                    }
765                    _ => {}
766                }
767            }
768
769            if matches!(state.interaction, Interaction::AutoScrolling { .. })
770                && matches!(
771                    event,
772                    Event::Mouse(
773                        mouse::Event::ButtonPressed(_)
774                            | mouse::Event::WheelScrolled { .. }
775                    ) | Event::Touch(_)
776                        | Event::Keyboard(_)
777                )
778            {
779                state.interaction = Interaction::None;
780                shell.capture_event();
781                shell.invalidate_layout();
782                shell.request_redraw();
783                return;
784            }
785
786            if state.last_scrolled.is_none()
787                || !matches!(
788                    event,
789                    Event::Mouse(mouse::Event::WheelScrolled { .. })
790                )
791            {
792                let translation =
793                    state.translation(self.direction, bounds, content_bounds);
794
795                let cursor = match cursor_over_scrollable {
796                    Some(cursor_position)
797                        if !(mouse_over_x_scrollbar
798                            || mouse_over_y_scrollbar) =>
799                    {
800                        mouse::Cursor::Available(cursor_position + translation)
801                    }
802                    _ => cursor.levitate() + translation,
803                };
804
805                let had_input_method = shell.input_method().is_enabled();
806
807                self.content.as_widget_mut().update(
808                    &mut tree.children[0],
809                    event,
810                    content,
811                    cursor,
812                    renderer,
813                    clipboard,
814                    shell,
815                    &Rectangle {
816                        y: bounds.y + translation.y,
817                        x: bounds.x + translation.x,
818                        ..bounds
819                    },
820                );
821
822                if !had_input_method
823                    && let InputMethod::Enabled { cursor, .. } =
824                        shell.input_method_mut()
825                {
826                    *cursor = *cursor - translation;
827                }
828            };
829
830            if matches!(
831                event,
832                Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
833                    | Event::Touch(
834                        touch::Event::FingerLifted { .. }
835                            | touch::Event::FingerLost { .. }
836                    )
837            ) {
838                state.interaction = Interaction::None;
839                return;
840            }
841
842            if shell.is_event_captured() {
843                return;
844            }
845
846            match event {
847                Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
848                    if cursor_over_scrollable.is_none() {
849                        return;
850                    }
851
852                    let delta = match *delta {
853                        mouse::ScrollDelta::Lines { x, y } => {
854                            let is_shift_pressed =
855                                state.keyboard_modifiers.shift();
856
857                            // macOS automatically inverts the axes when Shift is pressed
858                            let (x, y) = if cfg!(target_os = "macos")
859                                && is_shift_pressed
860                            {
861                                (y, x)
862                            } else {
863                                (x, y)
864                            };
865
866                            let movement = if !is_shift_pressed {
867                                Vector::new(x, y)
868                            } else {
869                                Vector::new(y, x)
870                            };
871
872                            // TODO: Configurable speed/friction (?)
873                            -movement * 60.0
874                        }
875                        mouse::ScrollDelta::Pixels { x, y } => {
876                            -Vector::new(x, y)
877                        }
878                    };
879
880                    state.scroll(
881                        self.direction.align(delta),
882                        bounds,
883                        content_bounds,
884                    );
885
886                    let has_scrolled = notify_scroll(
887                        state,
888                        &self.on_scroll,
889                        bounds,
890                        content_bounds,
891                        shell,
892                    );
893
894                    let in_transaction = state.last_scrolled.is_some();
895
896                    if has_scrolled || in_transaction {
897                        shell.capture_event();
898                    }
899                }
900                Event::Mouse(mouse::Event::ButtonPressed(
901                    mouse::Button::Middle,
902                )) if self.auto_scroll
903                    && matches!(state.interaction, Interaction::None) =>
904                {
905                    let Some(origin) = cursor_over_scrollable else {
906                        return;
907                    };
908
909                    state.interaction = Interaction::AutoScrolling {
910                        origin,
911                        current: origin,
912                        last_frame: None,
913                    };
914
915                    shell.capture_event();
916                    shell.invalidate_layout();
917                    shell.request_redraw();
918                }
919                Event::Touch(event)
920                    if matches!(
921                        state.interaction,
922                        Interaction::TouchScrolling(_)
923                    ) || (!mouse_over_y_scrollbar
924                        && !mouse_over_x_scrollbar) =>
925                {
926                    match event {
927                        touch::Event::FingerPressed { .. } => {
928                            let Some(position) = cursor_over_scrollable else {
929                                return;
930                            };
931
932                            state.interaction =
933                                Interaction::TouchScrolling(position);
934                        }
935                        touch::Event::FingerMoved { .. } => {
936                            let Interaction::TouchScrolling(
937                                scroll_box_touched_at,
938                            ) = state.interaction
939                            else {
940                                return;
941                            };
942
943                            let Some(cursor_position) = cursor.position()
944                            else {
945                                return;
946                            };
947
948                            let delta = Vector::new(
949                                scroll_box_touched_at.x - cursor_position.x,
950                                scroll_box_touched_at.y - cursor_position.y,
951                            );
952
953                            state.scroll(
954                                self.direction.align(delta),
955                                bounds,
956                                content_bounds,
957                            );
958
959                            state.interaction =
960                                Interaction::TouchScrolling(cursor_position);
961
962                            // TODO: bubble up touch movements if not consumed.
963                            let _ = notify_scroll(
964                                state,
965                                &self.on_scroll,
966                                bounds,
967                                content_bounds,
968                                shell,
969                            );
970                        }
971                        _ => {}
972                    }
973
974                    shell.capture_event();
975                }
976                Event::Mouse(mouse::Event::CursorMoved { position }) => {
977                    if let Interaction::AutoScrolling {
978                        origin,
979                        last_frame,
980                        ..
981                    } = state.interaction
982                    {
983                        let delta = *position - origin;
984
985                        state.interaction = Interaction::AutoScrolling {
986                            origin,
987                            current: *position,
988                            last_frame,
989                        };
990
991                        if (delta.x.abs() >= AUTOSCROLL_DEADZONE
992                            || delta.y.abs() >= AUTOSCROLL_DEADZONE)
993                            && last_frame.is_none()
994                        {
995                            shell.request_redraw();
996                        }
997                    }
998                }
999                Event::Keyboard(keyboard::Event::ModifiersChanged(
1000                    modifiers,
1001                )) => {
1002                    state.keyboard_modifiers = *modifiers;
1003                }
1004                Event::Window(window::Event::RedrawRequested(now)) => {
1005                    if let Interaction::AutoScrolling {
1006                        origin,
1007                        current,
1008                        last_frame,
1009                    } = state.interaction
1010                    {
1011                        if last_frame == Some(*now) {
1012                            shell.request_redraw();
1013                            return;
1014                        }
1015
1016                        state.interaction = Interaction::AutoScrolling {
1017                            origin,
1018                            current,
1019                            last_frame: None,
1020                        };
1021
1022                        let mut delta = current - origin;
1023
1024                        if delta.x.abs() < AUTOSCROLL_DEADZONE {
1025                            delta.x = 0.0;
1026                        }
1027
1028                        if delta.y.abs() < AUTOSCROLL_DEADZONE {
1029                            delta.y = 0.0;
1030                        }
1031
1032                        if delta.x != 0.0 || delta.y != 0.0 {
1033                            let time_delta =
1034                                if let Some(last_frame) = last_frame {
1035                                    *now - last_frame
1036                                } else {
1037                                    Duration::ZERO
1038                                };
1039
1040                            let scroll_factor = time_delta.as_secs_f32();
1041
1042                            state.scroll(
1043                                self.direction.align(Vector::new(
1044                                    delta.x.signum()
1045                                        * delta
1046                                            .x
1047                                            .abs()
1048                                            .powf(AUTOSCROLL_SMOOTHNESS)
1049                                        * scroll_factor,
1050                                    delta.y.signum()
1051                                        * delta
1052                                            .y
1053                                            .abs()
1054                                            .powf(AUTOSCROLL_SMOOTHNESS)
1055                                        * scroll_factor,
1056                                )),
1057                                bounds,
1058                                content_bounds,
1059                            );
1060
1061                            let has_scrolled = notify_scroll(
1062                                state,
1063                                &self.on_scroll,
1064                                bounds,
1065                                content_bounds,
1066                                shell,
1067                            );
1068
1069                            if has_scrolled || time_delta.is_zero() {
1070                                state.interaction =
1071                                    Interaction::AutoScrolling {
1072                                        origin,
1073                                        current,
1074                                        last_frame: Some(*now),
1075                                    };
1076
1077                                shell.request_redraw();
1078                            }
1079
1080                            return;
1081                        }
1082                    }
1083
1084                    let _ = notify_viewport(
1085                        state,
1086                        &self.on_scroll,
1087                        bounds,
1088                        content_bounds,
1089                        shell,
1090                    );
1091                }
1092                _ => {}
1093            }
1094        };
1095
1096        update();
1097
1098        let status = if state.scrollers_grabbed() {
1099            Status::Dragged {
1100                is_horizontal_scrollbar_dragged: state
1101                    .x_scroller_grabbed_at()
1102                    .is_some(),
1103                is_vertical_scrollbar_dragged: state
1104                    .y_scroller_grabbed_at()
1105                    .is_some(),
1106                is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
1107                is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
1108            }
1109        } else if cursor_over_scrollable.is_some() {
1110            Status::Hovered {
1111                is_horizontal_scrollbar_hovered: mouse_over_x_scrollbar,
1112                is_vertical_scrollbar_hovered: mouse_over_y_scrollbar,
1113                is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
1114                is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
1115            }
1116        } else {
1117            Status::Active {
1118                is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
1119                is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
1120            }
1121        };
1122
1123        if let Event::Window(window::Event::RedrawRequested(_now)) = event {
1124            self.last_status = Some(status);
1125        }
1126
1127        if last_offsets != (state.offset_x, state.offset_y)
1128            || self
1129                .last_status
1130                .is_some_and(|last_status| last_status != status)
1131        {
1132            shell.request_redraw();
1133        }
1134    }
1135
1136    fn draw(
1137        &self,
1138        tree: &Tree,
1139        renderer: &mut Renderer,
1140        theme: &Theme,
1141        defaults: &renderer::Style,
1142        layout: Layout<'_>,
1143        cursor: mouse::Cursor,
1144        viewport: &Rectangle,
1145    ) {
1146        let state = tree.state.downcast_ref::<State>();
1147
1148        let bounds = layout.bounds();
1149        let content_layout = layout.children().next().unwrap();
1150        let content_bounds = content_layout.bounds();
1151
1152        let Some(visible_bounds) = bounds.intersection(viewport) else {
1153            return;
1154        };
1155
1156        let scrollbars =
1157            Scrollbars::new(state, self.direction, bounds, content_bounds);
1158
1159        let cursor_over_scrollable = cursor.position_over(bounds);
1160        let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
1161            scrollbars.is_mouse_over(cursor);
1162
1163        let translation =
1164            state.translation(self.direction, bounds, content_bounds);
1165
1166        let cursor = match cursor_over_scrollable {
1167            Some(cursor_position)
1168                if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
1169            {
1170                mouse::Cursor::Available(cursor_position + translation)
1171            }
1172            _ => mouse::Cursor::Unavailable,
1173        };
1174
1175        let style = theme.style(
1176            &self.class,
1177            self.last_status.unwrap_or(Status::Active {
1178                is_horizontal_scrollbar_disabled: false,
1179                is_vertical_scrollbar_disabled: false,
1180            }),
1181        );
1182
1183        container::draw_background(renderer, &style.container, layout.bounds());
1184
1185        // Draw inner content
1186        if scrollbars.active() {
1187            renderer.with_layer(visible_bounds, |renderer| {
1188                renderer.with_translation(
1189                    Vector::new(-translation.x, -translation.y),
1190                    |renderer| {
1191                        self.content.as_widget().draw(
1192                            &tree.children[0],
1193                            renderer,
1194                            theme,
1195                            defaults,
1196                            content_layout,
1197                            cursor,
1198                            &Rectangle {
1199                                y: visible_bounds.y + translation.y,
1200                                x: visible_bounds.x + translation.x,
1201                                ..visible_bounds
1202                            },
1203                        );
1204                    },
1205                );
1206            });
1207
1208            let draw_scrollbar =
1209                |renderer: &mut Renderer,
1210                 style: Rail,
1211                 scrollbar: &internals::Scrollbar| {
1212                    if scrollbar.bounds.width > 0.0
1213                        && scrollbar.bounds.height > 0.0
1214                        && (style.background.is_some()
1215                            || (style.border.color != Color::TRANSPARENT
1216                                && style.border.width > 0.0))
1217                    {
1218                        renderer.fill_quad(
1219                            renderer::Quad {
1220                                bounds: scrollbar.bounds,
1221                                border: style.border,
1222                                ..renderer::Quad::default()
1223                            },
1224                            style.background.unwrap_or(Background::Color(
1225                                Color::TRANSPARENT,
1226                            )),
1227                        );
1228                    }
1229
1230                    if let Some(scroller) = scrollbar.scroller
1231                        && scroller.bounds.width > 0.0
1232                        && scroller.bounds.height > 0.0
1233                        && (style.scroller.background
1234                            != Background::Color(Color::TRANSPARENT)
1235                            || (style.scroller.border.color
1236                                != Color::TRANSPARENT
1237                                && style.scroller.border.width > 0.0))
1238                    {
1239                        renderer.fill_quad(
1240                            renderer::Quad {
1241                                bounds: scroller.bounds,
1242                                border: style.scroller.border,
1243                                ..renderer::Quad::default()
1244                            },
1245                            style.scroller.background,
1246                        );
1247                    }
1248                };
1249
1250            renderer.with_layer(
1251                Rectangle {
1252                    width: (visible_bounds.width + 2.0).min(viewport.width),
1253                    height: (visible_bounds.height + 2.0).min(viewport.height),
1254                    ..visible_bounds
1255                },
1256                |renderer| {
1257                    if let Some(scrollbar) = scrollbars.y {
1258                        draw_scrollbar(
1259                            renderer,
1260                            style.vertical_rail,
1261                            &scrollbar,
1262                        );
1263                    }
1264
1265                    if let Some(scrollbar) = scrollbars.x {
1266                        draw_scrollbar(
1267                            renderer,
1268                            style.horizontal_rail,
1269                            &scrollbar,
1270                        );
1271                    }
1272
1273                    if let (Some(x), Some(y)) = (scrollbars.x, scrollbars.y) {
1274                        let background =
1275                            style.gap.or(style.container.background);
1276
1277                        if let Some(background) = background {
1278                            renderer.fill_quad(
1279                                renderer::Quad {
1280                                    bounds: Rectangle {
1281                                        x: y.bounds.x,
1282                                        y: x.bounds.y,
1283                                        width: y.bounds.width,
1284                                        height: x.bounds.height,
1285                                    },
1286                                    ..renderer::Quad::default()
1287                                },
1288                                background,
1289                            );
1290                        }
1291                    }
1292                },
1293            );
1294        } else {
1295            self.content.as_widget().draw(
1296                &tree.children[0],
1297                renderer,
1298                theme,
1299                defaults,
1300                content_layout,
1301                cursor,
1302                &Rectangle {
1303                    x: visible_bounds.x + translation.x,
1304                    y: visible_bounds.y + translation.y,
1305                    ..visible_bounds
1306                },
1307            );
1308        }
1309    }
1310
1311    fn mouse_interaction(
1312        &self,
1313        tree: &Tree,
1314        layout: Layout<'_>,
1315        cursor: mouse::Cursor,
1316        _viewport: &Rectangle,
1317        renderer: &Renderer,
1318    ) -> mouse::Interaction {
1319        let state = tree.state.downcast_ref::<State>();
1320        let bounds = layout.bounds();
1321        let cursor_over_scrollable = cursor.position_over(bounds);
1322
1323        let content_layout = layout.children().next().unwrap();
1324        let content_bounds = content_layout.bounds();
1325
1326        let scrollbars =
1327            Scrollbars::new(state, self.direction, bounds, content_bounds);
1328
1329        let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
1330            scrollbars.is_mouse_over(cursor);
1331
1332        if state.scrollers_grabbed() {
1333            return mouse::Interaction::None;
1334        }
1335
1336        let translation =
1337            state.translation(self.direction, bounds, content_bounds);
1338
1339        let cursor = match cursor_over_scrollable {
1340            Some(cursor_position)
1341                if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
1342            {
1343                mouse::Cursor::Available(cursor_position + translation)
1344            }
1345            _ => cursor.levitate() + translation,
1346        };
1347
1348        self.content.as_widget().mouse_interaction(
1349            &tree.children[0],
1350            content_layout,
1351            cursor,
1352            &Rectangle {
1353                y: bounds.y + translation.y,
1354                x: bounds.x + translation.x,
1355                ..bounds
1356            },
1357            renderer,
1358        )
1359    }
1360
1361    fn overlay<'b>(
1362        &'b mut self,
1363        tree: &'b mut Tree,
1364        layout: Layout<'b>,
1365        renderer: &Renderer,
1366        viewport: &Rectangle,
1367        translation: Vector,
1368    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
1369        let state = tree.state.downcast_ref::<State>();
1370        let bounds = layout.bounds();
1371        let content_layout = layout.children().next().unwrap();
1372        let content_bounds = content_layout.bounds();
1373        let visible_bounds = bounds.intersection(viewport).unwrap_or(*viewport);
1374        let offset = state.translation(self.direction, bounds, content_bounds);
1375
1376        let overlay = self.content.as_widget_mut().overlay(
1377            &mut tree.children[0],
1378            layout.children().next().unwrap(),
1379            renderer,
1380            &visible_bounds,
1381            translation - offset,
1382        );
1383
1384        let icon = if let Interaction::AutoScrolling { origin, .. } =
1385            state.interaction
1386        {
1387            let scrollbars =
1388                Scrollbars::new(state, self.direction, bounds, content_bounds);
1389
1390            Some(overlay::Element::new(Box::new(AutoScrollIcon {
1391                origin,
1392                vertical: scrollbars.y.is_some(),
1393                horizontal: scrollbars.x.is_some(),
1394                class: &self.class,
1395            })))
1396        } else {
1397            None
1398        };
1399
1400        match (overlay, icon) {
1401            (None, None) => None,
1402            (None, Some(icon)) => Some(icon),
1403            (Some(overlay), None) => Some(overlay),
1404            (Some(overlay), Some(icon)) => Some(overlay::Element::new(
1405                Box::new(overlay::Group::with_children(vec![overlay, icon])),
1406            )),
1407        }
1408    }
1409}
1410
1411struct AutoScrollIcon<'a, Class> {
1412    origin: Point,
1413    vertical: bool,
1414    horizontal: bool,
1415    class: &'a Class,
1416}
1417
1418impl<Class> AutoScrollIcon<'_, Class> {
1419    const SIZE: f32 = 40.0;
1420    const DOT: f32 = Self::SIZE / 10.0;
1421    const PADDING: f32 = Self::SIZE / 10.0;
1422}
1423
1424impl<Message, Theme, Renderer> core::Overlay<Message, Theme, Renderer>
1425    for AutoScrollIcon<'_, Theme::Class<'_>>
1426where
1427    Renderer: text::Renderer,
1428    Theme: Catalog,
1429{
1430    fn layout(&mut self, _renderer: &Renderer, _bounds: Size) -> layout::Node {
1431        layout::Node::new(Size::new(Self::SIZE, Self::SIZE))
1432            .move_to(self.origin - Vector::new(Self::SIZE, Self::SIZE) / 2.0)
1433    }
1434
1435    fn draw(
1436        &self,
1437        renderer: &mut Renderer,
1438        theme: &Theme,
1439        _style: &renderer::Style,
1440        layout: Layout<'_>,
1441        _cursor: mouse::Cursor,
1442    ) {
1443        let bounds = layout.bounds();
1444        let style = theme
1445            .style(
1446                self.class,
1447                Status::Active {
1448                    is_horizontal_scrollbar_disabled: false,
1449                    is_vertical_scrollbar_disabled: false,
1450                },
1451            )
1452            .auto_scroll;
1453
1454        renderer.with_layer(Rectangle::INFINITE, |renderer| {
1455            renderer.fill_quad(
1456                renderer::Quad {
1457                    bounds,
1458                    border: style.border,
1459                    shadow: style.shadow,
1460                    snap: false,
1461                },
1462                style.background,
1463            );
1464
1465            renderer.fill_quad(
1466                renderer::Quad {
1467                    bounds: Rectangle::new(
1468                        bounds.center()
1469                            - Vector::new(Self::DOT, Self::DOT) / 2.0,
1470                        Size::new(Self::DOT, Self::DOT),
1471                    ),
1472                    border: border::rounded(bounds.width),
1473                    snap: false,
1474                    ..renderer::Quad::default()
1475                },
1476                style.icon,
1477            );
1478
1479            let arrow = core::Text {
1480                content: String::new(),
1481                bounds: bounds.size(),
1482                size: Pixels::from(12),
1483                line_height: text::LineHeight::Relative(1.0),
1484                font: Renderer::ICON_FONT,
1485                align_x: text::Alignment::Center,
1486                align_y: alignment::Vertical::Center,
1487                shaping: text::Shaping::Basic,
1488                wrapping: text::Wrapping::None,
1489            };
1490
1491            if self.vertical {
1492                renderer.fill_text(
1493                    core::Text {
1494                        content: Renderer::SCROLL_UP_ICON.to_string(),
1495                        align_y: alignment::Vertical::Top,
1496                        ..arrow
1497                    },
1498                    Point::new(bounds.center_x(), bounds.y + Self::PADDING),
1499                    style.icon,
1500                    bounds,
1501                );
1502
1503                renderer.fill_text(
1504                    core::Text {
1505                        content: Renderer::SCROLL_DOWN_ICON.to_string(),
1506                        align_y: alignment::Vertical::Bottom,
1507                        ..arrow
1508                    },
1509                    Point::new(
1510                        bounds.center_x(),
1511                        bounds.y + bounds.height - Self::PADDING - 0.5,
1512                    ),
1513                    style.icon,
1514                    bounds,
1515                );
1516            }
1517
1518            if self.horizontal {
1519                renderer.fill_text(
1520                    core::Text {
1521                        content: Renderer::SCROLL_LEFT_ICON.to_string(),
1522                        align_x: text::Alignment::Left,
1523                        ..arrow
1524                    },
1525                    Point::new(
1526                        bounds.x + Self::PADDING + 1.0,
1527                        bounds.center_y() + 1.0,
1528                    ),
1529                    style.icon,
1530                    bounds,
1531                );
1532
1533                renderer.fill_text(
1534                    core::Text {
1535                        content: Renderer::SCROLL_RIGHT_ICON.to_string(),
1536                        align_x: text::Alignment::Right,
1537                        ..arrow
1538                    },
1539                    Point::new(
1540                        bounds.x + bounds.width - Self::PADDING - 1.0,
1541                        bounds.center_y() + 1.0,
1542                    ),
1543                    style.icon,
1544                    bounds,
1545                );
1546            }
1547        });
1548    }
1549
1550    fn index(&self) -> f32 {
1551        f32::MAX
1552    }
1553}
1554
1555impl<'a, Message, Theme, Renderer>
1556    From<Scrollable<'a, Message, Theme, Renderer>>
1557    for Element<'a, Message, Theme, Renderer>
1558where
1559    Message: 'a,
1560    Theme: 'a + Catalog,
1561    Renderer: 'a + text::Renderer,
1562{
1563    fn from(
1564        text_input: Scrollable<'a, Message, Theme, Renderer>,
1565    ) -> Element<'a, Message, Theme, Renderer> {
1566        Element::new(text_input)
1567    }
1568}
1569
1570fn notify_scroll<Message>(
1571    state: &mut State,
1572    on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
1573    bounds: Rectangle,
1574    content_bounds: Rectangle,
1575    shell: &mut Shell<'_, Message>,
1576) -> bool {
1577    if notify_viewport(state, on_scroll, bounds, content_bounds, shell) {
1578        state.last_scrolled = Some(Instant::now());
1579
1580        true
1581    } else {
1582        false
1583    }
1584}
1585
1586fn notify_viewport<Message>(
1587    state: &mut State,
1588    on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
1589    bounds: Rectangle,
1590    content_bounds: Rectangle,
1591    shell: &mut Shell<'_, Message>,
1592) -> bool {
1593    if content_bounds.width <= bounds.width
1594        && content_bounds.height <= bounds.height
1595    {
1596        return false;
1597    }
1598
1599    let viewport = Viewport {
1600        offset_x: state.offset_x,
1601        offset_y: state.offset_y,
1602        bounds,
1603        content_bounds,
1604    };
1605
1606    // Don't publish redundant viewports to shell
1607    if let Some(last_notified) = state.last_notified {
1608        let last_relative_offset = last_notified.relative_offset();
1609        let current_relative_offset = viewport.relative_offset();
1610
1611        let last_absolute_offset = last_notified.absolute_offset();
1612        let current_absolute_offset = viewport.absolute_offset();
1613
1614        let unchanged = |a: f32, b: f32| {
1615            (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan())
1616        };
1617
1618        if last_notified.bounds == bounds
1619            && last_notified.content_bounds == content_bounds
1620            && unchanged(last_relative_offset.x, current_relative_offset.x)
1621            && unchanged(last_relative_offset.y, current_relative_offset.y)
1622            && unchanged(last_absolute_offset.x, current_absolute_offset.x)
1623            && unchanged(last_absolute_offset.y, current_absolute_offset.y)
1624        {
1625            return false;
1626        }
1627    }
1628
1629    state.last_notified = Some(viewport);
1630
1631    if let Some(on_scroll) = on_scroll {
1632        shell.publish(on_scroll(viewport));
1633    }
1634
1635    true
1636}
1637
1638#[derive(Debug, Clone, Copy)]
1639struct State {
1640    offset_y: Offset,
1641    offset_x: Offset,
1642    interaction: Interaction,
1643    keyboard_modifiers: keyboard::Modifiers,
1644    last_notified: Option<Viewport>,
1645    last_scrolled: Option<Instant>,
1646    is_scrollbar_visible: bool,
1647}
1648
1649#[derive(Debug, Clone, Copy)]
1650enum Interaction {
1651    None,
1652    YScrollerGrabbed(f32),
1653    XScrollerGrabbed(f32),
1654    TouchScrolling(Point),
1655    AutoScrolling {
1656        origin: Point,
1657        current: Point,
1658        last_frame: Option<Instant>,
1659    },
1660}
1661
1662impl Default for State {
1663    fn default() -> Self {
1664        Self {
1665            offset_y: Offset::Absolute(0.0),
1666            offset_x: Offset::Absolute(0.0),
1667            interaction: Interaction::None,
1668            keyboard_modifiers: keyboard::Modifiers::default(),
1669            last_notified: None,
1670            last_scrolled: None,
1671            is_scrollbar_visible: true,
1672        }
1673    }
1674}
1675
1676impl operation::Scrollable for State {
1677    fn snap_to(&mut self, offset: RelativeOffset<Option<f32>>) {
1678        State::snap_to(self, offset);
1679    }
1680
1681    fn scroll_to(&mut self, offset: AbsoluteOffset<Option<f32>>) {
1682        State::scroll_to(self, offset);
1683    }
1684
1685    fn scroll_by(
1686        &mut self,
1687        offset: AbsoluteOffset,
1688        bounds: Rectangle,
1689        content_bounds: Rectangle,
1690    ) {
1691        State::scroll_by(self, offset, bounds, content_bounds);
1692    }
1693}
1694
1695#[derive(Debug, Clone, Copy, PartialEq)]
1696enum Offset {
1697    Absolute(f32),
1698    Relative(f32),
1699}
1700
1701impl Offset {
1702    fn absolute(self, viewport: f32, content: f32) -> f32 {
1703        match self {
1704            Offset::Absolute(absolute) => {
1705                absolute.min((content - viewport).max(0.0))
1706            }
1707            Offset::Relative(percentage) => {
1708                ((content - viewport) * percentage).max(0.0)
1709            }
1710        }
1711    }
1712
1713    fn translation(
1714        self,
1715        viewport: f32,
1716        content: f32,
1717        alignment: Anchor,
1718    ) -> f32 {
1719        let offset = self.absolute(viewport, content);
1720
1721        match alignment {
1722            Anchor::Start => offset,
1723            Anchor::End => ((content - viewport).max(0.0) - offset).max(0.0),
1724        }
1725    }
1726}
1727
1728/// The current [`Viewport`] of the [`Scrollable`].
1729#[derive(Debug, Clone, Copy)]
1730pub struct Viewport {
1731    offset_x: Offset,
1732    offset_y: Offset,
1733    bounds: Rectangle,
1734    content_bounds: Rectangle,
1735}
1736
1737impl Viewport {
1738    /// Returns the [`AbsoluteOffset`] of the current [`Viewport`].
1739    pub fn absolute_offset(&self) -> AbsoluteOffset {
1740        let x = self
1741            .offset_x
1742            .absolute(self.bounds.width, self.content_bounds.width);
1743        let y = self
1744            .offset_y
1745            .absolute(self.bounds.height, self.content_bounds.height);
1746
1747        AbsoluteOffset { x, y }
1748    }
1749
1750    /// Returns the [`AbsoluteOffset`] of the current [`Viewport`], but with its
1751    /// alignment reversed.
1752    ///
1753    /// This method can be useful to switch the alignment of a [`Scrollable`]
1754    /// while maintaining its scrolling position.
1755    pub fn absolute_offset_reversed(&self) -> AbsoluteOffset {
1756        let AbsoluteOffset { x, y } = self.absolute_offset();
1757
1758        AbsoluteOffset {
1759            x: (self.content_bounds.width - self.bounds.width).max(0.0) - x,
1760            y: (self.content_bounds.height - self.bounds.height).max(0.0) - y,
1761        }
1762    }
1763
1764    /// Returns the [`RelativeOffset`] of the current [`Viewport`].
1765    pub fn relative_offset(&self) -> RelativeOffset {
1766        let AbsoluteOffset { x, y } = self.absolute_offset();
1767
1768        let x = x / (self.content_bounds.width - self.bounds.width);
1769        let y = y / (self.content_bounds.height - self.bounds.height);
1770
1771        RelativeOffset { x, y }
1772    }
1773
1774    /// Returns the bounds of the current [`Viewport`].
1775    pub fn bounds(&self) -> Rectangle {
1776        self.bounds
1777    }
1778
1779    /// Returns the content bounds of the current [`Viewport`].
1780    pub fn content_bounds(&self) -> Rectangle {
1781        self.content_bounds
1782    }
1783}
1784
1785impl State {
1786    fn new() -> Self {
1787        State::default()
1788    }
1789
1790    fn scroll(
1791        &mut self,
1792        delta: Vector<f32>,
1793        bounds: Rectangle,
1794        content_bounds: Rectangle,
1795    ) {
1796        if bounds.height < content_bounds.height {
1797            self.offset_y = Offset::Absolute(
1798                (self.offset_y.absolute(bounds.height, content_bounds.height)
1799                    + delta.y)
1800                    .clamp(0.0, content_bounds.height - bounds.height),
1801            );
1802        }
1803
1804        if bounds.width < content_bounds.width {
1805            self.offset_x = Offset::Absolute(
1806                (self.offset_x.absolute(bounds.width, content_bounds.width)
1807                    + delta.x)
1808                    .clamp(0.0, content_bounds.width - bounds.width),
1809            );
1810        }
1811    }
1812
1813    fn scroll_y_to(
1814        &mut self,
1815        percentage: f32,
1816        bounds: Rectangle,
1817        content_bounds: Rectangle,
1818    ) {
1819        self.offset_y = Offset::Relative(percentage.clamp(0.0, 1.0));
1820        self.unsnap(bounds, content_bounds);
1821    }
1822
1823    fn scroll_x_to(
1824        &mut self,
1825        percentage: f32,
1826        bounds: Rectangle,
1827        content_bounds: Rectangle,
1828    ) {
1829        self.offset_x = Offset::Relative(percentage.clamp(0.0, 1.0));
1830        self.unsnap(bounds, content_bounds);
1831    }
1832
1833    fn snap_to(&mut self, offset: RelativeOffset<Option<f32>>) {
1834        if let Some(x) = offset.x {
1835            self.offset_x = Offset::Relative(x.clamp(0.0, 1.0));
1836        }
1837
1838        if let Some(y) = offset.y {
1839            self.offset_y = Offset::Relative(y.clamp(0.0, 1.0));
1840        }
1841    }
1842
1843    fn scroll_to(&mut self, offset: AbsoluteOffset<Option<f32>>) {
1844        if let Some(x) = offset.x {
1845            self.offset_x = Offset::Absolute(x.max(0.0));
1846        }
1847
1848        if let Some(y) = offset.y {
1849            self.offset_y = Offset::Absolute(y.max(0.0));
1850        }
1851    }
1852
1853    /// Scroll by the provided [`AbsoluteOffset`].
1854    fn scroll_by(
1855        &mut self,
1856        offset: AbsoluteOffset,
1857        bounds: Rectangle,
1858        content_bounds: Rectangle,
1859    ) {
1860        self.scroll(Vector::new(offset.x, offset.y), bounds, content_bounds);
1861    }
1862
1863    /// Unsnaps the current scroll position, if snapped, given the bounds of the
1864    /// [`Scrollable`] and its contents.
1865    fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) {
1866        self.offset_x = Offset::Absolute(
1867            self.offset_x.absolute(bounds.width, content_bounds.width),
1868        );
1869        self.offset_y = Offset::Absolute(
1870            self.offset_y.absolute(bounds.height, content_bounds.height),
1871        );
1872    }
1873
1874    /// Returns the scrolling translation of the [`State`], given a [`Direction`],
1875    /// the bounds of the [`Scrollable`] and its contents.
1876    fn translation(
1877        &self,
1878        direction: Direction,
1879        bounds: Rectangle,
1880        content_bounds: Rectangle,
1881    ) -> Vector {
1882        Vector::new(
1883            if let Some(horizontal) = direction.horizontal() {
1884                self.offset_x
1885                    .translation(
1886                        bounds.width,
1887                        content_bounds.width,
1888                        horizontal.alignment,
1889                    )
1890                    .round()
1891            } else {
1892                0.0
1893            },
1894            if let Some(vertical) = direction.vertical() {
1895                self.offset_y
1896                    .translation(
1897                        bounds.height,
1898                        content_bounds.height,
1899                        vertical.alignment,
1900                    )
1901                    .round()
1902            } else {
1903                0.0
1904            },
1905        )
1906    }
1907
1908    fn scrollers_grabbed(&self) -> bool {
1909        matches!(
1910            self.interaction,
1911            Interaction::YScrollerGrabbed(_) | Interaction::XScrollerGrabbed(_),
1912        )
1913    }
1914
1915    pub fn y_scroller_grabbed_at(&self) -> Option<f32> {
1916        let Interaction::YScrollerGrabbed(at) = self.interaction else {
1917            return None;
1918        };
1919
1920        Some(at)
1921    }
1922
1923    pub fn x_scroller_grabbed_at(&self) -> Option<f32> {
1924        let Interaction::XScrollerGrabbed(at) = self.interaction else {
1925            return None;
1926        };
1927
1928        Some(at)
1929    }
1930}
1931
1932#[derive(Debug)]
1933/// State of both [`Scrollbar`]s.
1934struct Scrollbars {
1935    y: Option<internals::Scrollbar>,
1936    x: Option<internals::Scrollbar>,
1937}
1938
1939impl Scrollbars {
1940    /// Create y and/or x scrollbar(s) if content is overflowing the [`Scrollable`] bounds.
1941    fn new(
1942        state: &State,
1943        direction: Direction,
1944        bounds: Rectangle,
1945        content_bounds: Rectangle,
1946    ) -> Self {
1947        let translation = state.translation(direction, bounds, content_bounds);
1948
1949        let show_scrollbar_x = direction
1950            .horizontal()
1951            .filter(|_scrollbar| content_bounds.width > bounds.width);
1952
1953        let show_scrollbar_y = direction
1954            .vertical()
1955            .filter(|_scrollbar| content_bounds.height > bounds.height);
1956
1957        let y_scrollbar = if let Some(vertical) = show_scrollbar_y {
1958            let Scrollbar {
1959                width,
1960                margin,
1961                scroller_width,
1962                ..
1963            } = *vertical;
1964
1965            // Adjust the height of the vertical scrollbar if the horizontal scrollbar
1966            // is present
1967            let x_scrollbar_height = show_scrollbar_x
1968                .map_or(0.0, |h| h.width.max(h.scroller_width) + h.margin);
1969
1970            let total_scrollbar_width =
1971                width.max(scroller_width) + 2.0 * margin;
1972
1973            // Total bounds of the scrollbar + margin + scroller width
1974            let total_scrollbar_bounds = Rectangle {
1975                x: bounds.x + bounds.width - total_scrollbar_width,
1976                y: bounds.y,
1977                width: total_scrollbar_width,
1978                height: (bounds.height - x_scrollbar_height).max(0.0),
1979            };
1980
1981            // Bounds of just the scrollbar
1982            let scrollbar_bounds = Rectangle {
1983                x: bounds.x + bounds.width
1984                    - total_scrollbar_width / 2.0
1985                    - width / 2.0,
1986                y: bounds.y,
1987                width,
1988                height: (bounds.height - x_scrollbar_height).max(0.0),
1989            };
1990
1991            let ratio = bounds.height / content_bounds.height;
1992
1993            let scroller = if ratio >= 1.0 {
1994                None
1995            } else {
1996                // min height for easier grabbing with super tall content
1997                let scroller_height =
1998                    (scrollbar_bounds.height * ratio).max(2.0);
1999                let scroller_offset =
2000                    translation.y * ratio * scrollbar_bounds.height
2001                        / bounds.height;
2002
2003                let scroller_bounds = Rectangle {
2004                    x: bounds.x + bounds.width
2005                        - total_scrollbar_width / 2.0
2006                        - scroller_width / 2.0,
2007                    y: (scrollbar_bounds.y + scroller_offset).max(0.0),
2008                    width: scroller_width,
2009                    height: scroller_height,
2010                };
2011
2012                Some(internals::Scroller {
2013                    bounds: scroller_bounds,
2014                })
2015            };
2016
2017            Some(internals::Scrollbar {
2018                total_bounds: total_scrollbar_bounds,
2019                bounds: scrollbar_bounds,
2020                scroller,
2021                alignment: vertical.alignment,
2022                disabled: content_bounds.height <= bounds.height,
2023            })
2024        } else {
2025            None
2026        };
2027
2028        let x_scrollbar = if let Some(horizontal) = show_scrollbar_x {
2029            let Scrollbar {
2030                width,
2031                margin,
2032                scroller_width,
2033                ..
2034            } = *horizontal;
2035
2036            // Need to adjust the width of the horizontal scrollbar if the vertical scrollbar
2037            // is present
2038            let scrollbar_y_width = y_scrollbar
2039                .map_or(0.0, |scrollbar| scrollbar.total_bounds.width);
2040
2041            let total_scrollbar_height =
2042                width.max(scroller_width) + 2.0 * margin;
2043
2044            // Total bounds of the scrollbar + margin + scroller width
2045            let total_scrollbar_bounds = Rectangle {
2046                x: bounds.x,
2047                y: bounds.y + bounds.height - total_scrollbar_height,
2048                width: (bounds.width - scrollbar_y_width).max(0.0),
2049                height: total_scrollbar_height,
2050            };
2051
2052            // Bounds of just the scrollbar
2053            let scrollbar_bounds = Rectangle {
2054                x: bounds.x,
2055                y: bounds.y + bounds.height
2056                    - total_scrollbar_height / 2.0
2057                    - width / 2.0,
2058                width: (bounds.width - scrollbar_y_width).max(0.0),
2059                height: width,
2060            };
2061
2062            let ratio = bounds.width / content_bounds.width;
2063
2064            let scroller = if ratio >= 1.0 {
2065                None
2066            } else {
2067                // min width for easier grabbing with extra wide content
2068                let scroller_length = (scrollbar_bounds.width * ratio).max(2.0);
2069                let scroller_offset =
2070                    translation.x * ratio * scrollbar_bounds.width
2071                        / bounds.width;
2072
2073                let scroller_bounds = Rectangle {
2074                    x: (scrollbar_bounds.x + scroller_offset).max(0.0),
2075                    y: bounds.y + bounds.height
2076                        - total_scrollbar_height / 2.0
2077                        - scroller_width / 2.0,
2078                    width: scroller_length,
2079                    height: scroller_width,
2080                };
2081
2082                Some(internals::Scroller {
2083                    bounds: scroller_bounds,
2084                })
2085            };
2086
2087            Some(internals::Scrollbar {
2088                total_bounds: total_scrollbar_bounds,
2089                bounds: scrollbar_bounds,
2090                scroller,
2091                alignment: horizontal.alignment,
2092                disabled: content_bounds.width <= bounds.width,
2093            })
2094        } else {
2095            None
2096        };
2097
2098        Self {
2099            y: y_scrollbar,
2100            x: x_scrollbar,
2101        }
2102    }
2103
2104    fn is_mouse_over(&self, cursor: mouse::Cursor) -> (bool, bool) {
2105        if let Some(cursor_position) = cursor.position() {
2106            (
2107                self.y
2108                    .as_ref()
2109                    .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
2110                    .unwrap_or(false),
2111                self.x
2112                    .as_ref()
2113                    .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
2114                    .unwrap_or(false),
2115            )
2116        } else {
2117            (false, false)
2118        }
2119    }
2120
2121    fn is_y_disabled(&self) -> bool {
2122        self.y.map(|y| y.disabled).unwrap_or(false)
2123    }
2124
2125    fn is_x_disabled(&self) -> bool {
2126        self.x.map(|x| x.disabled).unwrap_or(false)
2127    }
2128
2129    fn grab_y_scroller(&self, cursor_position: Point) -> Option<f32> {
2130        let scrollbar = self.y?;
2131        let scroller = scrollbar.scroller?;
2132
2133        if scrollbar.total_bounds.contains(cursor_position) {
2134            Some(if scroller.bounds.contains(cursor_position) {
2135                (cursor_position.y - scroller.bounds.y) / scroller.bounds.height
2136            } else {
2137                0.5
2138            })
2139        } else {
2140            None
2141        }
2142    }
2143
2144    fn grab_x_scroller(&self, cursor_position: Point) -> Option<f32> {
2145        let scrollbar = self.x?;
2146        let scroller = scrollbar.scroller?;
2147
2148        if scrollbar.total_bounds.contains(cursor_position) {
2149            Some(if scroller.bounds.contains(cursor_position) {
2150                (cursor_position.x - scroller.bounds.x) / scroller.bounds.width
2151            } else {
2152                0.5
2153            })
2154        } else {
2155            None
2156        }
2157    }
2158
2159    fn active(&self) -> bool {
2160        self.y.is_some() || self.x.is_some()
2161    }
2162}
2163
2164pub(super) mod internals {
2165    use crate::core::{Point, Rectangle};
2166
2167    use super::Anchor;
2168
2169    #[derive(Debug, Copy, Clone)]
2170    pub struct Scrollbar {
2171        pub total_bounds: Rectangle,
2172        pub bounds: Rectangle,
2173        pub scroller: Option<Scroller>,
2174        pub alignment: Anchor,
2175        pub disabled: bool,
2176    }
2177
2178    impl Scrollbar {
2179        /// Returns whether the mouse is over the scrollbar or not.
2180        pub fn is_mouse_over(&self, cursor_position: Point) -> bool {
2181            self.total_bounds.contains(cursor_position)
2182        }
2183
2184        /// Returns the y-axis scrolled percentage from the cursor position.
2185        pub fn scroll_percentage_y(
2186            &self,
2187            grabbed_at: f32,
2188            cursor_position: Point,
2189        ) -> f32 {
2190            if let Some(scroller) = self.scroller {
2191                let percentage = (cursor_position.y
2192                    - self.bounds.y
2193                    - scroller.bounds.height * grabbed_at)
2194                    / (self.bounds.height - scroller.bounds.height);
2195
2196                match self.alignment {
2197                    Anchor::Start => percentage,
2198                    Anchor::End => 1.0 - percentage,
2199                }
2200            } else {
2201                0.0
2202            }
2203        }
2204
2205        /// Returns the x-axis scrolled percentage from the cursor position.
2206        pub fn scroll_percentage_x(
2207            &self,
2208            grabbed_at: f32,
2209            cursor_position: Point,
2210        ) -> f32 {
2211            if let Some(scroller) = self.scroller {
2212                let percentage = (cursor_position.x
2213                    - self.bounds.x
2214                    - scroller.bounds.width * grabbed_at)
2215                    / (self.bounds.width - scroller.bounds.width);
2216
2217                match self.alignment {
2218                    Anchor::Start => percentage,
2219                    Anchor::End => 1.0 - percentage,
2220                }
2221            } else {
2222                0.0
2223            }
2224        }
2225    }
2226
2227    /// The handle of a [`Scrollbar`].
2228    #[derive(Debug, Clone, Copy)]
2229    pub struct Scroller {
2230        /// The bounds of the [`Scroller`].
2231        pub bounds: Rectangle,
2232    }
2233}
2234
2235/// The possible status of a [`Scrollable`].
2236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2237pub enum Status {
2238    /// The [`Scrollable`] can be interacted with.
2239    Active {
2240        /// Whether or not the horizontal scrollbar is disabled meaning the content isn't overflowing.
2241        is_horizontal_scrollbar_disabled: bool,
2242        /// Whether or not the vertical scrollbar is disabled meaning the content isn't overflowing.
2243        is_vertical_scrollbar_disabled: bool,
2244    },
2245    /// The [`Scrollable`] is being hovered.
2246    Hovered {
2247        /// Indicates if the horizontal scrollbar is being hovered.
2248        is_horizontal_scrollbar_hovered: bool,
2249        /// Indicates if the vertical scrollbar is being hovered.
2250        is_vertical_scrollbar_hovered: bool,
2251        /// Whether or not the horizontal scrollbar is disabled meaning the content isn't overflowing.
2252        is_horizontal_scrollbar_disabled: bool,
2253        /// Whether or not the vertical scrollbar is disabled meaning the content isn't overflowing.
2254        is_vertical_scrollbar_disabled: bool,
2255    },
2256    /// The [`Scrollable`] is being dragged.
2257    Dragged {
2258        /// Indicates if the horizontal scrollbar is being dragged.
2259        is_horizontal_scrollbar_dragged: bool,
2260        /// Indicates if the vertical scrollbar is being dragged.
2261        is_vertical_scrollbar_dragged: bool,
2262        /// Whether or not the horizontal scrollbar is disabled meaning the content isn't overflowing.
2263        is_horizontal_scrollbar_disabled: bool,
2264        /// Whether or not the vertical scrollbar is disabled meaning the content isn't overflowing.
2265        is_vertical_scrollbar_disabled: bool,
2266    },
2267}
2268
2269/// The appearance of a scrollable.
2270#[derive(Debug, Clone, Copy, PartialEq)]
2271pub struct Style {
2272    /// The [`container::Style`] of a scrollable.
2273    pub container: container::Style,
2274    /// The vertical [`Rail`] appearance.
2275    pub vertical_rail: Rail,
2276    /// The horizontal [`Rail`] appearance.
2277    pub horizontal_rail: Rail,
2278    /// The [`Background`] of the gap between a horizontal and vertical scrollbar.
2279    pub gap: Option<Background>,
2280    /// The appearance of the [`AutoScroll`] overlay.
2281    pub auto_scroll: AutoScroll,
2282}
2283
2284/// The appearance of the scrollbar of a scrollable.
2285#[derive(Debug, Clone, Copy, PartialEq)]
2286pub struct Rail {
2287    /// The [`Background`] of a scrollbar.
2288    pub background: Option<Background>,
2289    /// The [`Border`] of a scrollbar.
2290    pub border: Border,
2291    /// The appearance of the [`Scroller`] of a scrollbar.
2292    pub scroller: Scroller,
2293}
2294
2295/// The appearance of the scroller of a scrollable.
2296#[derive(Debug, Clone, Copy, PartialEq)]
2297pub struct Scroller {
2298    /// The [`Background`] of the scroller.
2299    pub background: Background,
2300    /// The [`Border`] of the scroller.
2301    pub border: Border,
2302}
2303
2304/// The appearance of the autoscroll overlay of a scrollable.
2305#[derive(Debug, Clone, Copy, PartialEq)]
2306pub struct AutoScroll {
2307    /// The [`Background`] of the [`AutoScroll`] overlay.
2308    pub background: Background,
2309    /// The [`Border`] of the [`AutoScroll`] overlay.
2310    pub border: Border,
2311    /// Thje [`Shadow`] of the [`AutoScroll`] overlay.
2312    pub shadow: Shadow,
2313    /// The [`Color`] for the arrow icons of the [`AutoScroll`] overlay.
2314    pub icon: Color,
2315}
2316
2317/// The theme catalog of a [`Scrollable`].
2318pub trait Catalog {
2319    /// The item class of the [`Catalog`].
2320    type Class<'a>;
2321
2322    /// The default class produced by the [`Catalog`].
2323    fn default<'a>() -> Self::Class<'a>;
2324
2325    /// The [`Style`] of a class with the given status.
2326    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
2327}
2328
2329/// A styling function for a [`Scrollable`].
2330pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
2331
2332impl Catalog for Theme {
2333    type Class<'a> = StyleFn<'a, Self>;
2334
2335    fn default<'a>() -> Self::Class<'a> {
2336        Box::new(default)
2337    }
2338
2339    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
2340        class(self, status)
2341    }
2342}
2343
2344/// The default style of a [`Scrollable`].
2345pub fn default(theme: &Theme, status: Status) -> Style {
2346    let palette = theme.extended_palette();
2347
2348    let scrollbar = Rail {
2349        background: Some(palette.background.weak.color.into()),
2350        border: border::rounded(2),
2351        scroller: Scroller {
2352            background: palette.background.strongest.color.into(),
2353            border: border::rounded(2),
2354        },
2355    };
2356
2357    let auto_scroll = AutoScroll {
2358        background: palette.background.base.color.scale_alpha(0.9).into(),
2359        border: border::rounded(u32::MAX)
2360            .width(1)
2361            .color(palette.background.base.text.scale_alpha(0.8)),
2362        shadow: Shadow {
2363            color: Color::BLACK.scale_alpha(0.7),
2364            offset: Vector::ZERO,
2365            blur_radius: 2.0,
2366        },
2367        icon: palette.background.base.text.scale_alpha(0.8),
2368    };
2369
2370    match status {
2371        Status::Active { .. } => Style {
2372            container: container::Style::default(),
2373            vertical_rail: scrollbar,
2374            horizontal_rail: scrollbar,
2375            gap: None,
2376            auto_scroll,
2377        },
2378        Status::Hovered {
2379            is_horizontal_scrollbar_hovered,
2380            is_vertical_scrollbar_hovered,
2381            ..
2382        } => {
2383            let hovered_scrollbar = Rail {
2384                scroller: Scroller {
2385                    background: palette.primary.strong.color.into(),
2386                    ..scrollbar.scroller
2387                },
2388                ..scrollbar
2389            };
2390
2391            Style {
2392                container: container::Style::default(),
2393                vertical_rail: if is_vertical_scrollbar_hovered {
2394                    hovered_scrollbar
2395                } else {
2396                    scrollbar
2397                },
2398                horizontal_rail: if is_horizontal_scrollbar_hovered {
2399                    hovered_scrollbar
2400                } else {
2401                    scrollbar
2402                },
2403                gap: None,
2404                auto_scroll,
2405            }
2406        }
2407        Status::Dragged {
2408            is_horizontal_scrollbar_dragged,
2409            is_vertical_scrollbar_dragged,
2410            ..
2411        } => {
2412            let dragged_scrollbar = Rail {
2413                scroller: Scroller {
2414                    background: palette.primary.base.color.into(),
2415                    ..scrollbar.scroller
2416                },
2417                ..scrollbar
2418            };
2419
2420            Style {
2421                container: container::Style::default(),
2422                vertical_rail: if is_vertical_scrollbar_dragged {
2423                    dragged_scrollbar
2424                } else {
2425                    scrollbar
2426                },
2427                horizontal_rail: if is_horizontal_scrollbar_dragged {
2428                    dragged_scrollbar
2429                } else {
2430                    scrollbar
2431                },
2432                gap: None,
2433                auto_scroll,
2434            }
2435        }
2436    }
2437}