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