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