iced_native/widget/
scrollable.rs

1//! Navigate an endless amount of content with a scrollbar.
2use crate::event::{self, Event};
3use crate::keyboard;
4use crate::layout;
5use crate::mouse;
6use crate::overlay;
7use crate::renderer;
8use crate::touch;
9use crate::widget;
10use crate::widget::operation::{self, Operation};
11use crate::widget::tree::{self, Tree};
12use crate::{
13    Background, Clipboard, Color, Command, Element, Layout, Length, Pixels,
14    Point, Rectangle, Shell, Size, Vector, Widget,
15};
16
17pub use iced_style::scrollable::StyleSheet;
18pub use operation::scrollable::RelativeOffset;
19
20pub mod style {
21    //! The styles of a [`Scrollable`].
22    //!
23    //! [`Scrollable`]: crate::widget::Scrollable
24    pub use iced_style::scrollable::{Scrollbar, Scroller};
25}
26
27/// A widget that can vertically display an infinite amount of content with a
28/// scrollbar.
29#[allow(missing_debug_implementations)]
30pub struct Scrollable<'a, Message, Renderer>
31where
32    Renderer: crate::Renderer,
33    Renderer::Theme: StyleSheet,
34{
35    id: Option<Id>,
36    width: Length,
37    height: Length,
38    vertical: Properties,
39    horizontal: Option<Properties>,
40    content: Element<'a, Message, Renderer>,
41    on_scroll: Option<Box<dyn Fn(RelativeOffset) -> Message + 'a>>,
42    style: <Renderer::Theme as StyleSheet>::Style,
43}
44
45impl<'a, Message, Renderer> Scrollable<'a, Message, Renderer>
46where
47    Renderer: crate::Renderer,
48    Renderer::Theme: StyleSheet,
49{
50    /// Creates a new [`Scrollable`].
51    pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self {
52        Scrollable {
53            id: None,
54            width: Length::Shrink,
55            height: Length::Shrink,
56            vertical: Properties::default(),
57            horizontal: None,
58            content: content.into(),
59            on_scroll: None,
60            style: Default::default(),
61        }
62    }
63
64    /// Sets the [`Id`] of the [`Scrollable`].
65    pub fn id(mut self, id: Id) -> Self {
66        self.id = Some(id);
67        self
68    }
69
70    /// Sets the width of the [`Scrollable`].
71    pub fn width(mut self, width: impl Into<Length>) -> Self {
72        self.width = width.into();
73        self
74    }
75
76    /// Sets the height of the [`Scrollable`].
77    pub fn height(mut self, height: impl Into<Length>) -> Self {
78        self.height = height.into();
79        self
80    }
81
82    /// Configures the vertical scrollbar of the [`Scrollable`] .
83    pub fn vertical_scroll(mut self, properties: Properties) -> Self {
84        self.vertical = properties;
85        self
86    }
87
88    /// Configures the horizontal scrollbar of the [`Scrollable`] .
89    pub fn horizontal_scroll(mut self, properties: Properties) -> Self {
90        self.horizontal = Some(properties);
91        self
92    }
93
94    /// Sets a function to call when the [`Scrollable`] is scrolled.
95    ///
96    /// The function takes the new relative x & y offset of the [`Scrollable`]
97    /// (e.g. `0` means beginning, while `1` means end).
98    pub fn on_scroll(
99        mut self,
100        f: impl Fn(RelativeOffset) -> Message + 'a,
101    ) -> Self {
102        self.on_scroll = Some(Box::new(f));
103        self
104    }
105
106    /// Sets the style of the [`Scrollable`] .
107    pub fn style(
108        mut self,
109        style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
110    ) -> Self {
111        self.style = style.into();
112        self
113    }
114}
115
116/// Properties of a scrollbar within a [`Scrollable`].
117#[derive(Debug)]
118pub struct Properties {
119    width: f32,
120    margin: f32,
121    scroller_width: f32,
122}
123
124impl Default for Properties {
125    fn default() -> Self {
126        Self {
127            width: 10.0,
128            margin: 0.0,
129            scroller_width: 10.0,
130        }
131    }
132}
133
134impl Properties {
135    /// Creates new [`Properties`] for use in a [`Scrollable`].
136    pub fn new() -> Self {
137        Self::default()
138    }
139
140    /// Sets the scrollbar width of the [`Scrollable`] .
141    /// Silently enforces a minimum width of 1.
142    pub fn width(mut self, width: impl Into<Pixels>) -> Self {
143        self.width = width.into().0.max(1.0);
144        self
145    }
146
147    /// Sets the scrollbar margin of the [`Scrollable`] .
148    pub fn margin(mut self, margin: impl Into<Pixels>) -> Self {
149        self.margin = margin.into().0;
150        self
151    }
152
153    /// Sets the scroller width of the [`Scrollable`] .
154    /// Silently enforces a minimum width of 1.
155    pub fn scroller_width(mut self, scroller_width: impl Into<Pixels>) -> Self {
156        self.scroller_width = scroller_width.into().0.max(1.0);
157        self
158    }
159}
160
161impl<'a, Message, Renderer> Widget<Message, Renderer>
162    for Scrollable<'a, Message, Renderer>
163where
164    Renderer: crate::Renderer,
165    Renderer::Theme: StyleSheet,
166{
167    fn tag(&self) -> tree::Tag {
168        tree::Tag::of::<State>()
169    }
170
171    fn state(&self) -> tree::State {
172        tree::State::new(State::new())
173    }
174
175    fn children(&self) -> Vec<Tree> {
176        vec![Tree::new(&self.content)]
177    }
178
179    fn diff(&self, tree: &mut Tree) {
180        tree.diff_children(std::slice::from_ref(&self.content))
181    }
182
183    fn width(&self) -> Length {
184        self.width
185    }
186
187    fn height(&self) -> Length {
188        self.height
189    }
190
191    fn layout(
192        &self,
193        renderer: &Renderer,
194        limits: &layout::Limits,
195    ) -> layout::Node {
196        layout(
197            renderer,
198            limits,
199            self.width,
200            self.height,
201            self.horizontal.is_some(),
202            |renderer, limits| {
203                self.content.as_widget().layout(renderer, limits)
204            },
205        )
206    }
207
208    fn operate(
209        &self,
210        tree: &mut Tree,
211        layout: Layout<'_>,
212        renderer: &Renderer,
213        operation: &mut dyn Operation<Message>,
214    ) {
215        let state = tree.state.downcast_mut::<State>();
216
217        operation.scrollable(state, self.id.as_ref().map(|id| &id.0));
218
219        operation.container(
220            self.id.as_ref().map(|id| &id.0),
221            &mut |operation| {
222                self.content.as_widget().operate(
223                    &mut tree.children[0],
224                    layout.children().next().unwrap(),
225                    renderer,
226                    operation,
227                );
228            },
229        );
230    }
231
232    fn on_event(
233        &mut self,
234        tree: &mut Tree,
235        event: Event,
236        layout: Layout<'_>,
237        cursor_position: Point,
238        renderer: &Renderer,
239        clipboard: &mut dyn Clipboard,
240        shell: &mut Shell<'_, Message>,
241    ) -> event::Status {
242        update(
243            tree.state.downcast_mut::<State>(),
244            event,
245            layout,
246            cursor_position,
247            clipboard,
248            shell,
249            &self.vertical,
250            self.horizontal.as_ref(),
251            &self.on_scroll,
252            |event, layout, cursor_position, clipboard, shell| {
253                self.content.as_widget_mut().on_event(
254                    &mut tree.children[0],
255                    event,
256                    layout,
257                    cursor_position,
258                    renderer,
259                    clipboard,
260                    shell,
261                )
262            },
263        )
264    }
265
266    fn draw(
267        &self,
268        tree: &Tree,
269        renderer: &mut Renderer,
270        theme: &Renderer::Theme,
271        style: &renderer::Style,
272        layout: Layout<'_>,
273        cursor_position: Point,
274        _viewport: &Rectangle,
275    ) {
276        draw(
277            tree.state.downcast_ref::<State>(),
278            renderer,
279            theme,
280            layout,
281            cursor_position,
282            &self.vertical,
283            self.horizontal.as_ref(),
284            &self.style,
285            |renderer, layout, cursor_position, viewport| {
286                self.content.as_widget().draw(
287                    &tree.children[0],
288                    renderer,
289                    theme,
290                    style,
291                    layout,
292                    cursor_position,
293                    viewport,
294                )
295            },
296        )
297    }
298
299    fn mouse_interaction(
300        &self,
301        tree: &Tree,
302        layout: Layout<'_>,
303        cursor_position: Point,
304        _viewport: &Rectangle,
305        renderer: &Renderer,
306    ) -> mouse::Interaction {
307        mouse_interaction(
308            tree.state.downcast_ref::<State>(),
309            layout,
310            cursor_position,
311            &self.vertical,
312            self.horizontal.as_ref(),
313            |layout, cursor_position, viewport| {
314                self.content.as_widget().mouse_interaction(
315                    &tree.children[0],
316                    layout,
317                    cursor_position,
318                    viewport,
319                    renderer,
320                )
321            },
322        )
323    }
324
325    fn overlay<'b>(
326        &'b mut self,
327        tree: &'b mut Tree,
328        layout: Layout<'_>,
329        renderer: &Renderer,
330    ) -> Option<overlay::Element<'b, Message, Renderer>> {
331        self.content
332            .as_widget_mut()
333            .overlay(
334                &mut tree.children[0],
335                layout.children().next().unwrap(),
336                renderer,
337            )
338            .map(|overlay| {
339                let bounds = layout.bounds();
340                let content_layout = layout.children().next().unwrap();
341                let content_bounds = content_layout.bounds();
342                let offset = tree
343                    .state
344                    .downcast_ref::<State>()
345                    .offset(bounds, content_bounds);
346
347                overlay.translate(Vector::new(-offset.x, -offset.y))
348            })
349    }
350}
351
352impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>>
353    for Element<'a, Message, Renderer>
354where
355    Message: 'a,
356    Renderer: 'a + crate::Renderer,
357    Renderer::Theme: StyleSheet,
358{
359    fn from(
360        text_input: Scrollable<'a, Message, Renderer>,
361    ) -> Element<'a, Message, Renderer> {
362        Element::new(text_input)
363    }
364}
365
366/// The identifier of a [`Scrollable`].
367#[derive(Debug, Clone, PartialEq, Eq, Hash)]
368pub struct Id(widget::Id);
369
370impl Id {
371    /// Creates a custom [`Id`].
372    pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self {
373        Self(widget::Id::new(id))
374    }
375
376    /// Creates a unique [`Id`].
377    ///
378    /// This function produces a different [`Id`] every time it is called.
379    pub fn unique() -> Self {
380        Self(widget::Id::unique())
381    }
382}
383
384impl From<Id> for widget::Id {
385    fn from(id: Id) -> Self {
386        id.0
387    }
388}
389
390/// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`Id`]
391/// to the provided `percentage` along the x & y axis.
392pub fn snap_to<Message: 'static>(
393    id: Id,
394    offset: RelativeOffset,
395) -> Command<Message> {
396    Command::widget(operation::scrollable::snap_to(id.0, offset))
397}
398
399/// Computes the layout of a [`Scrollable`].
400pub fn layout<Renderer>(
401    renderer: &Renderer,
402    limits: &layout::Limits,
403    width: Length,
404    height: Length,
405    horizontal_enabled: bool,
406    layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
407) -> layout::Node {
408    let limits = limits.width(width).height(height);
409
410    let child_limits = layout::Limits::new(
411        Size::new(limits.min().width, 0.0),
412        Size::new(
413            if horizontal_enabled {
414                f32::INFINITY
415            } else {
416                limits.max().width
417            },
418            f32::MAX,
419        ),
420    );
421
422    let content = layout_content(renderer, &child_limits);
423    let size = limits.resolve(content.size());
424
425    layout::Node::with_children(size, vec![content])
426}
427
428/// Processes an [`Event`] and updates the [`State`] of a [`Scrollable`]
429/// accordingly.
430pub fn update<Message>(
431    state: &mut State,
432    event: Event,
433    layout: Layout<'_>,
434    cursor_position: Point,
435    clipboard: &mut dyn Clipboard,
436    shell: &mut Shell<'_, Message>,
437    vertical: &Properties,
438    horizontal: Option<&Properties>,
439    on_scroll: &Option<Box<dyn Fn(RelativeOffset) -> Message + '_>>,
440    update_content: impl FnOnce(
441        Event,
442        Layout<'_>,
443        Point,
444        &mut dyn Clipboard,
445        &mut Shell<'_, Message>,
446    ) -> event::Status,
447) -> event::Status {
448    let bounds = layout.bounds();
449    let mouse_over_scrollable = bounds.contains(cursor_position);
450
451    let content = layout.children().next().unwrap();
452    let content_bounds = content.bounds();
453
454    let scrollbars =
455        Scrollbars::new(state, vertical, horizontal, bounds, content_bounds);
456
457    let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
458        scrollbars.is_mouse_over(cursor_position);
459
460    let event_status = {
461        let cursor_position = if mouse_over_scrollable
462            && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar)
463        {
464            cursor_position + state.offset(bounds, content_bounds)
465        } else {
466            // TODO: Make `cursor_position` an `Option<Point>` so we can encode
467            // cursor availability.
468            // This will probably happen naturally once we add multi-window
469            // support.
470            Point::new(-1.0, -1.0)
471        };
472
473        update_content(
474            event.clone(),
475            content,
476            cursor_position,
477            clipboard,
478            shell,
479        )
480    };
481
482    if let event::Status::Captured = event_status {
483        return event::Status::Captured;
484    }
485
486    if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = event
487    {
488        state.keyboard_modifiers = modifiers;
489
490        return event::Status::Ignored;
491    }
492
493    if mouse_over_scrollable {
494        match event {
495            Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
496                let delta = match delta {
497                    mouse::ScrollDelta::Lines { x, y } => {
498                        // TODO: Configurable speed/friction (?)
499                        let movement = if state.keyboard_modifiers.shift() {
500                            Vector::new(y, x)
501                        } else {
502                            Vector::new(x, y)
503                        };
504
505                        movement * 60.0
506                    }
507                    mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y),
508                };
509
510                state.scroll(delta, bounds, content_bounds);
511
512                notify_on_scroll(
513                    state,
514                    on_scroll,
515                    bounds,
516                    content_bounds,
517                    shell,
518                );
519
520                return event::Status::Captured;
521            }
522            Event::Touch(event)
523                if state.scroll_area_touched_at.is_some()
524                    || !mouse_over_y_scrollbar && !mouse_over_x_scrollbar =>
525            {
526                match event {
527                    touch::Event::FingerPressed { .. } => {
528                        state.scroll_area_touched_at = Some(cursor_position);
529                    }
530                    touch::Event::FingerMoved { .. } => {
531                        if let Some(scroll_box_touched_at) =
532                            state.scroll_area_touched_at
533                        {
534                            let delta = Vector::new(
535                                cursor_position.x - scroll_box_touched_at.x,
536                                cursor_position.y - scroll_box_touched_at.y,
537                            );
538
539                            state.scroll(delta, bounds, content_bounds);
540
541                            state.scroll_area_touched_at =
542                                Some(cursor_position);
543
544                            notify_on_scroll(
545                                state,
546                                on_scroll,
547                                bounds,
548                                content_bounds,
549                                shell,
550                            );
551                        }
552                    }
553                    touch::Event::FingerLifted { .. }
554                    | touch::Event::FingerLost { .. } => {
555                        state.scroll_area_touched_at = None;
556                    }
557                }
558
559                return event::Status::Captured;
560            }
561            _ => {}
562        }
563    }
564
565    if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at {
566        match event {
567            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
568            | Event::Touch(touch::Event::FingerLifted { .. })
569            | Event::Touch(touch::Event::FingerLost { .. }) => {
570                state.y_scroller_grabbed_at = None;
571
572                return event::Status::Captured;
573            }
574            Event::Mouse(mouse::Event::CursorMoved { .. })
575            | Event::Touch(touch::Event::FingerMoved { .. }) => {
576                if let Some(scrollbar) = scrollbars.y {
577                    state.scroll_y_to(
578                        scrollbar.scroll_percentage_y(
579                            scroller_grabbed_at,
580                            cursor_position,
581                        ),
582                        bounds,
583                        content_bounds,
584                    );
585
586                    notify_on_scroll(
587                        state,
588                        on_scroll,
589                        bounds,
590                        content_bounds,
591                        shell,
592                    );
593
594                    return event::Status::Captured;
595                }
596            }
597            _ => {}
598        }
599    } else if mouse_over_y_scrollbar {
600        match event {
601            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
602            | Event::Touch(touch::Event::FingerPressed { .. }) => {
603                if let (Some(scroller_grabbed_at), Some(scrollbar)) =
604                    (scrollbars.grab_y_scroller(cursor_position), scrollbars.y)
605                {
606                    state.scroll_y_to(
607                        scrollbar.scroll_percentage_y(
608                            scroller_grabbed_at,
609                            cursor_position,
610                        ),
611                        bounds,
612                        content_bounds,
613                    );
614
615                    state.y_scroller_grabbed_at = Some(scroller_grabbed_at);
616
617                    notify_on_scroll(
618                        state,
619                        on_scroll,
620                        bounds,
621                        content_bounds,
622                        shell,
623                    );
624                }
625
626                return event::Status::Captured;
627            }
628            _ => {}
629        }
630    }
631
632    if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at {
633        match event {
634            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
635            | Event::Touch(touch::Event::FingerLifted { .. })
636            | Event::Touch(touch::Event::FingerLost { .. }) => {
637                state.x_scroller_grabbed_at = None;
638
639                return event::Status::Captured;
640            }
641            Event::Mouse(mouse::Event::CursorMoved { .. })
642            | Event::Touch(touch::Event::FingerMoved { .. }) => {
643                if let Some(scrollbar) = scrollbars.x {
644                    state.scroll_x_to(
645                        scrollbar.scroll_percentage_x(
646                            scroller_grabbed_at,
647                            cursor_position,
648                        ),
649                        bounds,
650                        content_bounds,
651                    );
652
653                    notify_on_scroll(
654                        state,
655                        on_scroll,
656                        bounds,
657                        content_bounds,
658                        shell,
659                    );
660                }
661
662                return event::Status::Captured;
663            }
664            _ => {}
665        }
666    } else if mouse_over_x_scrollbar {
667        match event {
668            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
669            | Event::Touch(touch::Event::FingerPressed { .. }) => {
670                if let (Some(scroller_grabbed_at), Some(scrollbar)) =
671                    (scrollbars.grab_x_scroller(cursor_position), scrollbars.x)
672                {
673                    state.scroll_x_to(
674                        scrollbar.scroll_percentage_x(
675                            scroller_grabbed_at,
676                            cursor_position,
677                        ),
678                        bounds,
679                        content_bounds,
680                    );
681
682                    state.x_scroller_grabbed_at = Some(scroller_grabbed_at);
683
684                    notify_on_scroll(
685                        state,
686                        on_scroll,
687                        bounds,
688                        content_bounds,
689                        shell,
690                    );
691
692                    return event::Status::Captured;
693                }
694            }
695            _ => {}
696        }
697    }
698
699    event::Status::Ignored
700}
701
702/// Computes the current [`mouse::Interaction`] of a [`Scrollable`].
703pub fn mouse_interaction(
704    state: &State,
705    layout: Layout<'_>,
706    cursor_position: Point,
707    vertical: &Properties,
708    horizontal: Option<&Properties>,
709    content_interaction: impl FnOnce(
710        Layout<'_>,
711        Point,
712        &Rectangle,
713    ) -> mouse::Interaction,
714) -> mouse::Interaction {
715    let bounds = layout.bounds();
716    let mouse_over_scrollable = bounds.contains(cursor_position);
717
718    let content_layout = layout.children().next().unwrap();
719    let content_bounds = content_layout.bounds();
720
721    let scrollbars =
722        Scrollbars::new(state, vertical, horizontal, bounds, content_bounds);
723
724    let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
725        scrollbars.is_mouse_over(cursor_position);
726
727    if (mouse_over_x_scrollbar || mouse_over_y_scrollbar)
728        || state.scrollers_grabbed()
729    {
730        mouse::Interaction::Idle
731    } else {
732        let offset = state.offset(bounds, content_bounds);
733
734        let cursor_position = if mouse_over_scrollable
735            && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar)
736        {
737            cursor_position + offset
738        } else {
739            Point::new(-1.0, -1.0)
740        };
741
742        content_interaction(
743            content_layout,
744            cursor_position,
745            &Rectangle {
746                y: bounds.y + offset.y,
747                x: bounds.x + offset.x,
748                ..bounds
749            },
750        )
751    }
752}
753
754/// Draws a [`Scrollable`].
755pub fn draw<Renderer>(
756    state: &State,
757    renderer: &mut Renderer,
758    theme: &Renderer::Theme,
759    layout: Layout<'_>,
760    cursor_position: Point,
761    vertical: &Properties,
762    horizontal: Option<&Properties>,
763    style: &<Renderer::Theme as StyleSheet>::Style,
764    draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &Rectangle),
765) where
766    Renderer: crate::Renderer,
767    Renderer::Theme: StyleSheet,
768{
769    let bounds = layout.bounds();
770    let content_layout = layout.children().next().unwrap();
771    let content_bounds = content_layout.bounds();
772
773    let scrollbars =
774        Scrollbars::new(state, vertical, horizontal, bounds, content_bounds);
775
776    let mouse_over_scrollable = bounds.contains(cursor_position);
777    let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
778        scrollbars.is_mouse_over(cursor_position);
779
780    let offset = state.offset(bounds, content_bounds);
781
782    let cursor_position = if mouse_over_scrollable
783        && !(mouse_over_x_scrollbar || mouse_over_y_scrollbar)
784    {
785        cursor_position + offset
786    } else {
787        Point::new(-1.0, -1.0)
788    };
789
790    // Draw inner content
791    if scrollbars.active() {
792        renderer.with_layer(bounds, |renderer| {
793            renderer.with_translation(
794                Vector::new(-offset.x, -offset.y),
795                |renderer| {
796                    draw_content(
797                        renderer,
798                        content_layout,
799                        cursor_position,
800                        &Rectangle {
801                            y: bounds.y + offset.y,
802                            x: bounds.x + offset.x,
803                            ..bounds
804                        },
805                    );
806                },
807            );
808        });
809
810        let draw_scrollbar =
811            |renderer: &mut Renderer,
812             style: style::Scrollbar,
813             scrollbar: &Scrollbar| {
814                //track
815                if style.background.is_some()
816                    || (style.border_color != Color::TRANSPARENT
817                        && style.border_width > 0.0)
818                {
819                    renderer.fill_quad(
820                        renderer::Quad {
821                            bounds: scrollbar.bounds,
822                            border_radius: style.border_radius.into(),
823                            border_width: style.border_width,
824                            border_color: style.border_color,
825                        },
826                        style
827                            .background
828                            .unwrap_or(Background::Color(Color::TRANSPARENT)),
829                    );
830                }
831
832                //thumb
833                if style.scroller.color != Color::TRANSPARENT
834                    || (style.scroller.border_color != Color::TRANSPARENT
835                        && style.scroller.border_width > 0.0)
836                {
837                    renderer.fill_quad(
838                        renderer::Quad {
839                            bounds: scrollbar.scroller.bounds,
840                            border_radius: style.scroller.border_radius.into(),
841                            border_width: style.scroller.border_width,
842                            border_color: style.scroller.border_color,
843                        },
844                        style.scroller.color,
845                    );
846                }
847            };
848
849        renderer.with_layer(
850            Rectangle {
851                width: bounds.width + 2.0,
852                height: bounds.height + 2.0,
853                ..bounds
854            },
855            |renderer| {
856                //draw y scrollbar
857                if let Some(scrollbar) = scrollbars.y {
858                    let style = if state.y_scroller_grabbed_at.is_some() {
859                        theme.dragging(style)
860                    } else if mouse_over_scrollable {
861                        theme.hovered(style, mouse_over_y_scrollbar)
862                    } else {
863                        theme.active(style)
864                    };
865
866                    draw_scrollbar(renderer, style, &scrollbar);
867                }
868
869                //draw x scrollbar
870                if let Some(scrollbar) = scrollbars.x {
871                    let style = if state.x_scroller_grabbed_at.is_some() {
872                        theme.dragging_horizontal(style)
873                    } else if mouse_over_scrollable {
874                        theme.hovered_horizontal(style, mouse_over_x_scrollbar)
875                    } else {
876                        theme.active_horizontal(style)
877                    };
878
879                    draw_scrollbar(renderer, style, &scrollbar);
880                }
881            },
882        );
883    } else {
884        draw_content(
885            renderer,
886            content_layout,
887            cursor_position,
888            &Rectangle {
889                x: bounds.x + offset.x,
890                y: bounds.y + offset.y,
891                ..bounds
892            },
893        );
894    }
895}
896
897fn notify_on_scroll<Message>(
898    state: &mut State,
899    on_scroll: &Option<Box<dyn Fn(RelativeOffset) -> Message + '_>>,
900    bounds: Rectangle,
901    content_bounds: Rectangle,
902    shell: &mut Shell<'_, Message>,
903) {
904    if let Some(on_scroll) = on_scroll {
905        if content_bounds.width <= bounds.width
906            && content_bounds.height <= bounds.height
907        {
908            return;
909        }
910
911        let x = state.offset_x.absolute(bounds.width, content_bounds.width)
912            / (content_bounds.width - bounds.width);
913
914        let y = state
915            .offset_y
916            .absolute(bounds.height, content_bounds.height)
917            / (content_bounds.height - bounds.height);
918
919        let new_offset = RelativeOffset { x, y };
920
921        // Don't publish redundant offsets to shell
922        if let Some(prev_offset) = state.last_notified {
923            let unchanged = |a: f32, b: f32| {
924                (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan())
925            };
926
927            if unchanged(prev_offset.x, new_offset.x)
928                && unchanged(prev_offset.y, new_offset.y)
929            {
930                return;
931            }
932        }
933
934        shell.publish(on_scroll(new_offset));
935        state.last_notified = Some(new_offset);
936    }
937}
938
939/// The local state of a [`Scrollable`].
940#[derive(Debug, Clone, Copy)]
941pub struct State {
942    scroll_area_touched_at: Option<Point>,
943    offset_y: Offset,
944    y_scroller_grabbed_at: Option<f32>,
945    offset_x: Offset,
946    x_scroller_grabbed_at: Option<f32>,
947    keyboard_modifiers: keyboard::Modifiers,
948    last_notified: Option<RelativeOffset>,
949}
950
951impl Default for State {
952    fn default() -> Self {
953        Self {
954            scroll_area_touched_at: None,
955            offset_y: Offset::Absolute(0.0),
956            y_scroller_grabbed_at: None,
957            offset_x: Offset::Absolute(0.0),
958            x_scroller_grabbed_at: None,
959            keyboard_modifiers: keyboard::Modifiers::default(),
960            last_notified: None,
961        }
962    }
963}
964
965impl operation::Scrollable for State {
966    fn snap_to(&mut self, offset: RelativeOffset) {
967        State::snap_to(self, offset);
968    }
969}
970
971#[derive(Debug, Clone, Copy)]
972enum Offset {
973    Absolute(f32),
974    Relative(f32),
975}
976
977impl Offset {
978    fn absolute(self, window: f32, content: f32) -> f32 {
979        match self {
980            Offset::Absolute(absolute) => {
981                absolute.min((content - window).max(0.0))
982            }
983            Offset::Relative(percentage) => {
984                ((content - window) * percentage).max(0.0)
985            }
986        }
987    }
988}
989
990impl State {
991    /// Creates a new [`State`] with the scrollbar(s) at the beginning.
992    pub fn new() -> Self {
993        State::default()
994    }
995
996    /// Apply a scrolling offset to the current [`State`], given the bounds of
997    /// the [`Scrollable`] and its contents.
998    pub fn scroll(
999        &mut self,
1000        delta: Vector<f32>,
1001        bounds: Rectangle,
1002        content_bounds: Rectangle,
1003    ) {
1004        if bounds.height < content_bounds.height {
1005            self.offset_y = Offset::Absolute(
1006                (self.offset_y.absolute(bounds.height, content_bounds.height)
1007                    - delta.y)
1008                    .clamp(0.0, content_bounds.height - bounds.height),
1009            )
1010        }
1011
1012        if bounds.width < content_bounds.width {
1013            self.offset_x = Offset::Absolute(
1014                (self.offset_x.absolute(bounds.width, content_bounds.width)
1015                    - delta.x)
1016                    .clamp(0.0, content_bounds.width - bounds.width),
1017            );
1018        }
1019    }
1020
1021    /// Scrolls the [`Scrollable`] to a relative amount along the y axis.
1022    ///
1023    /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at
1024    /// the end.
1025    pub fn scroll_y_to(
1026        &mut self,
1027        percentage: f32,
1028        bounds: Rectangle,
1029        content_bounds: Rectangle,
1030    ) {
1031        self.offset_y = Offset::Relative(percentage.clamp(0.0, 1.0));
1032        self.unsnap(bounds, content_bounds);
1033    }
1034
1035    /// Scrolls the [`Scrollable`] to a relative amount along the x axis.
1036    ///
1037    /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at
1038    /// the end.
1039    pub fn scroll_x_to(
1040        &mut self,
1041        percentage: f32,
1042        bounds: Rectangle,
1043        content_bounds: Rectangle,
1044    ) {
1045        self.offset_x = Offset::Relative(percentage.clamp(0.0, 1.0));
1046        self.unsnap(bounds, content_bounds);
1047    }
1048
1049    /// Snaps the scroll position to a [`RelativeOffset`].
1050    pub fn snap_to(&mut self, offset: RelativeOffset) {
1051        self.offset_x = Offset::Relative(offset.x.clamp(0.0, 1.0));
1052        self.offset_y = Offset::Relative(offset.y.clamp(0.0, 1.0));
1053    }
1054
1055    /// Unsnaps the current scroll position, if snapped, given the bounds of the
1056    /// [`Scrollable`] and its contents.
1057    pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) {
1058        self.offset_x = Offset::Absolute(
1059            self.offset_x.absolute(bounds.width, content_bounds.width),
1060        );
1061        self.offset_y = Offset::Absolute(
1062            self.offset_y.absolute(bounds.height, content_bounds.height),
1063        );
1064    }
1065
1066    /// Returns the scrolling offset of the [`State`], given the bounds of the
1067    /// [`Scrollable`] and its contents.
1068    pub fn offset(
1069        &self,
1070        bounds: Rectangle,
1071        content_bounds: Rectangle,
1072    ) -> Vector {
1073        Vector::new(
1074            self.offset_x.absolute(bounds.width, content_bounds.width),
1075            self.offset_y.absolute(bounds.height, content_bounds.height),
1076        )
1077    }
1078
1079    /// Returns whether any scroller is currently grabbed or not.
1080    pub fn scrollers_grabbed(&self) -> bool {
1081        self.x_scroller_grabbed_at.is_some()
1082            || self.y_scroller_grabbed_at.is_some()
1083    }
1084}
1085
1086#[derive(Debug)]
1087/// State of both [`Scrollbar`]s.
1088struct Scrollbars {
1089    y: Option<Scrollbar>,
1090    x: Option<Scrollbar>,
1091}
1092
1093impl Scrollbars {
1094    /// Create y and/or x scrollbar(s) if content is overflowing the [`Scrollable`] bounds.
1095    fn new(
1096        state: &State,
1097        vertical: &Properties,
1098        horizontal: Option<&Properties>,
1099        bounds: Rectangle,
1100        content_bounds: Rectangle,
1101    ) -> Self {
1102        let offset = state.offset(bounds, content_bounds);
1103
1104        let show_scrollbar_x = horizontal.and_then(|h| {
1105            if content_bounds.width > bounds.width {
1106                Some(h)
1107            } else {
1108                None
1109            }
1110        });
1111
1112        let y_scrollbar = if content_bounds.height > bounds.height {
1113            let Properties {
1114                width,
1115                margin,
1116                scroller_width,
1117            } = *vertical;
1118
1119            // Adjust the height of the vertical scrollbar if the horizontal scrollbar
1120            // is present
1121            let x_scrollbar_height = show_scrollbar_x
1122                .map_or(0.0, |h| h.width.max(h.scroller_width) + h.margin);
1123
1124            let total_scrollbar_width =
1125                width.max(scroller_width) + 2.0 * margin;
1126
1127            // Total bounds of the scrollbar + margin + scroller width
1128            let total_scrollbar_bounds = Rectangle {
1129                x: bounds.x + bounds.width - total_scrollbar_width,
1130                y: bounds.y,
1131                width: total_scrollbar_width,
1132                height: (bounds.height - x_scrollbar_height).max(0.0),
1133            };
1134
1135            // Bounds of just the scrollbar
1136            let scrollbar_bounds = Rectangle {
1137                x: bounds.x + bounds.width
1138                    - total_scrollbar_width / 2.0
1139                    - width / 2.0,
1140                y: bounds.y,
1141                width,
1142                height: (bounds.height - x_scrollbar_height).max(0.0),
1143            };
1144
1145            let ratio = bounds.height / content_bounds.height;
1146            // min height for easier grabbing with super tall content
1147            let scroller_height = (bounds.height * ratio).max(2.0);
1148            let scroller_offset = offset.y * ratio;
1149
1150            let scroller_bounds = Rectangle {
1151                x: bounds.x + bounds.width
1152                    - total_scrollbar_width / 2.0
1153                    - scroller_width / 2.0,
1154                y: (scrollbar_bounds.y + scroller_offset - x_scrollbar_height)
1155                    .max(0.0),
1156                width: scroller_width,
1157                height: scroller_height,
1158            };
1159
1160            Some(Scrollbar {
1161                total_bounds: total_scrollbar_bounds,
1162                bounds: scrollbar_bounds,
1163                scroller: Scroller {
1164                    bounds: scroller_bounds,
1165                },
1166            })
1167        } else {
1168            None
1169        };
1170
1171        let x_scrollbar = if let Some(horizontal) = show_scrollbar_x {
1172            let Properties {
1173                width,
1174                margin,
1175                scroller_width,
1176            } = *horizontal;
1177
1178            // Need to adjust the width of the horizontal scrollbar if the vertical scrollbar
1179            // is present
1180            let scrollbar_y_width = y_scrollbar.map_or(0.0, |_| {
1181                vertical.width.max(vertical.scroller_width) + vertical.margin
1182            });
1183
1184            let total_scrollbar_height =
1185                width.max(scroller_width) + 2.0 * margin;
1186
1187            // Total bounds of the scrollbar + margin + scroller width
1188            let total_scrollbar_bounds = Rectangle {
1189                x: bounds.x,
1190                y: bounds.y + bounds.height - total_scrollbar_height,
1191                width: (bounds.width - scrollbar_y_width).max(0.0),
1192                height: total_scrollbar_height,
1193            };
1194
1195            // Bounds of just the scrollbar
1196            let scrollbar_bounds = Rectangle {
1197                x: bounds.x,
1198                y: bounds.y + bounds.height
1199                    - total_scrollbar_height / 2.0
1200                    - width / 2.0,
1201                width: (bounds.width - scrollbar_y_width).max(0.0),
1202                height: width,
1203            };
1204
1205            let ratio = bounds.width / content_bounds.width;
1206            // min width for easier grabbing with extra wide content
1207            let scroller_length = (bounds.width * ratio).max(2.0);
1208            let scroller_offset = offset.x * ratio;
1209
1210            let scroller_bounds = Rectangle {
1211                x: (scrollbar_bounds.x + scroller_offset - scrollbar_y_width)
1212                    .max(0.0),
1213                y: bounds.y + bounds.height
1214                    - total_scrollbar_height / 2.0
1215                    - scroller_width / 2.0,
1216                width: scroller_length,
1217                height: scroller_width,
1218            };
1219
1220            Some(Scrollbar {
1221                total_bounds: total_scrollbar_bounds,
1222                bounds: scrollbar_bounds,
1223                scroller: Scroller {
1224                    bounds: scroller_bounds,
1225                },
1226            })
1227        } else {
1228            None
1229        };
1230
1231        Self {
1232            y: y_scrollbar,
1233            x: x_scrollbar,
1234        }
1235    }
1236
1237    fn is_mouse_over(&self, cursor_position: Point) -> (bool, bool) {
1238        (
1239            self.y
1240                .as_ref()
1241                .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
1242                .unwrap_or(false),
1243            self.x
1244                .as_ref()
1245                .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
1246                .unwrap_or(false),
1247        )
1248    }
1249
1250    fn grab_y_scroller(&self, cursor_position: Point) -> Option<f32> {
1251        self.y.and_then(|scrollbar| {
1252            if scrollbar.total_bounds.contains(cursor_position) {
1253                Some(if scrollbar.scroller.bounds.contains(cursor_position) {
1254                    (cursor_position.y - scrollbar.scroller.bounds.y)
1255                        / scrollbar.scroller.bounds.height
1256                } else {
1257                    0.5
1258                })
1259            } else {
1260                None
1261            }
1262        })
1263    }
1264
1265    fn grab_x_scroller(&self, cursor_position: Point) -> Option<f32> {
1266        self.x.and_then(|scrollbar| {
1267            if scrollbar.total_bounds.contains(cursor_position) {
1268                Some(if scrollbar.scroller.bounds.contains(cursor_position) {
1269                    (cursor_position.x - scrollbar.scroller.bounds.x)
1270                        / scrollbar.scroller.bounds.width
1271                } else {
1272                    0.5
1273                })
1274            } else {
1275                None
1276            }
1277        })
1278    }
1279
1280    fn active(&self) -> bool {
1281        self.y.is_some() || self.x.is_some()
1282    }
1283}
1284
1285/// The scrollbar of a [`Scrollable`].
1286#[derive(Debug, Copy, Clone)]
1287struct Scrollbar {
1288    /// The total bounds of the [`Scrollbar`], including the scrollbar, the scroller,
1289    /// and the scrollbar margin.
1290    total_bounds: Rectangle,
1291
1292    /// The bounds of just the [`Scrollbar`].
1293    bounds: Rectangle,
1294
1295    /// The state of this scrollbar's [`Scroller`].
1296    scroller: Scroller,
1297}
1298
1299impl Scrollbar {
1300    /// Returns whether the mouse is over the scrollbar or not.
1301    fn is_mouse_over(&self, cursor_position: Point) -> bool {
1302        self.total_bounds.contains(cursor_position)
1303    }
1304
1305    /// Returns the y-axis scrolled percentage from the cursor position.
1306    fn scroll_percentage_y(
1307        &self,
1308        grabbed_at: f32,
1309        cursor_position: Point,
1310    ) -> f32 {
1311        if cursor_position.x < 0.0 && cursor_position.y < 0.0 {
1312            // cursor position is unavailable! Set to either end or beginning of scrollbar depending
1313            // on where the thumb currently is in the track
1314            (self.scroller.bounds.y / self.total_bounds.height).round()
1315        } else {
1316            (cursor_position.y
1317                - self.bounds.y
1318                - self.scroller.bounds.height * grabbed_at)
1319                / (self.bounds.height - self.scroller.bounds.height)
1320        }
1321    }
1322
1323    /// Returns the x-axis scrolled percentage from the cursor position.
1324    fn scroll_percentage_x(
1325        &self,
1326        grabbed_at: f32,
1327        cursor_position: Point,
1328    ) -> f32 {
1329        if cursor_position.x < 0.0 && cursor_position.y < 0.0 {
1330            (self.scroller.bounds.x / self.total_bounds.width).round()
1331        } else {
1332            (cursor_position.x
1333                - self.bounds.x
1334                - self.scroller.bounds.width * grabbed_at)
1335                / (self.bounds.width - self.scroller.bounds.width)
1336        }
1337    }
1338}
1339
1340/// The handle of a [`Scrollbar`].
1341#[derive(Debug, Clone, Copy)]
1342struct Scroller {
1343    /// The bounds of the [`Scroller`].
1344    bounds: Rectangle,
1345}