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