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