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::{
10        CursorPoint,
11        Point2D,
12        Size2D,
13    },
14    node::Node,
15    prelude::Direction,
16    size::Size,
17};
18
19use crate::scrollviews::{
20    ScrollBar,
21    ScrollBarContext,
22    ScrollBarThumbEvents,
23    ScrollConfig,
24    ScrollController,
25    shared::{
26        Axis,
27        get_container_sizes,
28        get_corrected_scroll_position,
29        get_scroll_position_from_cursor,
30        get_scroll_position_from_wheel,
31        get_scrollbar_pos_and_size,
32        handle_key_event,
33        is_scrollbar_visible,
34    },
35    use_scroll_controller,
36};
37
38/// Defines how each item of a [`VirtualScrollView`] is sized along the scroll axis.
39///
40/// Build one from a fixed value or from a closure that resolves the size of each
41/// item by its index:
42///
43/// ```rust
44/// # use freya::prelude::*;
45/// let fixed: ItemSize = 25.0f32.into();
46/// let dynamic: ItemSize =
47///     (|index: usize| if index.is_multiple_of(2) { 25.0 } else { 50.0 }).into();
48/// ```
49#[derive(Clone, PartialEq)]
50pub enum ItemSize {
51    /// Every item shares the same size in pixels.
52    Fixed(f32),
53    /// Each item is sized individually through a callback that receives its index.
54    Dynamic(Callback<usize, f32>),
55}
56
57impl ItemSize {
58    /// Size in pixels of the item at `index`.
59    fn at(&self, index: usize) -> f32 {
60        match self {
61            Self::Fixed(size) => *size,
62            Self::Dynamic(callback) => callback.call(index),
63        }
64    }
65
66    /// Range of items that fall inside the viewport for the given scroll position,
67    /// together with the offset that positions the first visible item correctly.
68    fn visible_range(
69        &self,
70        viewport_size: f32,
71        scroll_position: f32,
72        length: usize,
73    ) -> (Range<usize>, f32) {
74        let scroll_distance = (-scroll_position).max(0.0);
75        match self {
76            Self::Fixed(size) => {
77                if *size <= 0.0 {
78                    return (0..0, 0.0);
79                }
80                let start = scroll_distance / size;
81                let potentially_visible = (viewport_size / size) + 1.0;
82                let start_index = (start as usize).min(length);
83                let end_index = ((start + potentially_visible) as usize).min(length);
84                (
85                    start_index..end_index,
86                    start.floor() * size - scroll_distance,
87                )
88            }
89            Self::Dynamic(callback) => {
90                let mut start = 0;
91                let mut cumulative = 0.0;
92                while start < length {
93                    let item = callback.call(start);
94                    if cumulative + item > scroll_distance {
95                        break;
96                    }
97                    cumulative += item;
98                    start += 1;
99                }
100                let offset = cumulative - scroll_distance;
101                let mut end = start;
102                while end < length && cumulative < scroll_distance + viewport_size {
103                    cumulative += callback.call(end);
104                    end += 1;
105                }
106                (start..end, offset)
107            }
108        }
109    }
110
111    /// Total size of the content along the scroll axis.
112    ///
113    /// [`Self::Fixed`] is exact. [`Self::Dynamic`] extrapolates from the average size of
114    /// the items down to the viewport bottom, keeping the scrollbar stable as it scrolls.
115    fn total_size(&self, viewport_size: f32, scroll_position: f32, length: usize) -> f32 {
116        match self {
117            Self::Fixed(size) => size * length as f32,
118            Self::Dynamic(callback) => {
119                if length == 0 {
120                    return 0.0;
121                }
122                let viewport_bottom = (-scroll_position).max(0.0) + viewport_size;
123                let mut measured = callback.call(0);
124                let mut count = 1;
125                while count < length && measured < viewport_bottom {
126                    measured += callback.call(count);
127                    count += 1;
128                }
129                (measured / count as f32) * length as f32
130            }
131        }
132    }
133}
134
135impl Default for ItemSize {
136    fn default() -> Self {
137        Self::Fixed(0.)
138    }
139}
140
141impl From<f32> for ItemSize {
142    fn from(size: f32) -> Self {
143        Self::Fixed(size)
144    }
145}
146
147impl<F: Fn(usize) -> f32 + 'static> From<F> for ItemSize {
148    fn from(callback: F) -> Self {
149        Self::Dynamic(Callback::new(callback))
150    }
151}
152
153/// Data passed to a [`VirtualScrollView`] builder for each rendered item.
154#[derive(Debug, Clone, Copy, PartialEq)]
155pub struct VirtualItem {
156    pub index: usize,
157    pub size: f32,
158}
159
160/// One-direction scrollable area that dynamically builds and renders items based in their size and current available size,
161/// this is intended for apps using large sets of data that need good performance.
162///
163/// Unlike [`ScrollView`](crate::scrollviews::ScrollView), which lays out every child even when it is
164/// off screen, a `VirtualScrollView` takes a builder closure and only calls it for the items that
165/// are actually visible, so the cost stays roughly constant no matter how long the list is.
166///
167/// It needs two things to know which items fall inside the viewport:
168/// [`item_size`](VirtualScrollView::item_size), the fixed size of each item along the scroll axis,
169/// and [`length`](VirtualScrollView::length), the total number of items.
170///
171/// # Example
172///
173/// ```rust
174/// # use freya::prelude::*;
175/// fn app() -> impl IntoElement {
176///     rect().child(
177///         VirtualScrollView::new(|item, _| {
178///             rect()
179///                 .key(item.index)
180///                 .height(Size::px(item.size))
181///                 .padding(4.)
182///                 .child(format!("Item {}", item.index))
183///                 .into()
184///         })
185///         .length(300usize)
186///         .item_size(25.),
187///     )
188/// }
189/// ```
190///
191/// See the [interactive components demo](https://freyaui.dev/demo).
192#[derive(Clone)]
193pub struct VirtualScrollView<D, B: Fn(VirtualItem, &D) -> Element> {
194    builder: B,
195    builder_data: D,
196    item_size: ItemSize,
197    length: usize,
198    layout: LayoutData,
199    show_scrollbar: bool,
200    auto_hide_scrollbars: bool,
201    scroll_with_arrows: bool,
202    scroll_controller: Option<ScrollController>,
203    on_sized: Option<EventHandler<Event<SizedEventData>>>,
204    invert_scroll_wheel: bool,
205    drag_scrolling: bool,
206    scrollbar: Callback<ScrollBarContext, Element>,
207    key: DiffKey,
208}
209
210impl<D: PartialEq, B: Fn(VirtualItem, &D) -> Element> LayoutExt for VirtualScrollView<D, B> {
211    fn layout_mut(&mut self) -> &mut LayoutData {
212        &mut self.layout
213    }
214}
215
216impl<D: PartialEq, B: Fn(VirtualItem, &D) -> Element> ContainerSizeExt for VirtualScrollView<D, B> {}
217
218impl<D: PartialEq, B: Fn(VirtualItem, &D) -> Element> KeyExt for VirtualScrollView<D, B> {
219    fn key_mut(&mut self) -> &mut DiffKey {
220        &mut self.key
221    }
222}
223
224impl<D: PartialEq, B: Fn(VirtualItem, &D) -> Element> PartialEq for VirtualScrollView<D, B> {
225    fn eq(&self, other: &Self) -> bool {
226        self.builder_data == other.builder_data
227            && self.item_size == other.item_size
228            && self.length == other.length
229            && self.layout == other.layout
230            && self.show_scrollbar == other.show_scrollbar
231            && self.auto_hide_scrollbars == other.auto_hide_scrollbars
232            && self.scroll_with_arrows == other.scroll_with_arrows
233            && self.scroll_controller == other.scroll_controller
234            && self.invert_scroll_wheel == other.invert_scroll_wheel
235            && self.scrollbar == other.scrollbar
236    }
237}
238
239impl<B: Fn(VirtualItem, &()) -> Element> VirtualScrollView<(), B> {
240    /// Creates a virtual scroll view that builds each item on demand from its [`VirtualItem`].
241    pub fn new(builder: B) -> Self {
242        Self {
243            builder,
244            builder_data: (),
245            item_size: ItemSize::default(),
246            length: 0,
247            layout: {
248                let mut l = LayoutData::default();
249                l.layout.width = Size::fill();
250                l.layout.height = Size::fill();
251                l
252            },
253            show_scrollbar: true,
254            auto_hide_scrollbars: true,
255            scroll_with_arrows: true,
256            scroll_controller: None,
257            on_sized: None,
258            invert_scroll_wheel: false,
259            drag_scrolling: true,
260            scrollbar: ScrollBar::default_renderer(),
261            key: DiffKey::None,
262        }
263    }
264
265    /// Like [`new`](Self::new) but driven by the given [`ScrollController`].
266    pub fn new_controlled(builder: B, scroll_controller: ScrollController) -> Self {
267        Self {
268            builder,
269            builder_data: (),
270            item_size: ItemSize::default(),
271            length: 0,
272            layout: {
273                let mut l = LayoutData::default();
274                l.layout.width = Size::fill();
275                l.layout.height = Size::fill();
276                l
277            },
278            show_scrollbar: true,
279            auto_hide_scrollbars: true,
280            scroll_with_arrows: true,
281            scroll_controller: Some(scroll_controller),
282            on_sized: None,
283            invert_scroll_wheel: false,
284            drag_scrolling: true,
285            scrollbar: ScrollBar::default_renderer(),
286            key: DiffKey::None,
287        }
288    }
289}
290
291impl<D, B: Fn(VirtualItem, &D) -> Element> VirtualScrollView<D, B> {
292    /// Like [`new`](Self::new) but passes shared `builder_data` to every item build.
293    ///
294    /// The builder closure cannot be compared across renders, so data captured inside it never
295    /// triggers a rebuild. Passing the data here instead makes it part of the view's `PartialEq`,
296    /// so the visible items are rebuilt whenever it changes.
297    ///
298    /// ```rust
299    /// # use freya::prelude::*;
300    /// fn app() -> impl IntoElement {
301    ///     let items = use_state(|| vec!["a".to_string(), "b".to_string(), "c".to_string()]);
302    ///
303    ///     // The current items are passed as data, so editing `items` rebuilds the visible rows.
304    ///     VirtualScrollView::new_with_data(items.read().clone(), |item, items: &Vec<String>| {
305    ///         rect()
306    ///             .key(item.index)
307    ///             .height(Size::px(item.size))
308    ///             .child(items[item.index].clone())
309    ///             .into()
310    ///     })
311    ///     .length(items.read().len())
312    ///     .item_size(25.)
313    /// }
314    /// ```
315    pub fn new_with_data(builder_data: D, builder: B) -> Self {
316        Self {
317            builder,
318            builder_data,
319            item_size: ItemSize::default(),
320            length: 0,
321            layout: Node {
322                width: Size::fill(),
323                height: Size::fill(),
324                ..Default::default()
325            }
326            .into(),
327            show_scrollbar: true,
328            auto_hide_scrollbars: true,
329            scroll_with_arrows: true,
330            scroll_controller: None,
331            on_sized: None,
332            invert_scroll_wheel: false,
333            drag_scrolling: true,
334            scrollbar: ScrollBar::default_renderer(),
335            key: DiffKey::None,
336        }
337    }
338
339    /// Like [`new_with_data`](Self::new_with_data) but driven by the given [`ScrollController`].
340    pub fn new_with_data_controlled(
341        builder_data: D,
342        builder: B,
343        scroll_controller: ScrollController,
344    ) -> Self {
345        Self {
346            builder,
347            builder_data,
348            item_size: ItemSize::default(),
349            length: 0,
350
351            layout: Node {
352                width: Size::fill(),
353                height: Size::fill(),
354                ..Default::default()
355            }
356            .into(),
357            show_scrollbar: true,
358            auto_hide_scrollbars: true,
359            scroll_with_arrows: true,
360            scroll_controller: Some(scroll_controller),
361            on_sized: None,
362            invert_scroll_wheel: false,
363            drag_scrolling: true,
364            scrollbar: ScrollBar::default_renderer(),
365            key: DiffKey::None,
366        }
367    }
368
369    /// Toggles whether the scrollbar is shown when the content overflows.
370    pub fn show_scrollbar(mut self, show_scrollbar: bool) -> Self {
371        self.show_scrollbar = show_scrollbar;
372        self
373    }
374
375    /// Toggles hiding idle scrollbars, enabled by default.
376    /// When disabled, scrollbars remain visible while the content overflows.
377    pub fn auto_hide_scrollbars(mut self, auto_hide_scrollbars: bool) -> Self {
378        self.auto_hide_scrollbars = auto_hide_scrollbars;
379        self
380    }
381
382    /// Sets the axis the items flow and scroll in.
383    pub fn direction(mut self, direction: Direction) -> Self {
384        self.layout.direction = direction;
385        self
386    }
387
388    /// Toggles whether the arrow keys scroll the view while it is focused.
389    pub fn scroll_with_arrows(mut self, scroll_with_arrows: impl Into<bool>) -> Self {
390        self.scroll_with_arrows = scroll_with_arrows.into();
391        self
392    }
393
394    /// Sets the size of the items along the scroll axis, used to decide which items to render.
395    ///
396    /// Accepts an [`ItemSize`], so it can be a fixed size for every item or a closure that
397    /// resolves the size of each item by its index.
398    pub fn item_size(mut self, item_size: impl Into<ItemSize>) -> Self {
399        self.item_size = item_size.into();
400        self
401    }
402
403    /// Sets the total number of items the view can scroll through.
404    pub fn length(mut self, length: impl Into<usize>) -> Self {
405        self.length = length.into();
406        self
407    }
408
409    /// Inverts the direction of the mouse wheel relative to the content.
410    pub fn invert_scroll_wheel(mut self, invert_scroll_wheel: impl Into<bool>) -> Self {
411        self.invert_scroll_wheel = invert_scroll_wheel.into();
412        self
413    }
414
415    /// Toggles scrolling by dragging the content, useful mainly for touch input.
416    pub fn drag_scrolling(mut self, drag_scrolling: bool) -> Self {
417        self.drag_scrolling = drag_scrolling;
418        self
419    }
420
421    /// Sets the renderer used for each visible scrollbar.
422    pub fn scrollbar(mut self, scrollbar: impl Into<Callback<ScrollBarContext, Element>>) -> Self {
423        self.scrollbar = scrollbar.into();
424        self
425    }
426
427    /// Attaches a [`ScrollController`] to drive this view externally.
428    pub fn scroll_controller(
429        mut self,
430        scroll_controller: impl Into<Option<ScrollController>>,
431    ) -> Self {
432        self.scroll_controller = scroll_controller.into();
433        self
434    }
435
436    /// Runs the handler with the size of the visible area, which excludes the scrollbars.
437    pub fn on_sized(mut self, on_sized: impl Into<EventHandler<Event<SizedEventData>>>) -> Self {
438        self.on_sized = Some(on_sized.into());
439        self
440    }
441
442    /// Sets the minimum width the scroll view can shrink to.
443    pub fn min_width(mut self, min_width: impl Into<Size>) -> Self {
444        self.layout.minimum_width = min_width.into();
445        self
446    }
447
448    /// Sets the minimum height the scroll view can shrink to.
449    pub fn min_height(mut self, min_height: impl Into<Size>) -> Self {
450        self.layout.minimum_height = min_height.into();
451        self
452    }
453
454    /// Caps the width of the scroll view.
455    pub fn max_width(mut self, max_width: impl Into<Size>) -> Self {
456        self.layout.maximum_width = max_width.into();
457        self
458    }
459
460    /// Caps the height of the scroll view.
461    pub fn max_height(mut self, max_height: impl Into<Size>) -> Self {
462        self.layout.maximum_height = max_height.into();
463        self
464    }
465}
466
467impl<D: PartialEq + 'static, B: Fn(VirtualItem, &D) -> Element + 'static> Component
468    for VirtualScrollView<D, B>
469{
470    fn render(self: &VirtualScrollView<D, B>) -> impl IntoElement {
471        let a11y_id = use_a11y();
472        let mut timeout = use_timeout(|| Duration::from_millis(800));
473        let mut pressing_shift = use_state(|| false);
474        let mut clicking_scrollbar = use_state::<Option<(Axis, f64)>>(|| None);
475        let mut size = use_state(SizedEventData::default);
476        let mut scroll_controller = self
477            .scroll_controller
478            .unwrap_or_else(|| use_scroll_controller(ScrollConfig::default));
479        let mut dragging_content = use_state::<Option<CursorPoint>>(|| None);
480        let mut drag_origin = use_state::<Option<CursorPoint>>(|| None);
481        let (scrolled_x, scrolled_y) = scroll_controller.into();
482        let layout = &self.layout.layout;
483        let direction = layout.direction;
484        let drag_scrolling = self.drag_scrolling;
485
486        let viewport_width = size.read().area.width();
487        let viewport_height = size.read().area.height();
488
489        let (inner_width, inner_height) = match direction {
490            Direction::Vertical => (
491                size.read().inner_sizes.width,
492                self.item_size
493                    .total_size(viewport_height, scrolled_y as f32, self.length),
494            ),
495            Direction::Horizontal => (
496                self.item_size
497                    .total_size(viewport_width, scrolled_x as f32, self.length),
498                size.read().inner_sizes.height,
499            ),
500        };
501
502        let corrected_scrolled_x =
503            get_corrected_scroll_position(inner_width, size.read().area.width(), scrolled_x as f32);
504
505        let corrected_scrolled_y = get_corrected_scroll_position(
506            inner_height,
507            size.read().area.height(),
508            scrolled_y as f32,
509        );
510
511        let smooth_position = scroll_controller
512            .animated_position(Point2D::new(corrected_scrolled_x, corrected_scrolled_y));
513        let rendered_position = Point2D::new(
514            get_corrected_scroll_position(inner_width, size.read().area.width(), smooth_position.x),
515            get_corrected_scroll_position(
516                inner_height,
517                size.read().area.height(),
518                smooth_position.y,
519            ),
520        );
521
522        let horizontal_scrollbar_is_visible =
523            is_scrollbar_visible(self.show_scrollbar, inner_width, size.read().area.width());
524        let vertical_scrollbar_is_visible =
525            is_scrollbar_visible(self.show_scrollbar, inner_height, size.read().area.height());
526
527        let (scrollbar_x, scrollbar_width) =
528            get_scrollbar_pos_and_size(inner_width, size.read().area.width(), rendered_position.x);
529        let (scrollbar_y, scrollbar_height) = get_scrollbar_pos_and_size(
530            inner_height,
531            size.read().area.height(),
532            rendered_position.y,
533        );
534
535        let (container_width, content_width) = get_container_sizes(self.layout.width.clone());
536        let (container_height, content_height) = get_container_sizes(self.layout.height.clone());
537
538        let scroll_with_arrows = self.scroll_with_arrows;
539        let invert_scroll_wheel = self.invert_scroll_wheel;
540
541        let on_capture_global_pointer_up = move |e: Event<PointerEventData>| {
542            if clicking_scrollbar.read().is_some() {
543                e.prevent_default();
544                clicking_scrollbar.set(None);
545            }
546
547            if drag_scrolling && (dragging_content().is_some() || drag_origin().is_some()) {
548                if dragging_content().is_some() {
549                    e.prevent_default();
550                    let content = Size2D::new(inner_width, inner_height);
551                    scroll_controller.release_drag(
552                        rendered_position,
553                        content,
554                        size.read().area.size,
555                    );
556                }
557                dragging_content.set(None);
558                drag_origin.set(None);
559            }
560        };
561
562        let on_wheel = move |e: Event<WheelEventData>| {
563            // Only invert direction on deviced-sourced wheel events
564            let invert_direction = e.source.is_device()
565                && (*pressing_shift.read() || invert_scroll_wheel)
566                && (!*pressing_shift.read() || !invert_scroll_wheel);
567
568            let (x_movement, y_movement) = if invert_direction {
569                (e.delta_y as f32, e.delta_x as f32)
570            } else {
571                (e.delta_x as f32, e.delta_y as f32)
572            };
573
574            // TODO(marc2332): Enable and test touchpad smooth scrolling in mac and windows
575            let scroll_gesture =
576                TargetPlatform::get() == TargetPlatform::Linux && e.source == WheelSource::Pixel;
577            if scroll_gesture
578                && scroll_controller.scroll_gesture_phase(e.phase, (x_movement, y_movement).into())
579            {
580                return;
581            }
582
583            let animate = e.source == WheelSource::Line || scroll_gesture;
584            if animate {
585                scroll_controller.animate_from(rendered_position);
586            } else {
587                scroll_controller.stop();
588            }
589            let (base_x, base_y) = if animate {
590                let position = scroll_controller.position();
591                (position.x, position.y)
592            } else {
593                (rendered_position.x, rendered_position.y)
594            };
595
596            // Vertical scroll
597            let scroll_position_y = get_scroll_position_from_wheel(
598                y_movement,
599                inner_height,
600                size.read().area.height(),
601                base_y,
602            );
603            scroll_controller.scroll_to_y(scroll_position_y).then(|| {
604                e.stop_propagation();
605            });
606
607            // Horizontal scroll
608            let scroll_position_x = get_scroll_position_from_wheel(
609                x_movement,
610                inner_width,
611                size.read().area.width(),
612                base_x,
613            );
614            scroll_controller.scroll_to_x(scroll_position_x).then(|| {
615                e.stop_propagation();
616            });
617            timeout.reset();
618        };
619
620        let on_mouse_move = move |_| {
621            timeout.reset();
622        };
623
624        let on_capture_global_pointer_move = move |e: Event<PointerEventData>| {
625            if drag_scrolling {
626                if let Some(prev) = dragging_content() {
627                    let coords = e.global_location();
628                    let delta = prev - coords;
629
630                    scroll_controller.drag(delta.to_f32());
631                    scroll_controller.scroll_to_y((rendered_position.y - delta.y as f32) as i32);
632                    scroll_controller.scroll_to_x((rendered_position.x - delta.x as f32) as i32);
633
634                    dragging_content.set(Some(coords));
635                    e.prevent_default();
636                    timeout.reset();
637                    a11y_id.request_focus();
638                    return;
639                } else if let Some(origin) = drag_origin() {
640                    let coords = e.global_location();
641                    let distance = (origin - coords).abs();
642
643                    // Small threshold so taps can reach children (e.g. hover on buttons)
644                    // without being immediately consumed by drag scrolling.
645                    const DRAG_THRESHOLD: f64 = 2.0;
646
647                    if distance.x > DRAG_THRESHOLD || distance.y > DRAG_THRESHOLD {
648                        let delta = origin - coords;
649
650                        scroll_controller.drag(delta.to_f32());
651                        scroll_controller
652                            .scroll_to_y((rendered_position.y - delta.y as f32) as i32);
653                        scroll_controller
654                            .scroll_to_x((rendered_position.x - delta.x as f32) as i32);
655
656                        dragging_content.set(Some(coords));
657                        e.prevent_default();
658                        timeout.reset();
659                        a11y_id.request_focus();
660                    }
661                    return;
662                }
663            }
664
665            let clicking_scrollbar = clicking_scrollbar.peek();
666
667            if clicking_scrollbar.is_some() {
668                scroll_controller.stop();
669            }
670
671            if let Some((Axis::Y, y)) = *clicking_scrollbar {
672                let coordinates = e.element_location();
673                let cursor_y = coordinates.y - y - size.read().area.min_y() as f64;
674
675                let scroll_position = get_scroll_position_from_cursor(
676                    cursor_y as f32,
677                    inner_height,
678                    size.read().area.height(),
679                );
680
681                scroll_controller.scroll_to_y(scroll_position);
682            } else if let Some((Axis::X, x)) = *clicking_scrollbar {
683                let coordinates = e.element_location();
684                let cursor_x = coordinates.x - x - size.read().area.min_x() as f64;
685
686                let scroll_position = get_scroll_position_from_cursor(
687                    cursor_x as f32,
688                    inner_width,
689                    size.read().area.width(),
690                );
691
692                scroll_controller.scroll_to_x(scroll_position);
693            }
694
695            if clicking_scrollbar.is_some() {
696                e.prevent_default();
697                timeout.reset();
698                a11y_id.request_focus();
699            }
700        };
701
702        let on_key_down = move |e: Event<KeyboardEventData>| {
703            if !scroll_with_arrows
704                && (e.key == Key::Named(NamedKey::ArrowUp)
705                    || e.key == Key::Named(NamedKey::ArrowRight)
706                    || e.key == Key::Named(NamedKey::ArrowDown)
707                    || e.key == Key::Named(NamedKey::ArrowLeft))
708            {
709                return;
710            }
711            let x = corrected_scrolled_x;
712            let y = corrected_scrolled_y;
713            let inner_height = inner_height;
714            let inner_width = inner_width;
715            let viewport_height = size.read().area.height();
716            let viewport_width = size.read().area.width();
717            if let Some((x, y)) = handle_key_event(
718                &e.key,
719                (x, y),
720                inner_height,
721                inner_width,
722                viewport_height,
723                viewport_width,
724                direction,
725            ) {
726                scroll_controller.animate_from(rendered_position);
727                scroll_controller.scroll_to_x(x as i32);
728                scroll_controller.scroll_to_y(y as i32);
729                e.stop_propagation();
730                timeout.reset();
731            }
732        };
733
734        let on_global_key_down = move |e: Event<KeyboardEventData>| {
735            let data = e;
736            if data.key == Key::Named(NamedKey::Shift) {
737                pressing_shift.set(true);
738            }
739        };
740
741        let on_global_key_up = move |e: Event<KeyboardEventData>| {
742            let data = e;
743            if data.key == Key::Named(NamedKey::Shift) {
744                pressing_shift.set(false);
745            }
746        };
747
748        let (viewport_size, scroll_position) = if direction == Direction::vertical() {
749            (viewport_height, rendered_position.y)
750        } else {
751            (viewport_width, rendered_position.x)
752        };
753
754        let (render_range, item_offset) =
755            self.item_size
756                .visible_range(viewport_size, scroll_position, self.length);
757
758        let children = render_range
759            .map(|i| {
760                let item = VirtualItem {
761                    index: i,
762                    size: self.item_size.at(i),
763                };
764                (self.builder)(item, &self.builder_data)
765            })
766            .collect::<Vec<Element>>();
767
768        let (offset_x, offset_y) = match direction {
769            Direction::Vertical => (rendered_position.x, item_offset),
770            Direction::Horizontal => (item_offset, rendered_position.y),
771        };
772
773        let on_pointer_down = move |e: Event<PointerEventData>| {
774            if drag_scrolling && matches!(e.data(), PointerEventData::Touch(_)) {
775                scroll_controller.begin_drag();
776                drag_origin.set(Some(e.global_location()));
777            }
778        };
779
780        rect()
781            .width(layout.width.clone())
782            .height(layout.height.clone())
783            .min_width(layout.minimum_width.clone())
784            .min_height(layout.minimum_height.clone())
785            .max_width(layout.maximum_width.clone())
786            .max_height(layout.maximum_height.clone())
787            .a11y_id(a11y_id)
788            .a11y_role(AccessibilityRole::ScrollView)
789            .a11y_builder(move |node| {
790                node.set_scroll_x(rendered_position.x as f64);
791                node.set_scroll_y(rendered_position.y as f64)
792            })
793            .scrollable(true)
794            .on_wheel(on_wheel)
795            .on_capture_global_pointer_up(on_capture_global_pointer_up)
796            .on_mouse_move(on_mouse_move)
797            .on_capture_global_pointer_move(on_capture_global_pointer_move)
798            .on_key_down(on_key_down)
799            .on_global_key_up(on_global_key_up)
800            .on_global_key_down(on_global_key_down)
801            .on_pointer_down(on_pointer_down)
802            .child(
803                rect()
804                    .width(container_width)
805                    .height(container_height)
806                    .horizontal()
807                    .child(
808                        rect()
809                            .direction(direction)
810                            .width(content_width)
811                            .height(content_height)
812                            .offset_x(offset_x)
813                            .offset_y(offset_y)
814                            .overflow(Overflow::Clip)
815                            .on_sized({
816                                let item_size = self.item_size.clone();
817                                let length = self.length;
818                                let on_sized = self.on_sized.clone();
819                                move |e: Event<SizedEventData>| {
820                                    size.set_if_modified(e.clone());
821                                    let content_size = match direction {
822                                        Direction::Vertical => Size2D::new(
823                                            e.inner_sizes.width,
824                                            item_size.total_size(
825                                                e.area.height(),
826                                                scrolled_y as f32,
827                                                length,
828                                            ),
829                                        ),
830                                        Direction::Horizontal => Size2D::new(
831                                            item_size.total_size(
832                                                e.area.width(),
833                                                scrolled_x as f32,
834                                                length,
835                                            ),
836                                            e.inner_sizes.height,
837                                        ),
838                                    };
839
840                                    scroll_controller.apply_layout(content_size, e.area.size);
841                                    if let Some(on_sized) = &on_sized {
842                                        on_sized.call(e);
843                                    }
844                                }
845                            })
846                            .children(children),
847                    )
848                    .maybe_child(vertical_scrollbar_is_visible.then_some({
849                        rect().child(self.scrollbar.call(ScrollBarContext {
850                            axis: Axis::Y,
851                            scroll_position: rendered_position,
852                            viewport_size: size.read().area.size,
853                            content_size: Size2D::new(inner_width, inner_height),
854                            scroll_controller,
855                            timeout,
856                            auto_hide_scrollbars: self.auto_hide_scrollbars,
857                            clicking_scrollbar,
858                            thumb_events: ScrollBarThumbEvents::new(Axis::Y, clicking_scrollbar),
859                            thumb_offset: scrollbar_y,
860                            track_size: Size::px(size.read().area.height()),
861                            thumb_length: scrollbar_height,
862                        }))
863                    })),
864            )
865            .maybe_child(horizontal_scrollbar_is_visible.then_some({
866                rect().child(self.scrollbar.call(ScrollBarContext {
867                    axis: Axis::X,
868                    scroll_position: rendered_position,
869                    viewport_size: size.read().area.size,
870                    content_size: Size2D::new(inner_width, inner_height),
871                    scroll_controller,
872                    timeout,
873                    auto_hide_scrollbars: self.auto_hide_scrollbars,
874                    clicking_scrollbar,
875                    thumb_events: ScrollBarThumbEvents::new(Axis::X, clicking_scrollbar),
876                    thumb_offset: scrollbar_x,
877                    track_size: Size::px(size.read().area.width()),
878                    thumb_length: scrollbar_width,
879                }))
880            }))
881    }
882
883    fn render_key(&self) -> DiffKey {
884        self.key.clone().or(self.default_key())
885    }
886}