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