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