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