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