freya_components/scrollviews/
scrollview.rs

1use std::time::Duration;
2
3use freya_core::prelude::*;
4use freya_sdk::timeout::use_timeout;
5use torin::{
6    prelude::Direction,
7    size::Size,
8};
9
10use crate::scrollviews::{
11    ScrollBar,
12    ScrollConfig,
13    ScrollController,
14    ScrollThumb,
15    shared::{
16        Axis,
17        get_container_sizes,
18        get_corrected_scroll_position,
19        get_scroll_position_from_cursor,
20        get_scroll_position_from_wheel,
21        get_scrollbar_pos_and_size,
22        handle_key_event,
23        is_scrollbar_visible,
24    },
25    use_scroll_controller,
26};
27
28/// Scrollable area with bidirectional support and scrollbars.
29///
30/// # Example
31///
32/// ```rust
33/// # use freya::prelude::*;
34/// fn app() -> impl IntoElement {
35///     ScrollView::new()
36///         .child("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum laoreet tristique diam, ut gravida enim. Phasellus viverra vitae risus sit amet iaculis. Morbi porttitor quis nisl eu vulputate. Etiam vitae ligula a purus suscipit iaculis non ac risus. Suspendisse potenti. Aenean orci massa, ornare ut elit id, tristique commodo dui. Vestibulum laoreet tristique diam, ut gravida enim. Phasellus viverra vitae risus sit amet iaculis. Vestibulum laoreet tristique diam, ut gravida enim. Phasellus viverra vitae risus sit amet iaculis. Vestibulum laoreet tristique diam, ut gravida enim. Phasellus viverra vitae risus sit amet iaculis.")
37/// }
38///
39/// # use freya_testing::prelude::*;
40/// # launch_doc(|| {
41/// #   rect().center().expanded().child(app())
42/// # },
43/// # "./images/gallery_scrollview.png")
44/// #
45/// # .with_hook(|t| {
46/// #   t.move_cursor((125., 115.));
47/// #   t.sync_and_update();
48/// # });
49/// ```
50///
51/// # Preview
52/// ![ScrollView Preview][scrollview]
53#[cfg_attr(feature = "docs",
54    doc = embed_doc_image::embed_image!("scrollview", "images/gallery_scrollview.png")
55)]
56#[derive(Clone, PartialEq)]
57pub struct ScrollView {
58    children: Vec<Element>,
59    width: Size,
60    height: Size,
61    show_scrollbar: bool,
62    direction: Direction,
63    spacing: f32,
64    scroll_with_arrows: bool,
65    scroll_controller: Option<ScrollController>,
66    invert_scroll_wheel: bool,
67    key: DiffKey,
68}
69
70impl ChildrenExt for ScrollView {
71    fn get_children(&mut self) -> &mut Vec<Element> {
72        &mut self.children
73    }
74}
75
76impl KeyExt for ScrollView {
77    fn write_key(&mut self) -> &mut DiffKey {
78        &mut self.key
79    }
80}
81
82impl Default for ScrollView {
83    fn default() -> Self {
84        Self {
85            children: Vec::default(),
86            width: Size::fill(),
87            height: Size::fill(),
88            show_scrollbar: true,
89            direction: Direction::Vertical,
90            spacing: 0.,
91            scroll_with_arrows: true,
92            scroll_controller: None,
93            invert_scroll_wheel: false,
94            key: DiffKey::None,
95        }
96    }
97}
98
99impl ScrollView {
100    pub fn new() -> Self {
101        Self::default()
102    }
103
104    pub fn new_controlled(scroll_controller: ScrollController) -> Self {
105        Self {
106            children: Vec::default(),
107            width: Size::fill(),
108            height: Size::fill(),
109            show_scrollbar: true,
110            direction: Direction::Vertical,
111            spacing: 0.,
112            scroll_with_arrows: true,
113            scroll_controller: Some(scroll_controller),
114            invert_scroll_wheel: false,
115            key: DiffKey::None,
116        }
117    }
118
119    pub fn show_scrollbar(mut self, show_scrollbar: bool) -> Self {
120        self.show_scrollbar = show_scrollbar;
121        self
122    }
123
124    pub fn width(mut self, width: Size) -> Self {
125        self.width = width;
126        self
127    }
128
129    pub fn height(mut self, height: Size) -> Self {
130        self.height = height;
131        self
132    }
133
134    pub fn direction(mut self, direction: Direction) -> Self {
135        self.direction = direction;
136        self
137    }
138
139    pub fn spacing(mut self, spacing: impl Into<f32>) -> Self {
140        self.spacing = spacing.into();
141        self
142    }
143
144    pub fn scroll_with_arrows(mut self, scroll_with_arrows: impl Into<bool>) -> Self {
145        self.scroll_with_arrows = scroll_with_arrows.into();
146        self
147    }
148
149    pub fn invert_scroll_wheel(mut self, invert_scroll_wheel: impl Into<bool>) -> Self {
150        self.invert_scroll_wheel = invert_scroll_wheel.into();
151        self
152    }
153}
154
155impl Render for ScrollView {
156    fn render(self: &ScrollView) -> impl IntoElement {
157        let focus = use_focus();
158        let mut timeout = use_timeout(|| Duration::from_millis(800));
159        let mut pressing_shift = use_state(|| false);
160        let mut pressing_alt = use_state(|| false);
161        let mut clicking_scrollbar = use_state::<Option<(Axis, f64)>>(|| None);
162        let mut size = use_state(SizedEventData::default);
163        let mut scroll_controller = self
164            .scroll_controller
165            .unwrap_or_else(|| use_scroll_controller(ScrollConfig::default));
166        let (scrolled_x, scrolled_y) = scroll_controller.into();
167        let direction = self.direction;
168
169        scroll_controller.use_apply(
170            size.read().inner_sizes.width,
171            size.read().inner_sizes.height,
172        );
173
174        let corrected_scrolled_x = get_corrected_scroll_position(
175            size.read().inner_sizes.width,
176            size.read().area.width(),
177            scrolled_x as f32,
178        );
179
180        let corrected_scrolled_y = get_corrected_scroll_position(
181            size.read().inner_sizes.height,
182            size.read().area.height(),
183            scrolled_y as f32,
184        );
185        let horizontal_scrollbar_is_visible = !timeout.elapsed()
186            && is_scrollbar_visible(
187                self.show_scrollbar,
188                size.read().inner_sizes.width,
189                size.read().area.width(),
190            );
191        let vertical_scrollbar_is_visible = !timeout.elapsed()
192            && is_scrollbar_visible(
193                self.show_scrollbar,
194                size.read().inner_sizes.height,
195                size.read().area.height(),
196            );
197
198        let (scrollbar_x, scrollbar_width) = get_scrollbar_pos_and_size(
199            size.read().inner_sizes.width,
200            size.read().area.width(),
201            corrected_scrolled_x,
202        );
203        let (scrollbar_y, scrollbar_height) = get_scrollbar_pos_and_size(
204            size.read().inner_sizes.height,
205            size.read().area.height(),
206            corrected_scrolled_y,
207        );
208
209        let (container_width, content_width) = get_container_sizes(self.width.clone());
210        let (container_height, content_height) = get_container_sizes(self.height.clone());
211
212        let scroll_with_arrows = self.scroll_with_arrows;
213        let invert_scroll_wheel = self.invert_scroll_wheel;
214
215        let on_global_mouse_up = move |_| {
216            clicking_scrollbar.set_if_modified(None);
217        };
218
219        let on_wheel = move |e: Event<WheelEventData>| {
220            // Only invert direction on deviced-sourced wheel events
221            let invert_direction = e.source == WheelSource::Device
222                && (*pressing_shift.read() || invert_scroll_wheel)
223                && (!*pressing_shift.read() || !invert_scroll_wheel);
224
225            let (x_movement, y_movement) = if invert_direction {
226                (e.delta_y as f32, e.delta_x as f32)
227            } else {
228                (e.delta_x as f32, e.delta_y as f32)
229            };
230
231            // Vertical scroll
232            let scroll_position_y = get_scroll_position_from_wheel(
233                y_movement,
234                size.read().inner_sizes.height,
235                size.read().area.height(),
236                corrected_scrolled_y,
237            );
238            scroll_controller.scroll_to_y(scroll_position_y).then(|| {
239                e.stop_propagation();
240            });
241
242            // Horizontal scroll
243            let scroll_position_x = get_scroll_position_from_wheel(
244                x_movement,
245                size.read().inner_sizes.width,
246                size.read().area.width(),
247                corrected_scrolled_x,
248            );
249            scroll_controller.scroll_to_x(scroll_position_x).then(|| {
250                e.stop_propagation();
251            });
252            timeout.reset();
253        };
254
255        let on_mouse_move = move |_| {
256            timeout.reset();
257        };
258
259        let on_capture_global_mouse_move = move |e: Event<MouseEventData>| {
260            let clicking_scrollbar = clicking_scrollbar.peek();
261
262            if let Some((Axis::Y, y)) = *clicking_scrollbar {
263                let coordinates = e.element_location;
264                let cursor_y = coordinates.y - y - size.read().area.min_y() as f64;
265
266                let scroll_position = get_scroll_position_from_cursor(
267                    cursor_y as f32,
268                    size.read().inner_sizes.height,
269                    size.read().area.height(),
270                );
271
272                scroll_controller.scroll_to_y(scroll_position);
273            } else if let Some((Axis::X, x)) = *clicking_scrollbar {
274                let coordinates = e.element_location;
275                let cursor_x = coordinates.x - x - size.read().area.min_x() as f64;
276
277                let scroll_position = get_scroll_position_from_cursor(
278                    cursor_x as f32,
279                    size.read().inner_sizes.width,
280                    size.read().area.width(),
281                );
282
283                scroll_controller.scroll_to_x(scroll_position);
284            }
285
286            if clicking_scrollbar.is_some() {
287                e.prevent_default();
288                timeout.reset();
289                if !focus.is_focused() {
290                    focus.request_focus();
291                }
292            }
293        };
294
295        let on_key_down = move |e: Event<KeyboardEventData>| {
296            if !scroll_with_arrows
297                && (e.key == Key::Named(NamedKey::ArrowUp)
298                    || e.key == Key::Named(NamedKey::ArrowRight)
299                    || e.key == Key::Named(NamedKey::ArrowDown)
300                    || e.key == Key::Named(NamedKey::ArrowLeft))
301            {
302                return;
303            }
304            let x = corrected_scrolled_x;
305            let y = corrected_scrolled_y;
306            let inner_height = size.read().inner_sizes.height;
307            let inner_width = size.read().inner_sizes.width;
308            let viewport_height = size.read().area.height();
309            let viewport_width = size.read().area.width();
310            if let Some((x, y)) = handle_key_event(
311                &e.key,
312                (x, y),
313                inner_height,
314                inner_width,
315                viewport_height,
316                viewport_width,
317                direction,
318            ) {
319                scroll_controller.scroll_to_x(x as i32);
320                scroll_controller.scroll_to_y(y as i32);
321                e.stop_propagation();
322                timeout.reset();
323            }
324        };
325
326        let on_global_key_down = move |e: Event<KeyboardEventData>| {
327            let data = e;
328            if data.key == Key::Named(NamedKey::Shift) {
329                pressing_shift.set(true);
330            } else if data.key == Key::Named(NamedKey::Alt) {
331                pressing_alt.set(true);
332            }
333        };
334
335        let on_global_key_up = move |e: Event<KeyboardEventData>| {
336            let data = e;
337            if data.key == Key::Named(NamedKey::Shift) {
338                pressing_shift.set(false);
339            } else if data.key == Key::Named(NamedKey::Alt) {
340                pressing_alt.set(false);
341            }
342        };
343
344        rect()
345            .width(self.width.clone())
346            .height(self.height.clone())
347            .a11y_id(focus.a11y_id())
348            .a11y_focusable(false)
349            .a11y_role(AccessibilityRole::ScrollView)
350            .a11y_builder(move |node| {
351                node.set_scroll_x(corrected_scrolled_x as f64);
352                node.set_scroll_y(corrected_scrolled_y as f64)
353            })
354            .scrollable(true)
355            .on_wheel(on_wheel)
356            .on_global_mouse_up(on_global_mouse_up)
357            .on_mouse_move(on_mouse_move)
358            .on_capture_global_mouse_move(on_capture_global_mouse_move)
359            .on_key_down(on_key_down)
360            .on_global_key_up(on_global_key_up)
361            .on_global_key_down(on_global_key_down)
362            .child(
363                rect()
364                    .width(container_width)
365                    .height(container_height)
366                    .horizontal()
367                    .child(
368                        rect()
369                            .direction(self.direction)
370                            .width(content_width)
371                            .height(content_height)
372                            .offset_x(corrected_scrolled_x)
373                            .offset_y(corrected_scrolled_y)
374                            .spacing(self.spacing)
375                            .overflow(Overflow::Clip)
376                            .on_sized(move |e: Event<SizedEventData>| {
377                                size.set_if_modified(e.clone())
378                            })
379                            .children(self.children.clone()),
380                    )
381                    .maybe_child(vertical_scrollbar_is_visible.then_some({
382                        rect().child(ScrollBar {
383                            theme: None,
384                            clicking_scrollbar,
385                            axis: Axis::Y,
386                            offset: scrollbar_y,
387                            thumb: ScrollThumb {
388                                theme: None,
389                                clicking_scrollbar,
390                                axis: Axis::Y,
391                                size: scrollbar_height,
392                            },
393                        })
394                    })),
395            )
396            .maybe_child(horizontal_scrollbar_is_visible.then_some({
397                rect().child(ScrollBar {
398                    theme: None,
399                    clicking_scrollbar,
400                    axis: Axis::X,
401                    offset: scrollbar_x,
402                    thumb: ScrollThumb {
403                        theme: None,
404                        clicking_scrollbar,
405                        axis: Axis::X,
406                        size: scrollbar_width,
407                    },
408                })
409            }))
410    }
411
412    fn render_key(&self) -> DiffKey {
413        self.key.clone().or(self.default_key())
414    }
415}