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