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