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