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