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