Skip to main content

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
236impl<D: PartialEq + 'static, B: Fn(usize, &D) -> Element + 'static> Component
237    for VirtualScrollView<D, B>
238{
239    fn render(self: &VirtualScrollView<D, B>) -> impl IntoElement {
240        let focus = use_focus();
241        let mut timeout = use_timeout(|| Duration::from_millis(800));
242        let mut pressing_shift = use_state(|| false);
243        let mut pressing_alt = use_state(|| false);
244        let mut clicking_scrollbar = use_state::<Option<(Axis, f64)>>(|| None);
245        let mut size = use_state(SizedEventData::default);
246        let mut scroll_controller = self
247            .scroll_controller
248            .unwrap_or_else(|| use_scroll_controller(ScrollConfig::default));
249        let (scrolled_x, scrolled_y) = scroll_controller.into();
250        let layout = &self.layout.layout;
251        let direction = layout.direction;
252
253        let (inner_width, inner_height) = match direction {
254            Direction::Vertical => (
255                size.read().inner_sizes.width,
256                self.item_size * self.length as f32,
257            ),
258            Direction::Horizontal => (
259                self.item_size * self.length as f32,
260                size.read().inner_sizes.height,
261            ),
262        };
263
264        scroll_controller.use_apply(inner_width, inner_height);
265
266        let corrected_scrolled_x =
267            get_corrected_scroll_position(inner_width, size.read().area.width(), scrolled_x as f32);
268
269        let corrected_scrolled_y = get_corrected_scroll_position(
270            inner_height,
271            size.read().area.height(),
272            scrolled_y as f32,
273        );
274        let horizontal_scrollbar_is_visible = !timeout.elapsed()
275            && is_scrollbar_visible(self.show_scrollbar, inner_width, size.read().area.width());
276        let vertical_scrollbar_is_visible = !timeout.elapsed()
277            && is_scrollbar_visible(self.show_scrollbar, inner_height, size.read().area.height());
278
279        let (scrollbar_x, scrollbar_width) =
280            get_scrollbar_pos_and_size(inner_width, size.read().area.width(), corrected_scrolled_x);
281        let (scrollbar_y, scrollbar_height) = get_scrollbar_pos_and_size(
282            inner_height,
283            size.read().area.height(),
284            corrected_scrolled_y,
285        );
286
287        let (container_width, content_width) = get_container_sizes(self.layout.width.clone());
288        let (container_height, content_height) = get_container_sizes(self.layout.height.clone());
289
290        let scroll_with_arrows = self.scroll_with_arrows;
291        let invert_scroll_wheel = self.invert_scroll_wheel;
292
293        let on_global_mouse_up = move |_| {
294            clicking_scrollbar.set_if_modified(None);
295        };
296
297        let on_wheel = move |e: Event<WheelEventData>| {
298            // Only invert direction on deviced-sourced wheel events
299            let invert_direction = e.source == WheelSource::Device
300                && (*pressing_shift.read() || invert_scroll_wheel)
301                && (!*pressing_shift.read() || !invert_scroll_wheel);
302
303            let (x_movement, y_movement) = if invert_direction {
304                (e.delta_y as f32, e.delta_x as f32)
305            } else {
306                (e.delta_x as f32, e.delta_y as f32)
307            };
308
309            // Vertical scroll
310            let scroll_position_y = get_scroll_position_from_wheel(
311                y_movement,
312                inner_height,
313                size.read().area.height(),
314                corrected_scrolled_y,
315            );
316            scroll_controller.scroll_to_y(scroll_position_y).then(|| {
317                e.stop_propagation();
318            });
319
320            // Horizontal scroll
321            let scroll_position_x = get_scroll_position_from_wheel(
322                x_movement,
323                inner_width,
324                size.read().area.width(),
325                corrected_scrolled_x,
326            );
327            scroll_controller.scroll_to_x(scroll_position_x).then(|| {
328                e.stop_propagation();
329            });
330            timeout.reset();
331        };
332
333        let on_mouse_move = move |_| {
334            timeout.reset();
335        };
336
337        let on_capture_global_mouse_move = move |e: Event<MouseEventData>| {
338            let clicking_scrollbar = clicking_scrollbar.peek();
339
340            if let Some((Axis::Y, y)) = *clicking_scrollbar {
341                let coordinates = e.element_location;
342                let cursor_y = coordinates.y - y - size.read().area.min_y() as f64;
343
344                let scroll_position = get_scroll_position_from_cursor(
345                    cursor_y as f32,
346                    inner_height,
347                    size.read().area.height(),
348                );
349
350                scroll_controller.scroll_to_y(scroll_position);
351            } else if let Some((Axis::X, x)) = *clicking_scrollbar {
352                let coordinates = e.element_location;
353                let cursor_x = coordinates.x - x - size.read().area.min_x() as f64;
354
355                let scroll_position = get_scroll_position_from_cursor(
356                    cursor_x as f32,
357                    inner_width,
358                    size.read().area.width(),
359                );
360
361                scroll_controller.scroll_to_x(scroll_position);
362            }
363
364            if clicking_scrollbar.is_some() {
365                e.prevent_default();
366                timeout.reset();
367                if !focus.is_focused() {
368                    focus.request_focus();
369                }
370            }
371        };
372
373        let on_key_down = move |e: Event<KeyboardEventData>| {
374            if !scroll_with_arrows
375                && (e.key == Key::Named(NamedKey::ArrowUp)
376                    || e.key == Key::Named(NamedKey::ArrowRight)
377                    || e.key == Key::Named(NamedKey::ArrowDown)
378                    || e.key == Key::Named(NamedKey::ArrowLeft))
379            {
380                return;
381            }
382            let x = corrected_scrolled_x;
383            let y = corrected_scrolled_y;
384            let inner_height = inner_height;
385            let inner_width = inner_width;
386            let viewport_height = size.read().area.height();
387            let viewport_width = size.read().area.width();
388            if let Some((x, y)) = handle_key_event(
389                &e.key,
390                (x, y),
391                inner_height,
392                inner_width,
393                viewport_height,
394                viewport_width,
395                direction,
396            ) {
397                scroll_controller.scroll_to_x(x as i32);
398                scroll_controller.scroll_to_y(y as i32);
399                e.stop_propagation();
400                timeout.reset();
401            }
402        };
403
404        let on_global_key_down = move |e: Event<KeyboardEventData>| {
405            let data = e;
406            if data.key == Key::Named(NamedKey::Shift) {
407                pressing_shift.set(true);
408            } else if data.key == Key::Named(NamedKey::Alt) {
409                pressing_alt.set(true);
410            }
411        };
412
413        let on_global_key_up = move |e: Event<KeyboardEventData>| {
414            let data = e;
415            if data.key == Key::Named(NamedKey::Shift) {
416                pressing_shift.set(false);
417            } else if data.key == Key::Named(NamedKey::Alt) {
418                pressing_alt.set(false);
419            }
420        };
421
422        let (viewport_size, scroll_position) = if direction == Direction::vertical() {
423            (size.read().area.height(), corrected_scrolled_y)
424        } else {
425            (size.read().area.width(), corrected_scrolled_x)
426        };
427
428        let render_range = get_render_range(
429            viewport_size,
430            scroll_position,
431            self.item_size,
432            self.length as f32,
433        );
434
435        let children = render_range
436            .clone()
437            .map(|i| (self.builder)(i, &self.builder_data))
438            .collect::<Vec<Element>>();
439
440        let (offset_x, offset_y) = match direction {
441            Direction::Vertical => {
442                let offset_y_min =
443                    (-corrected_scrolled_y / self.item_size).floor() * self.item_size;
444                let offset_y = -(-corrected_scrolled_y - offset_y_min);
445
446                (corrected_scrolled_x, offset_y)
447            }
448            Direction::Horizontal => {
449                let offset_x_min =
450                    (-corrected_scrolled_x / self.item_size).floor() * self.item_size;
451                let offset_x = -(-corrected_scrolled_x - offset_x_min);
452
453                (offset_x, corrected_scrolled_y)
454            }
455        };
456
457        rect()
458            .width(layout.width.clone())
459            .height(layout.height.clone())
460            .a11y_id(focus.a11y_id())
461            .a11y_focusable(false)
462            .a11y_role(AccessibilityRole::ScrollView)
463            .a11y_builder(move |node| {
464                node.set_scroll_x(corrected_scrolled_x as f64);
465                node.set_scroll_y(corrected_scrolled_y as f64)
466            })
467            .scrollable(true)
468            .on_wheel(on_wheel)
469            .on_global_mouse_up(on_global_mouse_up)
470            .on_mouse_move(on_mouse_move)
471            .on_capture_global_mouse_move(on_capture_global_mouse_move)
472            .on_key_down(on_key_down)
473            .on_global_key_up(on_global_key_up)
474            .on_global_key_down(on_global_key_down)
475            .child(
476                rect()
477                    .width(container_width)
478                    .height(container_height)
479                    .horizontal()
480                    .child(
481                        rect()
482                            .direction(direction)
483                            .width(content_width)
484                            .height(content_height)
485                            .offset_x(offset_x)
486                            .offset_y(offset_y)
487                            .overflow(Overflow::Clip)
488                            .on_sized(move |e: Event<SizedEventData>| {
489                                size.set_if_modified(e.clone())
490                            })
491                            .children(children),
492                    )
493                    .maybe_child(vertical_scrollbar_is_visible.then_some({
494                        rect().child(ScrollBar {
495                            theme: None,
496                            clicking_scrollbar,
497                            axis: Axis::Y,
498                            offset: scrollbar_y,
499                            thumb: ScrollThumb {
500                                theme: None,
501                                clicking_scrollbar,
502                                axis: Axis::Y,
503                                size: scrollbar_height,
504                            },
505                        })
506                    })),
507            )
508            .maybe_child(horizontal_scrollbar_is_visible.then_some({
509                rect().child(ScrollBar {
510                    theme: None,
511                    clicking_scrollbar,
512                    axis: Axis::X,
513                    offset: scrollbar_x,
514                    thumb: ScrollThumb {
515                        theme: None,
516                        clicking_scrollbar,
517                        axis: Axis::X,
518                        size: scrollbar_width,
519                    },
520                })
521            }))
522    }
523
524    fn render_key(&self) -> DiffKey {
525        self.key.clone().or(self.default_key())
526    }
527}
528
529fn get_render_range(
530    viewport_size: f32,
531    scroll_position: f32,
532    item_size: f32,
533    item_length: f32,
534) -> Range<usize> {
535    let render_index_start = (-scroll_position) / item_size;
536    let potentially_visible_length = (viewport_size / item_size) + 1.0;
537    let remaining_length = item_length - render_index_start;
538
539    let render_index_end = if remaining_length <= potentially_visible_length {
540        item_length
541    } else {
542        render_index_start + potentially_visible_length
543    };
544
545    render_index_start as usize..(render_index_end as usize)
546}