iced_widget/
scrollable.rs

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