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