Skip to main content

iced_widget/
scrollable.rs

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