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