freya_components/scrollviews/
virtual_scrollview.rs

1use std::{
2    ops::Range,
3    time::Duration,
4};
5
6use freya_core::prelude::*;
7use freya_sdk::timeout::use_timeout;
8use torin::{
9    node::Node,
10    prelude::Direction,
11    size::Size,
12};
13
14use crate::scrollviews::{
15    ScrollBar,
16    ScrollConfig,
17    ScrollController,
18    ScrollThumb,
19    shared::{
20        Axis,
21        get_container_sizes,
22        get_corrected_scroll_position,
23        get_scroll_position_from_cursor,
24        get_scroll_position_from_wheel,
25        get_scrollbar_pos_and_size,
26        handle_key_event,
27        is_scrollbar_visible,
28    },
29    use_scroll_controller,
30};
31
32/// One-direction scrollable area that dynamically builds and renders items based in their size and current available size,
33/// this is intended for apps using large sets of data that need good performance.
34///
35/// # Example
36///
37/// ```rust
38/// # use freya::prelude::*;
39/// fn app() -> impl IntoElement {
40///     rect().child(
41///         VirtualScrollView::new(|i, _| {
42///             rect()
43///                 .key(i)
44///                 .height(Size::px(25.))
45///                 .padding(4.)
46///                 .child(format!("Item {i}"))
47///                 .into()
48///         })
49///         .length(300)
50///         .item_size(25.),
51///     )
52/// }
53///
54/// # use freya_testing::prelude::*;
55/// # launch_doc(|| {
56/// #   rect().center().expanded().child(app())
57/// # }, "./images/gallery_virtual_scrollview.png").with_hook(|t| {
58/// #   t.move_cursor((125., 115.));
59/// #   t.sync_and_update();
60/// # });
61/// ```
62///
63/// # Preview
64/// ![VirtualScrollView Preview][virtual_scrollview]
65#[cfg_attr(feature = "docs",
66    doc = embed_doc_image::embed_image!("virtual_scrollview", "images/gallery_virtual_scrollview.png")
67)]
68#[derive(Clone)]
69pub struct VirtualScrollView<D, B: Fn(usize, &D) -> Element> {
70    builder: B,
71    builder_data: D,
72    item_size: f32,
73    length: i32,
74    layout: LayoutData,
75    show_scrollbar: bool,
76    scroll_with_arrows: bool,
77    scroll_controller: Option<ScrollController>,
78    invert_scroll_wheel: bool,
79    key: DiffKey,
80}
81
82impl<D: PartialEq, B: Fn(usize, &D) -> Element> LayoutExt for VirtualScrollView<D, B> {
83    fn get_layout(&mut self) -> &mut LayoutData {
84        &mut self.layout
85    }
86}
87
88impl<D: PartialEq, B: Fn(usize, &D) -> Element> ContainerSizeExt for VirtualScrollView<D, B> {}
89
90impl<D: PartialEq, B: Fn(usize, &D) -> Element> KeyExt for VirtualScrollView<D, B> {
91    fn write_key(&mut self) -> &mut DiffKey {
92        &mut self.key
93    }
94}
95
96impl<D: PartialEq, B: Fn(usize, &D) -> Element> PartialEq for VirtualScrollView<D, B> {
97    fn eq(&self, other: &Self) -> bool {
98        self.builder_data == other.builder_data
99            && self.item_size == other.item_size
100            && self.length == other.length
101            && self.layout == other.layout
102            && self.show_scrollbar == other.show_scrollbar
103            && self.scroll_with_arrows == other.scroll_with_arrows
104            && self.scroll_controller == other.scroll_controller
105            && self.invert_scroll_wheel == other.invert_scroll_wheel
106    }
107}
108
109impl<B: Fn(usize, &()) -> Element> VirtualScrollView<(), B> {
110    pub fn new(builder: B) -> Self {
111        Self {
112            builder,
113            builder_data: (),
114            item_size: 0.,
115            length: 0,
116            layout: {
117                let mut l = LayoutData::default();
118                l.layout.width = Size::fill();
119                l.layout.height = Size::fill();
120                l
121            },
122            show_scrollbar: true,
123            scroll_with_arrows: true,
124            scroll_controller: None,
125            invert_scroll_wheel: false,
126            key: DiffKey::None,
127        }
128    }
129
130    pub fn new_controlled(builder: B, scroll_controller: ScrollController) -> Self {
131        Self {
132            builder,
133            builder_data: (),
134            item_size: 0.,
135            length: 0,
136            layout: {
137                let mut l = LayoutData::default();
138                l.layout.width = Size::fill();
139                l.layout.height = Size::fill();
140                l
141            },
142            show_scrollbar: true,
143            scroll_with_arrows: true,
144            scroll_controller: Some(scroll_controller),
145            invert_scroll_wheel: false,
146            key: DiffKey::None,
147        }
148    }
149}
150
151impl<D, B: Fn(usize, &D) -> Element> VirtualScrollView<D, B> {
152    pub fn new_with_data(builder_data: D, builder: B) -> Self {
153        Self {
154            builder,
155            builder_data,
156            item_size: 0.,
157            length: 0,
158            layout: Node {
159                width: Size::fill(),
160                height: Size::fill(),
161                ..Default::default()
162            }
163            .into(),
164            show_scrollbar: true,
165            scroll_with_arrows: true,
166            scroll_controller: None,
167            invert_scroll_wheel: false,
168            key: DiffKey::None,
169        }
170    }
171
172    pub fn new_with_data_controlled(
173        builder_data: D,
174        builder: B,
175        scroll_controller: ScrollController,
176    ) -> Self {
177        Self {
178            builder,
179            builder_data,
180            item_size: 0.,
181            length: 0,
182
183            layout: Node {
184                width: Size::fill(),
185                height: Size::fill(),
186                ..Default::default()
187            }
188            .into(),
189            show_scrollbar: true,
190            scroll_with_arrows: true,
191            scroll_controller: Some(scroll_controller),
192            invert_scroll_wheel: false,
193            key: DiffKey::None,
194        }
195    }
196
197    pub fn show_scrollbar(mut self, show_scrollbar: bool) -> Self {
198        self.show_scrollbar = show_scrollbar;
199        self
200    }
201
202    pub fn direction(mut self, direction: Direction) -> Self {
203        self.layout.direction = direction;
204        self
205    }
206
207    pub fn scroll_with_arrows(mut self, scroll_with_arrows: impl Into<bool>) -> Self {
208        self.scroll_with_arrows = scroll_with_arrows.into();
209        self
210    }
211
212    pub fn item_size(mut self, item_size: impl Into<f32>) -> Self {
213        self.item_size = item_size.into();
214        self
215    }
216
217    pub fn length(mut self, length: impl Into<i32>) -> Self {
218        self.length = length.into();
219        self
220    }
221
222    pub fn invert_scroll_wheel(mut self, invert_scroll_wheel: impl Into<bool>) -> Self {
223        self.invert_scroll_wheel = invert_scroll_wheel.into();
224        self
225    }
226
227    pub fn scroll_controller(
228        mut self,
229        scroll_controller: impl Into<Option<ScrollController>>,
230    ) -> Self {
231        self.scroll_controller = scroll_controller.into();
232        self
233    }
234
235    pub fn max_width(mut self, max_width: impl Into<Size>) -> Self {
236        self.layout.maximum_width = max_width.into();
237        self
238    }
239
240    pub fn max_height(mut self, max_height: impl Into<Size>) -> Self {
241        self.layout.maximum_height = max_height.into();
242        self
243    }
244}
245
246impl<D: PartialEq + 'static, B: Fn(usize, &D) -> Element + 'static> Component
247    for VirtualScrollView<D, B>
248{
249    fn render(self: &VirtualScrollView<D, B>) -> impl IntoElement {
250        let focus = use_focus();
251        let mut timeout = use_timeout(|| Duration::from_millis(800));
252        let mut pressing_shift = use_state(|| false);
253        let mut clicking_scrollbar = use_state::<Option<(Axis, f64)>>(|| None);
254        let mut size = use_state(SizedEventData::default);
255        let mut scroll_controller = self
256            .scroll_controller
257            .unwrap_or_else(|| use_scroll_controller(ScrollConfig::default));
258        let (scrolled_x, scrolled_y) = scroll_controller.into();
259        let layout = &self.layout.layout;
260        let direction = layout.direction;
261
262        let (inner_width, inner_height) = match direction {
263            Direction::Vertical => (
264                size.read().inner_sizes.width,
265                self.item_size * self.length as f32,
266            ),
267            Direction::Horizontal => (
268                self.item_size * self.length as f32,
269                size.read().inner_sizes.height,
270            ),
271        };
272
273        scroll_controller.use_apply(inner_width, inner_height);
274
275        let corrected_scrolled_x =
276            get_corrected_scroll_position(inner_width, size.read().area.width(), scrolled_x as f32);
277
278        let corrected_scrolled_y = get_corrected_scroll_position(
279            inner_height,
280            size.read().area.height(),
281            scrolled_y as f32,
282        );
283        let horizontal_scrollbar_is_visible = !timeout.elapsed()
284            && is_scrollbar_visible(self.show_scrollbar, inner_width, size.read().area.width());
285        let vertical_scrollbar_is_visible = !timeout.elapsed()
286            && is_scrollbar_visible(self.show_scrollbar, inner_height, size.read().area.height());
287
288        let (scrollbar_x, scrollbar_width) =
289            get_scrollbar_pos_and_size(inner_width, size.read().area.width(), corrected_scrolled_x);
290        let (scrollbar_y, scrollbar_height) = get_scrollbar_pos_and_size(
291            inner_height,
292            size.read().area.height(),
293            corrected_scrolled_y,
294        );
295
296        let (container_width, content_width) = get_container_sizes(self.layout.width.clone());
297        let (container_height, content_height) = get_container_sizes(self.layout.height.clone());
298
299        let scroll_with_arrows = self.scroll_with_arrows;
300        let invert_scroll_wheel = self.invert_scroll_wheel;
301
302        let on_capture_global_pointer_press = move |e: Event<PointerEventData>| {
303            if clicking_scrollbar.read().is_some() {
304                e.prevent_default();
305                clicking_scrollbar.set(None);
306            }
307        };
308
309        let on_wheel = move |e: Event<WheelEventData>| {
310            // Only invert direction on deviced-sourced wheel events
311            let invert_direction = e.source == WheelSource::Device
312                && (*pressing_shift.read() || invert_scroll_wheel)
313                && (!*pressing_shift.read() || !invert_scroll_wheel);
314
315            let (x_movement, y_movement) = if invert_direction {
316                (e.delta_y as f32, e.delta_x as f32)
317            } else {
318                (e.delta_x as f32, e.delta_y as f32)
319            };
320
321            // Vertical scroll
322            let scroll_position_y = get_scroll_position_from_wheel(
323                y_movement,
324                inner_height,
325                size.read().area.height(),
326                corrected_scrolled_y,
327            );
328            scroll_controller.scroll_to_y(scroll_position_y).then(|| {
329                e.stop_propagation();
330            });
331
332            // Horizontal scroll
333            let scroll_position_x = get_scroll_position_from_wheel(
334                x_movement,
335                inner_width,
336                size.read().area.width(),
337                corrected_scrolled_x,
338            );
339            scroll_controller.scroll_to_x(scroll_position_x).then(|| {
340                e.stop_propagation();
341            });
342            timeout.reset();
343        };
344
345        let on_mouse_move = move |_| {
346            timeout.reset();
347        };
348
349        let on_capture_global_pointer_move = move |e: Event<PointerEventData>| {
350            let clicking_scrollbar = clicking_scrollbar.peek();
351
352            if let Some((Axis::Y, y)) = *clicking_scrollbar {
353                let coordinates = e.element_location();
354                let cursor_y = coordinates.y - y - size.read().area.min_y() as f64;
355
356                let scroll_position = get_scroll_position_from_cursor(
357                    cursor_y as f32,
358                    inner_height,
359                    size.read().area.height(),
360                );
361
362                scroll_controller.scroll_to_y(scroll_position);
363            } else if let Some((Axis::X, x)) = *clicking_scrollbar {
364                let coordinates = e.element_location();
365                let cursor_x = coordinates.x - x - size.read().area.min_x() as f64;
366
367                let scroll_position = get_scroll_position_from_cursor(
368                    cursor_x as f32,
369                    inner_width,
370                    size.read().area.width(),
371                );
372
373                scroll_controller.scroll_to_x(scroll_position);
374            }
375
376            if clicking_scrollbar.is_some() {
377                e.prevent_default();
378                timeout.reset();
379                if !focus.is_focused() {
380                    focus.request_focus();
381                }
382            }
383        };
384
385        let on_key_down = move |e: Event<KeyboardEventData>| {
386            if !scroll_with_arrows
387                && (e.key == Key::Named(NamedKey::ArrowUp)
388                    || e.key == Key::Named(NamedKey::ArrowRight)
389                    || e.key == Key::Named(NamedKey::ArrowDown)
390                    || e.key == Key::Named(NamedKey::ArrowLeft))
391            {
392                return;
393            }
394            let x = corrected_scrolled_x;
395            let y = corrected_scrolled_y;
396            let inner_height = inner_height;
397            let inner_width = inner_width;
398            let viewport_height = size.read().area.height();
399            let viewport_width = size.read().area.width();
400            if let Some((x, y)) = handle_key_event(
401                &e.key,
402                (x, y),
403                inner_height,
404                inner_width,
405                viewport_height,
406                viewport_width,
407                direction,
408            ) {
409                scroll_controller.scroll_to_x(x as i32);
410                scroll_controller.scroll_to_y(y as i32);
411                e.stop_propagation();
412                timeout.reset();
413            }
414        };
415
416        let on_global_key_down = move |e: Event<KeyboardEventData>| {
417            let data = e;
418            if data.key == Key::Named(NamedKey::Shift) {
419                pressing_shift.set(true);
420            }
421        };
422
423        let on_global_key_up = move |e: Event<KeyboardEventData>| {
424            let data = e;
425            if data.key == Key::Named(NamedKey::Shift) {
426                pressing_shift.set(false);
427            }
428        };
429
430        let (viewport_size, scroll_position) = if direction == Direction::vertical() {
431            (size.read().area.height(), corrected_scrolled_y)
432        } else {
433            (size.read().area.width(), corrected_scrolled_x)
434        };
435
436        let render_range = get_render_range(
437            viewport_size,
438            scroll_position,
439            self.item_size,
440            self.length as f32,
441        );
442
443        let children = render_range
444            .clone()
445            .map(|i| (self.builder)(i, &self.builder_data))
446            .collect::<Vec<Element>>();
447
448        let (offset_x, offset_y) = match direction {
449            Direction::Vertical => {
450                let offset_y_min =
451                    (-corrected_scrolled_y / self.item_size).floor() * self.item_size;
452                let offset_y = -(-corrected_scrolled_y - offset_y_min);
453
454                (corrected_scrolled_x, offset_y)
455            }
456            Direction::Horizontal => {
457                let offset_x_min =
458                    (-corrected_scrolled_x / self.item_size).floor() * self.item_size;
459                let offset_x = -(-corrected_scrolled_x - offset_x_min);
460
461                (offset_x, corrected_scrolled_y)
462            }
463        };
464
465        rect()
466            .width(layout.width.clone())
467            .height(layout.height.clone())
468            .a11y_id(focus.a11y_id())
469            .a11y_focusable(false)
470            .a11y_role(AccessibilityRole::ScrollView)
471            .a11y_builder(move |node| {
472                node.set_scroll_x(corrected_scrolled_x as f64);
473                node.set_scroll_y(corrected_scrolled_y as f64)
474            })
475            .scrollable(true)
476            .on_wheel(on_wheel)
477            .on_capture_global_pointer_press(on_capture_global_pointer_press)
478            .on_mouse_move(on_mouse_move)
479            .on_capture_global_pointer_move(on_capture_global_pointer_move)
480            .on_key_down(on_key_down)
481            .on_global_key_up(on_global_key_up)
482            .on_global_key_down(on_global_key_down)
483            .child(
484                rect()
485                    .width(container_width.clone())
486                    .height(container_height.clone())
487                    .horizontal()
488                    .child(
489                        rect()
490                            .direction(direction)
491                            .width(content_width)
492                            .height(content_height)
493                            .offset_x(offset_x)
494                            .offset_y(offset_y)
495                            .overflow(Overflow::Clip)
496                            .on_sized(move |e: Event<SizedEventData>| {
497                                size.set_if_modified(e.clone())
498                            })
499                            .children(children),
500                    )
501                    .maybe_child(vertical_scrollbar_is_visible.then_some({
502                        rect().child(ScrollBar {
503                            theme: None,
504                            clicking_scrollbar,
505                            axis: Axis::Y,
506                            offset: scrollbar_y,
507                            size: Size::px(size.read().area.height()),
508                            thumb: ScrollThumb {
509                                theme: None,
510                                clicking_scrollbar,
511                                axis: Axis::Y,
512                                size: scrollbar_height,
513                            },
514                        })
515                    })),
516            )
517            .maybe_child(horizontal_scrollbar_is_visible.then_some({
518                rect().child(ScrollBar {
519                    theme: None,
520                    clicking_scrollbar,
521                    axis: Axis::X,
522                    offset: scrollbar_x,
523                    size: Size::px(size.read().area.width()),
524                    thumb: ScrollThumb {
525                        theme: None,
526                        clicking_scrollbar,
527                        axis: Axis::X,
528                        size: scrollbar_width,
529                    },
530                })
531            }))
532    }
533
534    fn render_key(&self) -> DiffKey {
535        self.key.clone().or(self.default_key())
536    }
537}
538
539fn get_render_range(
540    viewport_size: f32,
541    scroll_position: f32,
542    item_size: f32,
543    item_length: f32,
544) -> Range<usize> {
545    let render_index_start = (-scroll_position) / item_size;
546    let potentially_visible_length = (viewport_size / item_size) + 1.0;
547    let remaining_length = item_length - render_index_start;
548
549    let render_index_end = if remaining_length <= potentially_visible_length {
550        item_length
551    } else {
552        render_index_start + potentially_visible_length
553    };
554
555    render_index_start as usize..(render_index_end as usize)
556}