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 translation =
752                    state.translation(self.direction, bounds, content_bounds);
753
754                let cursor = match cursor_over_scrollable {
755                    Some(cursor_position)
756                        if !(mouse_over_x_scrollbar
757                            || mouse_over_y_scrollbar) =>
758                    {
759                        mouse::Cursor::Available(cursor_position + translation)
760                    }
761                    _ => cursor.levitate() + translation,
762                };
763
764                let had_input_method = shell.input_method().is_enabled();
765
766                self.content.as_widget_mut().update(
767                    &mut tree.children[0],
768                    event,
769                    content,
770                    cursor,
771                    renderer,
772                    clipboard,
773                    shell,
774                    &Rectangle {
775                        y: bounds.y + translation.y,
776                        x: bounds.x + translation.x,
777                        ..bounds
778                    },
779                );
780
781                if !had_input_method
782                    && let InputMethod::Enabled { position, .. } =
783                        shell.input_method_mut()
784                {
785                    *position = *position - translation;
786                }
787            };
788
789            if matches!(
790                event,
791                Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
792                    | Event::Touch(
793                        touch::Event::FingerLifted { .. }
794                            | touch::Event::FingerLost { .. }
795                    )
796            ) {
797                state.scroll_area_touched_at = None;
798                state.x_scroller_grabbed_at = None;
799                state.y_scroller_grabbed_at = None;
800
801                return;
802            }
803
804            if shell.is_event_captured() {
805                return;
806            }
807
808            if let Event::Keyboard(keyboard::Event::ModifiersChanged(
809                modifiers,
810            )) = event
811            {
812                state.keyboard_modifiers = *modifiers;
813
814                return;
815            }
816
817            match event {
818                Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
819                    if cursor_over_scrollable.is_none() {
820                        return;
821                    }
822
823                    let delta = match *delta {
824                        mouse::ScrollDelta::Lines { x, y } => {
825                            let is_shift_pressed =
826                                state.keyboard_modifiers.shift();
827
828                            // macOS automatically inverts the axes when Shift is pressed
829                            let (x, y) = if cfg!(target_os = "macos")
830                                && is_shift_pressed
831                            {
832                                (y, x)
833                            } else {
834                                (x, y)
835                            };
836
837                            let movement = if !is_shift_pressed {
838                                Vector::new(x, y)
839                            } else {
840                                Vector::new(y, x)
841                            };
842
843                            // TODO: Configurable speed/friction (?)
844                            -movement * 60.0
845                        }
846                        mouse::ScrollDelta::Pixels { x, y } => {
847                            -Vector::new(x, y)
848                        }
849                    };
850
851                    state.scroll(
852                        self.direction.align(delta),
853                        bounds,
854                        content_bounds,
855                    );
856
857                    let has_scrolled = notify_scroll(
858                        state,
859                        &self.on_scroll,
860                        bounds,
861                        content_bounds,
862                        shell,
863                    );
864
865                    let in_transaction = state.last_scrolled.is_some();
866
867                    if has_scrolled || in_transaction {
868                        shell.capture_event();
869                    }
870                }
871                Event::Touch(event)
872                    if state.scroll_area_touched_at.is_some()
873                        || !mouse_over_y_scrollbar
874                            && !mouse_over_x_scrollbar =>
875                {
876                    match event {
877                        touch::Event::FingerPressed { .. } => {
878                            let Some(cursor_position) = cursor.position()
879                            else {
880                                return;
881                            };
882
883                            state.scroll_area_touched_at =
884                                Some(cursor_position);
885                        }
886                        touch::Event::FingerMoved { .. } => {
887                            if let Some(scroll_box_touched_at) =
888                                state.scroll_area_touched_at
889                            {
890                                let Some(cursor_position) = cursor.position()
891                                else {
892                                    return;
893                                };
894
895                                let delta = Vector::new(
896                                    scroll_box_touched_at.x - cursor_position.x,
897                                    scroll_box_touched_at.y - cursor_position.y,
898                                );
899
900                                state.scroll(
901                                    self.direction.align(delta),
902                                    bounds,
903                                    content_bounds,
904                                );
905
906                                state.scroll_area_touched_at =
907                                    Some(cursor_position);
908
909                                // TODO: bubble up touch movements if not consumed.
910                                let _ = notify_scroll(
911                                    state,
912                                    &self.on_scroll,
913                                    bounds,
914                                    content_bounds,
915                                    shell,
916                                );
917                            }
918                        }
919                        _ => {}
920                    }
921
922                    shell.capture_event();
923                }
924                Event::Window(window::Event::RedrawRequested(_)) => {
925                    let _ = notify_viewport(
926                        state,
927                        &self.on_scroll,
928                        bounds,
929                        content_bounds,
930                        shell,
931                    );
932                }
933                _ => {}
934            }
935        };
936
937        update();
938
939        let status = if state.y_scroller_grabbed_at.is_some()
940            || state.x_scroller_grabbed_at.is_some()
941        {
942            Status::Dragged {
943                is_horizontal_scrollbar_dragged: state
944                    .x_scroller_grabbed_at
945                    .is_some(),
946                is_vertical_scrollbar_dragged: state
947                    .y_scroller_grabbed_at
948                    .is_some(),
949                is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
950                is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
951            }
952        } else if cursor_over_scrollable.is_some() {
953            Status::Hovered {
954                is_horizontal_scrollbar_hovered: mouse_over_x_scrollbar,
955                is_vertical_scrollbar_hovered: mouse_over_y_scrollbar,
956                is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
957                is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
958            }
959        } else {
960            Status::Active {
961                is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
962                is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
963            }
964        };
965
966        if let Event::Window(window::Event::RedrawRequested(_now)) = event {
967            self.last_status = Some(status);
968        }
969
970        if last_offsets != (state.offset_x, state.offset_y)
971            || self
972                .last_status
973                .is_some_and(|last_status| last_status != status)
974        {
975            shell.request_redraw();
976        }
977    }
978
979    fn draw(
980        &self,
981        tree: &Tree,
982        renderer: &mut Renderer,
983        theme: &Theme,
984        defaults: &renderer::Style,
985        layout: Layout<'_>,
986        cursor: mouse::Cursor,
987        viewport: &Rectangle,
988    ) {
989        let state = tree.state.downcast_ref::<State>();
990
991        let bounds = layout.bounds();
992        let content_layout = layout.children().next().unwrap();
993        let content_bounds = content_layout.bounds();
994
995        let Some(visible_bounds) = bounds.intersection(viewport) else {
996            return;
997        };
998
999        let scrollbars =
1000            Scrollbars::new(state, self.direction, bounds, content_bounds);
1001
1002        let cursor_over_scrollable = cursor.position_over(bounds);
1003        let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
1004            scrollbars.is_mouse_over(cursor);
1005
1006        let translation =
1007            state.translation(self.direction, bounds, content_bounds);
1008
1009        let cursor = match cursor_over_scrollable {
1010            Some(cursor_position)
1011                if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
1012            {
1013                mouse::Cursor::Available(cursor_position + translation)
1014            }
1015            _ => mouse::Cursor::Unavailable,
1016        };
1017
1018        let style = theme.style(
1019            &self.class,
1020            self.last_status.unwrap_or(Status::Active {
1021                is_horizontal_scrollbar_disabled: false,
1022                is_vertical_scrollbar_disabled: false,
1023            }),
1024        );
1025
1026        container::draw_background(renderer, &style.container, layout.bounds());
1027
1028        // Draw inner content
1029        if scrollbars.active() {
1030            renderer.with_layer(visible_bounds, |renderer| {
1031                renderer.with_translation(
1032                    Vector::new(-translation.x, -translation.y),
1033                    |renderer| {
1034                        self.content.as_widget().draw(
1035                            &tree.children[0],
1036                            renderer,
1037                            theme,
1038                            defaults,
1039                            content_layout,
1040                            cursor,
1041                            &Rectangle {
1042                                y: visible_bounds.y + translation.y,
1043                                x: visible_bounds.x + translation.x,
1044                                ..visible_bounds
1045                            },
1046                        );
1047                    },
1048                );
1049            });
1050
1051            let draw_scrollbar =
1052                |renderer: &mut Renderer,
1053                 style: Rail,
1054                 scrollbar: &internals::Scrollbar| {
1055                    if scrollbar.bounds.width > 0.0
1056                        && scrollbar.bounds.height > 0.0
1057                        && (style.background.is_some()
1058                            || (style.border.color != Color::TRANSPARENT
1059                                && style.border.width > 0.0))
1060                    {
1061                        renderer.fill_quad(
1062                            renderer::Quad {
1063                                bounds: scrollbar.bounds,
1064                                border: style.border,
1065                                ..renderer::Quad::default()
1066                            },
1067                            style.background.unwrap_or(Background::Color(
1068                                Color::TRANSPARENT,
1069                            )),
1070                        );
1071                    }
1072
1073                    if let Some(scroller) = scrollbar.scroller
1074                        && scroller.bounds.width > 0.0
1075                        && scroller.bounds.height > 0.0
1076                        && (style.scroller.color != Color::TRANSPARENT
1077                            || (style.scroller.border.color
1078                                != Color::TRANSPARENT
1079                                && style.scroller.border.width > 0.0))
1080                    {
1081                        renderer.fill_quad(
1082                            renderer::Quad {
1083                                bounds: scroller.bounds,
1084                                border: style.scroller.border,
1085                                ..renderer::Quad::default()
1086                            },
1087                            style.scroller.color,
1088                        );
1089                    }
1090                };
1091
1092            renderer.with_layer(
1093                Rectangle {
1094                    width: (visible_bounds.width + 2.0).min(viewport.width),
1095                    height: (visible_bounds.height + 2.0).min(viewport.height),
1096                    ..visible_bounds
1097                },
1098                |renderer| {
1099                    if let Some(scrollbar) = scrollbars.y {
1100                        draw_scrollbar(
1101                            renderer,
1102                            style.vertical_rail,
1103                            &scrollbar,
1104                        );
1105                    }
1106
1107                    if let Some(scrollbar) = scrollbars.x {
1108                        draw_scrollbar(
1109                            renderer,
1110                            style.horizontal_rail,
1111                            &scrollbar,
1112                        );
1113                    }
1114
1115                    if let (Some(x), Some(y)) = (scrollbars.x, scrollbars.y) {
1116                        let background =
1117                            style.gap.or(style.container.background);
1118
1119                        if let Some(background) = background {
1120                            renderer.fill_quad(
1121                                renderer::Quad {
1122                                    bounds: Rectangle {
1123                                        x: y.bounds.x,
1124                                        y: x.bounds.y,
1125                                        width: y.bounds.width,
1126                                        height: x.bounds.height,
1127                                    },
1128                                    ..renderer::Quad::default()
1129                                },
1130                                background,
1131                            );
1132                        }
1133                    }
1134                },
1135            );
1136        } else {
1137            self.content.as_widget().draw(
1138                &tree.children[0],
1139                renderer,
1140                theme,
1141                defaults,
1142                content_layout,
1143                cursor,
1144                &Rectangle {
1145                    x: visible_bounds.x + translation.x,
1146                    y: visible_bounds.y + translation.y,
1147                    ..visible_bounds
1148                },
1149            );
1150        }
1151    }
1152
1153    fn mouse_interaction(
1154        &self,
1155        tree: &Tree,
1156        layout: Layout<'_>,
1157        cursor: mouse::Cursor,
1158        _viewport: &Rectangle,
1159        renderer: &Renderer,
1160    ) -> mouse::Interaction {
1161        let state = tree.state.downcast_ref::<State>();
1162        let bounds = layout.bounds();
1163        let cursor_over_scrollable = cursor.position_over(bounds);
1164
1165        let content_layout = layout.children().next().unwrap();
1166        let content_bounds = content_layout.bounds();
1167
1168        let scrollbars =
1169            Scrollbars::new(state, self.direction, bounds, content_bounds);
1170
1171        let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
1172            scrollbars.is_mouse_over(cursor);
1173
1174        if state.scrollers_grabbed() {
1175            return mouse::Interaction::None;
1176        }
1177
1178        let translation =
1179            state.translation(self.direction, bounds, content_bounds);
1180
1181        let cursor = match cursor_over_scrollable {
1182            Some(cursor_position)
1183                if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
1184            {
1185                mouse::Cursor::Available(cursor_position + translation)
1186            }
1187            _ => cursor.levitate() + translation,
1188        };
1189
1190        self.content.as_widget().mouse_interaction(
1191            &tree.children[0],
1192            content_layout,
1193            cursor,
1194            &Rectangle {
1195                y: bounds.y + translation.y,
1196                x: bounds.x + translation.x,
1197                ..bounds
1198            },
1199            renderer,
1200        )
1201    }
1202
1203    fn overlay<'b>(
1204        &'b mut self,
1205        tree: &'b mut Tree,
1206        layout: Layout<'b>,
1207        renderer: &Renderer,
1208        viewport: &Rectangle,
1209        translation: Vector,
1210    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
1211        let bounds = layout.bounds();
1212        let content_layout = layout.children().next().unwrap();
1213        let content_bounds = content_layout.bounds();
1214        let visible_bounds = bounds.intersection(viewport).unwrap_or(*viewport);
1215
1216        let offset = tree.state.downcast_ref::<State>().translation(
1217            self.direction,
1218            bounds,
1219            content_bounds,
1220        );
1221
1222        self.content.as_widget_mut().overlay(
1223            &mut tree.children[0],
1224            layout.children().next().unwrap(),
1225            renderer,
1226            &visible_bounds,
1227            translation - offset,
1228        )
1229    }
1230}
1231
1232impl<'a, Message, Theme, Renderer>
1233    From<Scrollable<'a, Message, Theme, Renderer>>
1234    for Element<'a, Message, Theme, Renderer>
1235where
1236    Message: 'a,
1237    Theme: 'a + Catalog,
1238    Renderer: 'a + core::Renderer,
1239{
1240    fn from(
1241        text_input: Scrollable<'a, Message, Theme, Renderer>,
1242    ) -> Element<'a, Message, Theme, Renderer> {
1243        Element::new(text_input)
1244    }
1245}
1246
1247fn notify_scroll<Message>(
1248    state: &mut State,
1249    on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
1250    bounds: Rectangle,
1251    content_bounds: Rectangle,
1252    shell: &mut Shell<'_, Message>,
1253) -> bool {
1254    if notify_viewport(state, on_scroll, bounds, content_bounds, shell) {
1255        state.last_scrolled = Some(Instant::now());
1256
1257        true
1258    } else {
1259        false
1260    }
1261}
1262
1263fn notify_viewport<Message>(
1264    state: &mut State,
1265    on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
1266    bounds: Rectangle,
1267    content_bounds: Rectangle,
1268    shell: &mut Shell<'_, Message>,
1269) -> bool {
1270    if content_bounds.width <= bounds.width
1271        && content_bounds.height <= bounds.height
1272    {
1273        return false;
1274    }
1275
1276    let viewport = Viewport {
1277        offset_x: state.offset_x,
1278        offset_y: state.offset_y,
1279        bounds,
1280        content_bounds,
1281    };
1282
1283    // Don't publish redundant viewports to shell
1284    if let Some(last_notified) = state.last_notified {
1285        let last_relative_offset = last_notified.relative_offset();
1286        let current_relative_offset = viewport.relative_offset();
1287
1288        let last_absolute_offset = last_notified.absolute_offset();
1289        let current_absolute_offset = viewport.absolute_offset();
1290
1291        let unchanged = |a: f32, b: f32| {
1292            (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan())
1293        };
1294
1295        if last_notified.bounds == bounds
1296            && last_notified.content_bounds == content_bounds
1297            && unchanged(last_relative_offset.x, current_relative_offset.x)
1298            && unchanged(last_relative_offset.y, current_relative_offset.y)
1299            && unchanged(last_absolute_offset.x, current_absolute_offset.x)
1300            && unchanged(last_absolute_offset.y, current_absolute_offset.y)
1301        {
1302            return false;
1303        }
1304    }
1305
1306    state.last_notified = Some(viewport);
1307
1308    if let Some(on_scroll) = on_scroll {
1309        shell.publish(on_scroll(viewport));
1310    }
1311
1312    true
1313}
1314
1315#[derive(Debug, Clone, Copy)]
1316struct State {
1317    scroll_area_touched_at: Option<Point>,
1318    offset_y: Offset,
1319    y_scroller_grabbed_at: Option<f32>,
1320    offset_x: Offset,
1321    x_scroller_grabbed_at: Option<f32>,
1322    keyboard_modifiers: keyboard::Modifiers,
1323    last_notified: Option<Viewport>,
1324    last_scrolled: Option<Instant>,
1325    is_scrollbar_visible: bool,
1326}
1327
1328impl Default for State {
1329    fn default() -> Self {
1330        Self {
1331            scroll_area_touched_at: None,
1332            offset_y: Offset::Absolute(0.0),
1333            y_scroller_grabbed_at: None,
1334            offset_x: Offset::Absolute(0.0),
1335            x_scroller_grabbed_at: None,
1336            keyboard_modifiers: keyboard::Modifiers::default(),
1337            last_notified: None,
1338            last_scrolled: None,
1339            is_scrollbar_visible: true,
1340        }
1341    }
1342}
1343
1344impl operation::Scrollable for State {
1345    fn snap_to(&mut self, offset: RelativeOffset) {
1346        State::snap_to(self, offset);
1347    }
1348
1349    fn scroll_to(&mut self, offset: AbsoluteOffset) {
1350        State::scroll_to(self, offset);
1351    }
1352
1353    fn scroll_by(
1354        &mut self,
1355        offset: AbsoluteOffset,
1356        bounds: Rectangle,
1357        content_bounds: Rectangle,
1358    ) {
1359        State::scroll_by(self, offset, bounds, content_bounds);
1360    }
1361}
1362
1363#[derive(Debug, Clone, Copy, PartialEq)]
1364enum Offset {
1365    Absolute(f32),
1366    Relative(f32),
1367}
1368
1369impl Offset {
1370    fn absolute(self, viewport: f32, content: f32) -> f32 {
1371        match self {
1372            Offset::Absolute(absolute) => {
1373                absolute.min((content - viewport).max(0.0))
1374            }
1375            Offset::Relative(percentage) => {
1376                ((content - viewport) * percentage).max(0.0)
1377            }
1378        }
1379    }
1380
1381    fn translation(
1382        self,
1383        viewport: f32,
1384        content: f32,
1385        alignment: Anchor,
1386    ) -> f32 {
1387        let offset = self.absolute(viewport, content);
1388
1389        match alignment {
1390            Anchor::Start => offset,
1391            Anchor::End => ((content - viewport).max(0.0) - offset).max(0.0),
1392        }
1393    }
1394}
1395
1396/// The current [`Viewport`] of the [`Scrollable`].
1397#[derive(Debug, Clone, Copy)]
1398pub struct Viewport {
1399    offset_x: Offset,
1400    offset_y: Offset,
1401    bounds: Rectangle,
1402    content_bounds: Rectangle,
1403}
1404
1405impl Viewport {
1406    /// Returns the [`AbsoluteOffset`] of the current [`Viewport`].
1407    pub fn absolute_offset(&self) -> AbsoluteOffset {
1408        let x = self
1409            .offset_x
1410            .absolute(self.bounds.width, self.content_bounds.width);
1411        let y = self
1412            .offset_y
1413            .absolute(self.bounds.height, self.content_bounds.height);
1414
1415        AbsoluteOffset { x, y }
1416    }
1417
1418    /// Returns the [`AbsoluteOffset`] of the current [`Viewport`], but with its
1419    /// alignment reversed.
1420    ///
1421    /// This method can be useful to switch the alignment of a [`Scrollable`]
1422    /// while maintaining its scrolling position.
1423    pub fn absolute_offset_reversed(&self) -> AbsoluteOffset {
1424        let AbsoluteOffset { x, y } = self.absolute_offset();
1425
1426        AbsoluteOffset {
1427            x: (self.content_bounds.width - self.bounds.width).max(0.0) - x,
1428            y: (self.content_bounds.height - self.bounds.height).max(0.0) - y,
1429        }
1430    }
1431
1432    /// Returns the [`RelativeOffset`] of the current [`Viewport`].
1433    pub fn relative_offset(&self) -> RelativeOffset {
1434        let AbsoluteOffset { x, y } = self.absolute_offset();
1435
1436        let x = x / (self.content_bounds.width - self.bounds.width);
1437        let y = y / (self.content_bounds.height - self.bounds.height);
1438
1439        RelativeOffset { x, y }
1440    }
1441
1442    /// Returns the bounds of the current [`Viewport`].
1443    pub fn bounds(&self) -> Rectangle {
1444        self.bounds
1445    }
1446
1447    /// Returns the content bounds of the current [`Viewport`].
1448    pub fn content_bounds(&self) -> Rectangle {
1449        self.content_bounds
1450    }
1451}
1452
1453impl State {
1454    /// Creates a new [`State`] with the scrollbar(s) at the beginning.
1455    pub fn new() -> Self {
1456        State::default()
1457    }
1458
1459    /// Apply a scrolling offset to the current [`State`], given the bounds of
1460    /// the [`Scrollable`] and its contents.
1461    pub fn scroll(
1462        &mut self,
1463        delta: Vector<f32>,
1464        bounds: Rectangle,
1465        content_bounds: Rectangle,
1466    ) {
1467        if bounds.height < content_bounds.height {
1468            self.offset_y = Offset::Absolute(
1469                (self.offset_y.absolute(bounds.height, content_bounds.height)
1470                    + delta.y)
1471                    .clamp(0.0, content_bounds.height - bounds.height),
1472            );
1473        }
1474
1475        if bounds.width < content_bounds.width {
1476            self.offset_x = Offset::Absolute(
1477                (self.offset_x.absolute(bounds.width, content_bounds.width)
1478                    + delta.x)
1479                    .clamp(0.0, content_bounds.width - bounds.width),
1480            );
1481        }
1482    }
1483
1484    /// Scrolls the [`Scrollable`] to a relative amount along the y axis.
1485    ///
1486    /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at
1487    /// the end.
1488    pub fn scroll_y_to(
1489        &mut self,
1490        percentage: f32,
1491        bounds: Rectangle,
1492        content_bounds: Rectangle,
1493    ) {
1494        self.offset_y = Offset::Relative(percentage.clamp(0.0, 1.0));
1495        self.unsnap(bounds, content_bounds);
1496    }
1497
1498    /// Scrolls the [`Scrollable`] to a relative amount along the x axis.
1499    ///
1500    /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at
1501    /// the end.
1502    pub fn scroll_x_to(
1503        &mut self,
1504        percentage: f32,
1505        bounds: Rectangle,
1506        content_bounds: Rectangle,
1507    ) {
1508        self.offset_x = Offset::Relative(percentage.clamp(0.0, 1.0));
1509        self.unsnap(bounds, content_bounds);
1510    }
1511
1512    /// Snaps the scroll position to a [`RelativeOffset`].
1513    pub fn snap_to(&mut self, offset: RelativeOffset) {
1514        self.offset_x = Offset::Relative(offset.x.clamp(0.0, 1.0));
1515        self.offset_y = Offset::Relative(offset.y.clamp(0.0, 1.0));
1516    }
1517
1518    /// Scroll to the provided [`AbsoluteOffset`].
1519    pub fn scroll_to(&mut self, offset: AbsoluteOffset) {
1520        self.offset_x = Offset::Absolute(offset.x.max(0.0));
1521        self.offset_y = Offset::Absolute(offset.y.max(0.0));
1522    }
1523
1524    /// Scroll by the provided [`AbsoluteOffset`].
1525    pub fn scroll_by(
1526        &mut self,
1527        offset: AbsoluteOffset,
1528        bounds: Rectangle,
1529        content_bounds: Rectangle,
1530    ) {
1531        self.scroll(Vector::new(offset.x, offset.y), bounds, content_bounds);
1532    }
1533
1534    /// Unsnaps the current scroll position, if snapped, given the bounds of the
1535    /// [`Scrollable`] and its contents.
1536    pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) {
1537        self.offset_x = Offset::Absolute(
1538            self.offset_x.absolute(bounds.width, content_bounds.width),
1539        );
1540        self.offset_y = Offset::Absolute(
1541            self.offset_y.absolute(bounds.height, content_bounds.height),
1542        );
1543    }
1544
1545    /// Returns the scrolling translation of the [`State`], given a [`Direction`],
1546    /// the bounds of the [`Scrollable`] and its contents.
1547    fn translation(
1548        &self,
1549        direction: Direction,
1550        bounds: Rectangle,
1551        content_bounds: Rectangle,
1552    ) -> Vector {
1553        Vector::new(
1554            if let Some(horizontal) = direction.horizontal() {
1555                self.offset_x
1556                    .translation(
1557                        bounds.width,
1558                        content_bounds.width,
1559                        horizontal.alignment,
1560                    )
1561                    .round()
1562            } else {
1563                0.0
1564            },
1565            if let Some(vertical) = direction.vertical() {
1566                self.offset_y
1567                    .translation(
1568                        bounds.height,
1569                        content_bounds.height,
1570                        vertical.alignment,
1571                    )
1572                    .round()
1573            } else {
1574                0.0
1575            },
1576        )
1577    }
1578
1579    /// Returns whether any scroller is currently grabbed or not.
1580    pub fn scrollers_grabbed(&self) -> bool {
1581        self.x_scroller_grabbed_at.is_some()
1582            || self.y_scroller_grabbed_at.is_some()
1583    }
1584}
1585
1586#[derive(Debug)]
1587/// State of both [`Scrollbar`]s.
1588struct Scrollbars {
1589    y: Option<internals::Scrollbar>,
1590    x: Option<internals::Scrollbar>,
1591}
1592
1593impl Scrollbars {
1594    /// Create y and/or x scrollbar(s) if content is overflowing the [`Scrollable`] bounds.
1595    fn new(
1596        state: &State,
1597        direction: Direction,
1598        bounds: Rectangle,
1599        content_bounds: Rectangle,
1600    ) -> Self {
1601        let translation = state.translation(direction, bounds, content_bounds);
1602
1603        let show_scrollbar_x = direction
1604            .horizontal()
1605            .filter(|_scrollbar| content_bounds.width > bounds.width);
1606
1607        let show_scrollbar_y = direction
1608            .vertical()
1609            .filter(|_scrollbar| content_bounds.height > bounds.height);
1610
1611        let y_scrollbar = if let Some(vertical) = show_scrollbar_y {
1612            let Scrollbar {
1613                width,
1614                margin,
1615                scroller_width,
1616                ..
1617            } = *vertical;
1618
1619            // Adjust the height of the vertical scrollbar if the horizontal scrollbar
1620            // is present
1621            let x_scrollbar_height = show_scrollbar_x
1622                .map_or(0.0, |h| h.width.max(h.scroller_width) + h.margin);
1623
1624            let total_scrollbar_width =
1625                width.max(scroller_width) + 2.0 * margin;
1626
1627            // Total bounds of the scrollbar + margin + scroller width
1628            let total_scrollbar_bounds = Rectangle {
1629                x: bounds.x + bounds.width - total_scrollbar_width,
1630                y: bounds.y,
1631                width: total_scrollbar_width,
1632                height: (bounds.height - x_scrollbar_height).max(0.0),
1633            };
1634
1635            // Bounds of just the scrollbar
1636            let scrollbar_bounds = Rectangle {
1637                x: bounds.x + bounds.width
1638                    - total_scrollbar_width / 2.0
1639                    - width / 2.0,
1640                y: bounds.y,
1641                width,
1642                height: (bounds.height - x_scrollbar_height).max(0.0),
1643            };
1644
1645            let ratio = bounds.height / content_bounds.height;
1646
1647            let scroller = if ratio >= 1.0 {
1648                None
1649            } else {
1650                // min height for easier grabbing with super tall content
1651                let scroller_height =
1652                    (scrollbar_bounds.height * ratio).max(2.0);
1653                let scroller_offset =
1654                    translation.y * ratio * scrollbar_bounds.height
1655                        / bounds.height;
1656
1657                let scroller_bounds = Rectangle {
1658                    x: bounds.x + bounds.width
1659                        - total_scrollbar_width / 2.0
1660                        - scroller_width / 2.0,
1661                    y: (scrollbar_bounds.y + scroller_offset).max(0.0),
1662                    width: scroller_width,
1663                    height: scroller_height,
1664                };
1665
1666                Some(internals::Scroller {
1667                    bounds: scroller_bounds,
1668                })
1669            };
1670
1671            Some(internals::Scrollbar {
1672                total_bounds: total_scrollbar_bounds,
1673                bounds: scrollbar_bounds,
1674                scroller,
1675                alignment: vertical.alignment,
1676                disabled: content_bounds.height <= bounds.height,
1677            })
1678        } else {
1679            None
1680        };
1681
1682        let x_scrollbar = if let Some(horizontal) = show_scrollbar_x {
1683            let Scrollbar {
1684                width,
1685                margin,
1686                scroller_width,
1687                ..
1688            } = *horizontal;
1689
1690            // Need to adjust the width of the horizontal scrollbar if the vertical scrollbar
1691            // is present
1692            let scrollbar_y_width = y_scrollbar
1693                .map_or(0.0, |scrollbar| scrollbar.total_bounds.width);
1694
1695            let total_scrollbar_height =
1696                width.max(scroller_width) + 2.0 * margin;
1697
1698            // Total bounds of the scrollbar + margin + scroller width
1699            let total_scrollbar_bounds = Rectangle {
1700                x: bounds.x,
1701                y: bounds.y + bounds.height - total_scrollbar_height,
1702                width: (bounds.width - scrollbar_y_width).max(0.0),
1703                height: total_scrollbar_height,
1704            };
1705
1706            // Bounds of just the scrollbar
1707            let scrollbar_bounds = Rectangle {
1708                x: bounds.x,
1709                y: bounds.y + bounds.height
1710                    - total_scrollbar_height / 2.0
1711                    - width / 2.0,
1712                width: (bounds.width - scrollbar_y_width).max(0.0),
1713                height: width,
1714            };
1715
1716            let ratio = bounds.width / content_bounds.width;
1717
1718            let scroller = if ratio >= 1.0 {
1719                None
1720            } else {
1721                // min width for easier grabbing with extra wide content
1722                let scroller_length = (scrollbar_bounds.width * ratio).max(2.0);
1723                let scroller_offset =
1724                    translation.x * ratio * scrollbar_bounds.width
1725                        / bounds.width;
1726
1727                let scroller_bounds = Rectangle {
1728                    x: (scrollbar_bounds.x + scroller_offset).max(0.0),
1729                    y: bounds.y + bounds.height
1730                        - total_scrollbar_height / 2.0
1731                        - scroller_width / 2.0,
1732                    width: scroller_length,
1733                    height: scroller_width,
1734                };
1735
1736                Some(internals::Scroller {
1737                    bounds: scroller_bounds,
1738                })
1739            };
1740
1741            Some(internals::Scrollbar {
1742                total_bounds: total_scrollbar_bounds,
1743                bounds: scrollbar_bounds,
1744                scroller,
1745                alignment: horizontal.alignment,
1746                disabled: content_bounds.width <= bounds.width,
1747            })
1748        } else {
1749            None
1750        };
1751
1752        Self {
1753            y: y_scrollbar,
1754            x: x_scrollbar,
1755        }
1756    }
1757
1758    fn is_mouse_over(&self, cursor: mouse::Cursor) -> (bool, bool) {
1759        if let Some(cursor_position) = cursor.position() {
1760            (
1761                self.y
1762                    .as_ref()
1763                    .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
1764                    .unwrap_or(false),
1765                self.x
1766                    .as_ref()
1767                    .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
1768                    .unwrap_or(false),
1769            )
1770        } else {
1771            (false, false)
1772        }
1773    }
1774
1775    fn is_y_disabled(&self) -> bool {
1776        self.y.map(|y| y.disabled).unwrap_or(false)
1777    }
1778
1779    fn is_x_disabled(&self) -> bool {
1780        self.x.map(|x| x.disabled).unwrap_or(false)
1781    }
1782
1783    fn grab_y_scroller(&self, cursor_position: Point) -> Option<f32> {
1784        let scrollbar = self.y?;
1785        let scroller = scrollbar.scroller?;
1786
1787        if scrollbar.total_bounds.contains(cursor_position) {
1788            Some(if scroller.bounds.contains(cursor_position) {
1789                (cursor_position.y - scroller.bounds.y) / scroller.bounds.height
1790            } else {
1791                0.5
1792            })
1793        } else {
1794            None
1795        }
1796    }
1797
1798    fn grab_x_scroller(&self, cursor_position: Point) -> Option<f32> {
1799        let scrollbar = self.x?;
1800        let scroller = scrollbar.scroller?;
1801
1802        if scrollbar.total_bounds.contains(cursor_position) {
1803            Some(if scroller.bounds.contains(cursor_position) {
1804                (cursor_position.x - scroller.bounds.x) / scroller.bounds.width
1805            } else {
1806                0.5
1807            })
1808        } else {
1809            None
1810        }
1811    }
1812
1813    fn active(&self) -> bool {
1814        self.y.is_some() || self.x.is_some()
1815    }
1816}
1817
1818pub(super) mod internals {
1819    use crate::core::{Point, Rectangle};
1820
1821    use super::Anchor;
1822
1823    #[derive(Debug, Copy, Clone)]
1824    pub struct Scrollbar {
1825        pub total_bounds: Rectangle,
1826        pub bounds: Rectangle,
1827        pub scroller: Option<Scroller>,
1828        pub alignment: Anchor,
1829        pub disabled: bool,
1830    }
1831
1832    impl Scrollbar {
1833        /// Returns whether the mouse is over the scrollbar or not.
1834        pub fn is_mouse_over(&self, cursor_position: Point) -> bool {
1835            self.total_bounds.contains(cursor_position)
1836        }
1837
1838        /// Returns the y-axis scrolled percentage from the cursor position.
1839        pub fn scroll_percentage_y(
1840            &self,
1841            grabbed_at: f32,
1842            cursor_position: Point,
1843        ) -> f32 {
1844            if let Some(scroller) = self.scroller {
1845                let percentage = (cursor_position.y
1846                    - self.bounds.y
1847                    - scroller.bounds.height * grabbed_at)
1848                    / (self.bounds.height - scroller.bounds.height);
1849
1850                match self.alignment {
1851                    Anchor::Start => percentage,
1852                    Anchor::End => 1.0 - percentage,
1853                }
1854            } else {
1855                0.0
1856            }
1857        }
1858
1859        /// Returns the x-axis scrolled percentage from the cursor position.
1860        pub fn scroll_percentage_x(
1861            &self,
1862            grabbed_at: f32,
1863            cursor_position: Point,
1864        ) -> f32 {
1865            if let Some(scroller) = self.scroller {
1866                let percentage = (cursor_position.x
1867                    - self.bounds.x
1868                    - scroller.bounds.width * grabbed_at)
1869                    / (self.bounds.width - scroller.bounds.width);
1870
1871                match self.alignment {
1872                    Anchor::Start => percentage,
1873                    Anchor::End => 1.0 - percentage,
1874                }
1875            } else {
1876                0.0
1877            }
1878        }
1879    }
1880
1881    /// The handle of a [`Scrollbar`].
1882    #[derive(Debug, Clone, Copy)]
1883    pub struct Scroller {
1884        /// The bounds of the [`Scroller`].
1885        pub bounds: Rectangle,
1886    }
1887}
1888
1889/// The possible status of a [`Scrollable`].
1890#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1891pub enum Status {
1892    /// The [`Scrollable`] can be interacted with.
1893    Active {
1894        /// Whether or not the horizontal scrollbar is disabled meaning the content isn't overflowing.
1895        is_horizontal_scrollbar_disabled: bool,
1896        /// Whether or not the vertical scrollbar is disabled meaning the content isn't overflowing.
1897        is_vertical_scrollbar_disabled: bool,
1898    },
1899    /// The [`Scrollable`] is being hovered.
1900    Hovered {
1901        /// Indicates if the horizontal scrollbar is being hovered.
1902        is_horizontal_scrollbar_hovered: bool,
1903        /// Indicates if the vertical scrollbar is being hovered.
1904        is_vertical_scrollbar_hovered: bool,
1905        /// Whether or not the horizontal scrollbar is disabled meaning the content isn't overflowing.
1906        is_horizontal_scrollbar_disabled: bool,
1907        /// Whether or not the vertical scrollbar is disabled meaning the content isn't overflowing.
1908        is_vertical_scrollbar_disabled: bool,
1909    },
1910    /// The [`Scrollable`] is being dragged.
1911    Dragged {
1912        /// Indicates if the horizontal scrollbar is being dragged.
1913        is_horizontal_scrollbar_dragged: bool,
1914        /// Indicates if the vertical scrollbar is being dragged.
1915        is_vertical_scrollbar_dragged: bool,
1916        /// Whether or not the horizontal scrollbar is disabled meaning the content isn't overflowing.
1917        is_horizontal_scrollbar_disabled: bool,
1918        /// Whether or not the vertical scrollbar is disabled meaning the content isn't overflowing.
1919        is_vertical_scrollbar_disabled: bool,
1920    },
1921}
1922
1923/// The appearance of a scrollable.
1924#[derive(Debug, Clone, Copy, PartialEq)]
1925pub struct Style {
1926    /// The [`container::Style`] of a scrollable.
1927    pub container: container::Style,
1928    /// The vertical [`Rail`] appearance.
1929    pub vertical_rail: Rail,
1930    /// The horizontal [`Rail`] appearance.
1931    pub horizontal_rail: Rail,
1932    /// The [`Background`] of the gap between a horizontal and vertical scrollbar.
1933    pub gap: Option<Background>,
1934}
1935
1936/// The appearance of the scrollbar of a scrollable.
1937#[derive(Debug, Clone, Copy, PartialEq)]
1938pub struct Rail {
1939    /// The [`Background`] of a scrollbar.
1940    pub background: Option<Background>,
1941    /// The [`Border`] of a scrollbar.
1942    pub border: Border,
1943    /// The appearance of the [`Scroller`] of a scrollbar.
1944    pub scroller: Scroller,
1945}
1946
1947/// The appearance of the scroller of a scrollable.
1948#[derive(Debug, Clone, Copy, PartialEq)]
1949pub struct Scroller {
1950    /// The [`Color`] of the scroller.
1951    pub color: Color,
1952    /// The [`Border`] of the scroller.
1953    pub border: Border,
1954}
1955
1956/// The theme catalog of a [`Scrollable`].
1957pub trait Catalog {
1958    /// The item class of the [`Catalog`].
1959    type Class<'a>;
1960
1961    /// The default class produced by the [`Catalog`].
1962    fn default<'a>() -> Self::Class<'a>;
1963
1964    /// The [`Style`] of a class with the given status.
1965    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
1966}
1967
1968/// A styling function for a [`Scrollable`].
1969pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
1970
1971impl Catalog for Theme {
1972    type Class<'a> = StyleFn<'a, Self>;
1973
1974    fn default<'a>() -> Self::Class<'a> {
1975        Box::new(default)
1976    }
1977
1978    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
1979        class(self, status)
1980    }
1981}
1982
1983/// The default style of a [`Scrollable`].
1984pub fn default(theme: &Theme, status: Status) -> Style {
1985    let palette = theme.extended_palette();
1986
1987    let scrollbar = Rail {
1988        background: Some(palette.background.weak.color.into()),
1989        border: border::rounded(2),
1990        scroller: Scroller {
1991            color: palette.background.strongest.color,
1992            border: border::rounded(2),
1993        },
1994    };
1995
1996    match status {
1997        Status::Active { .. } => Style {
1998            container: container::Style::default(),
1999            vertical_rail: scrollbar,
2000            horizontal_rail: scrollbar,
2001            gap: None,
2002        },
2003        Status::Hovered {
2004            is_horizontal_scrollbar_hovered,
2005            is_vertical_scrollbar_hovered,
2006            ..
2007        } => {
2008            let hovered_scrollbar = Rail {
2009                scroller: Scroller {
2010                    color: palette.primary.strong.color,
2011                    ..scrollbar.scroller
2012                },
2013                ..scrollbar
2014            };
2015
2016            Style {
2017                container: container::Style::default(),
2018                vertical_rail: if is_vertical_scrollbar_hovered {
2019                    hovered_scrollbar
2020                } else {
2021                    scrollbar
2022                },
2023                horizontal_rail: if is_horizontal_scrollbar_hovered {
2024                    hovered_scrollbar
2025                } else {
2026                    scrollbar
2027                },
2028                gap: None,
2029            }
2030        }
2031        Status::Dragged {
2032            is_horizontal_scrollbar_dragged,
2033            is_vertical_scrollbar_dragged,
2034            ..
2035        } => {
2036            let dragged_scrollbar = Rail {
2037                scroller: Scroller {
2038                    color: palette.primary.base.color,
2039                    ..scrollbar.scroller
2040                },
2041                ..scrollbar
2042            };
2043
2044            Style {
2045                container: container::Style::default(),
2046                vertical_rail: if is_vertical_scrollbar_dragged {
2047                    dragged_scrollbar
2048                } else {
2049                    scrollbar
2050                },
2051                horizontal_rail: if is_horizontal_scrollbar_dragged {
2052                    dragged_scrollbar
2053                } else {
2054                    scrollbar
2055                },
2056                gap: None,
2057            }
2058        }
2059    }
2060}