Skip to main content

freya_components/scrollviews/
scrollview.rs

1use std::time::Duration;
2
3use freya_core::prelude::*;
4use freya_sdk::timeout::use_timeout;
5use torin::{
6    geometry::CursorPoint,
7    node::Node,
8    prelude::{
9        Direction,
10        Length,
11    },
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/// Scrollable area with bidirectional support and scrollbars.
34///
35/// # Example
36///
37/// ```rust
38/// # use freya::prelude::*;
39/// fn app() -> impl IntoElement {
40///     ScrollView::new()
41///         .child("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum laoreet tristique diam, ut gravida enim. Phasellus viverra vitae risus sit amet iaculis. Morbi porttitor quis nisl eu vulputate. Etiam vitae ligula a purus suscipit iaculis non ac risus. Suspendisse potenti. Aenean orci massa, ornare ut elit id, tristique commodo dui. Vestibulum laoreet tristique diam, ut gravida enim. Phasellus viverra vitae risus sit amet iaculis. Vestibulum laoreet tristique diam, ut gravida enim. Phasellus viverra vitae risus sit amet iaculis. Vestibulum laoreet tristique diam, ut gravida enim. Phasellus viverra vitae risus sit amet iaculis.")
42/// }
43///
44/// # use freya_testing::prelude::*;
45/// # launch_doc(|| {
46/// #   rect().center().expanded().child(app())
47/// # },
48/// # "./images/gallery_scrollview.png")
49/// #
50/// # .with_hook(|t| {
51/// #   t.move_cursor((125., 115.));
52/// #   t.sync_and_update();
53/// # });
54/// ```
55///
56/// # Preview
57/// ![ScrollView Preview][scrollview]
58#[cfg_attr(feature = "docs",
59    doc = embed_doc_image::embed_image!("scrollview", "images/gallery_scrollview.png")
60)]
61#[derive(Clone, PartialEq)]
62pub struct ScrollView {
63    children: Vec<Element>,
64    layout: LayoutData,
65    show_scrollbar: bool,
66    scroll_with_arrows: bool,
67    scroll_controller: Option<ScrollController>,
68    invert_scroll_wheel: bool,
69    drag_scrolling: bool,
70    key: DiffKey,
71}
72
73impl ChildrenExt for ScrollView {
74    fn get_children(&mut self) -> &mut Vec<Element> {
75        &mut self.children
76    }
77}
78
79impl KeyExt for ScrollView {
80    fn write_key(&mut self) -> &mut DiffKey {
81        &mut self.key
82    }
83}
84
85impl Default for ScrollView {
86    fn default() -> Self {
87        Self {
88            children: Vec::default(),
89            layout: Node {
90                width: Size::fill(),
91                height: Size::fill(),
92                ..Default::default()
93            }
94            .into(),
95            show_scrollbar: true,
96            scroll_with_arrows: true,
97            scroll_controller: None,
98            invert_scroll_wheel: false,
99            drag_scrolling: true,
100            key: DiffKey::None,
101        }
102    }
103}
104
105impl ScrollView {
106    pub fn new() -> Self {
107        Self::default()
108    }
109
110    pub fn new_controlled(scroll_controller: ScrollController) -> Self {
111        Self {
112            scroll_controller: Some(scroll_controller),
113            ..Default::default()
114        }
115    }
116
117    pub fn show_scrollbar(mut self, show_scrollbar: bool) -> Self {
118        self.show_scrollbar = show_scrollbar;
119        self
120    }
121
122    pub fn direction(mut self, direction: Direction) -> Self {
123        self.layout.direction = direction;
124        self
125    }
126
127    pub fn spacing(mut self, spacing: impl Into<f32>) -> Self {
128        self.layout.spacing = Length::new(spacing.into());
129        self
130    }
131
132    pub fn scroll_with_arrows(mut self, scroll_with_arrows: impl Into<bool>) -> Self {
133        self.scroll_with_arrows = scroll_with_arrows.into();
134        self
135    }
136
137    pub fn invert_scroll_wheel(mut self, invert_scroll_wheel: impl Into<bool>) -> Self {
138        self.invert_scroll_wheel = invert_scroll_wheel.into();
139        self
140    }
141
142    pub fn drag_scrolling(mut self, drag_scrolling: bool) -> Self {
143        self.drag_scrolling = drag_scrolling;
144        self
145    }
146
147    pub fn max_width(mut self, max_width: impl Into<Size>) -> Self {
148        self.layout.maximum_width = max_width.into();
149        self
150    }
151
152    pub fn max_height(mut self, max_height: impl Into<Size>) -> Self {
153        self.layout.maximum_height = max_height.into();
154        self
155    }
156}
157
158impl LayoutExt for ScrollView {
159    fn get_layout(&mut self) -> &mut LayoutData {
160        &mut self.layout
161    }
162}
163
164impl ContainerSizeExt for ScrollView {}
165impl ContainerPositionExt for ScrollView {}
166
167impl Component for ScrollView {
168    fn render(self: &ScrollView) -> impl IntoElement {
169        let a11y_id = use_a11y();
170        let mut timeout = use_timeout(|| Duration::from_millis(800));
171        let mut pressing_shift = use_state(|| false);
172        let mut clicking_scrollbar = use_state::<Option<(Axis, f64)>>(|| None);
173        let mut size = use_state(SizedEventData::default);
174        let mut scroll_controller = self
175            .scroll_controller
176            .unwrap_or_else(|| use_scroll_controller(ScrollConfig::default));
177        let mut dragging_content = use_state::<Option<CursorPoint>>(|| None);
178        let mut drag_origin = use_state::<Option<CursorPoint>>(|| None);
179        let (scrolled_x, scrolled_y) = scroll_controller.into();
180        let layout = &self.layout.layout;
181        let direction = layout.direction;
182        let drag_scrolling = self.drag_scrolling;
183
184        scroll_controller.use_apply(
185            size.read().inner_sizes.width,
186            size.read().inner_sizes.height,
187        );
188
189        let corrected_scrolled_x = get_corrected_scroll_position(
190            size.read().inner_sizes.width,
191            size.read().area.width(),
192            scrolled_x as f32,
193        );
194
195        let corrected_scrolled_y = get_corrected_scroll_position(
196            size.read().inner_sizes.height,
197            size.read().area.height(),
198            scrolled_y as f32,
199        );
200        let horizontal_scrollbar_is_visible = !timeout.elapsed()
201            && is_scrollbar_visible(
202                self.show_scrollbar,
203                size.read().inner_sizes.width,
204                size.read().area.width(),
205            );
206        let vertical_scrollbar_is_visible = !timeout.elapsed()
207            && is_scrollbar_visible(
208                self.show_scrollbar,
209                size.read().inner_sizes.height,
210                size.read().area.height(),
211            );
212
213        let (scrollbar_x, scrollbar_width) = get_scrollbar_pos_and_size(
214            size.read().inner_sizes.width,
215            size.read().area.width(),
216            corrected_scrolled_x,
217        );
218        let (scrollbar_y, scrollbar_height) = get_scrollbar_pos_and_size(
219            size.read().inner_sizes.height,
220            size.read().area.height(),
221            corrected_scrolled_y,
222        );
223
224        let (container_width, content_width) = get_container_sizes(layout.width.clone());
225        let (container_height, content_height) = get_container_sizes(layout.height.clone());
226
227        let scroll_with_arrows = self.scroll_with_arrows;
228        let invert_scroll_wheel = self.invert_scroll_wheel;
229
230        let on_capture_global_pointer_press = move |e: Event<PointerEventData>| {
231            if clicking_scrollbar.read().is_some() {
232                e.prevent_default();
233                clicking_scrollbar.set(None);
234            }
235
236            if drag_scrolling && (dragging_content().is_some() || drag_origin().is_some()) {
237                dragging_content.set(None);
238                drag_origin.set(None);
239            }
240        };
241
242        let on_wheel = move |e: Event<WheelEventData>| {
243            // Only invert direction on deviced-sourced wheel events
244            let invert_direction = e.source == WheelSource::Device
245                && (*pressing_shift.read() || invert_scroll_wheel)
246                && (!*pressing_shift.read() || !invert_scroll_wheel);
247
248            let (x_movement, y_movement) = if invert_direction {
249                (e.delta_y as f32, e.delta_x as f32)
250            } else {
251                (e.delta_x as f32, e.delta_y as f32)
252            };
253
254            // Vertical scroll
255            let scroll_position_y = get_scroll_position_from_wheel(
256                y_movement,
257                size.read().inner_sizes.height,
258                size.read().area.height(),
259                corrected_scrolled_y,
260            );
261            scroll_controller.scroll_to_y(scroll_position_y).then(|| {
262                e.stop_propagation();
263            });
264
265            // Horizontal scroll
266            let scroll_position_x = get_scroll_position_from_wheel(
267                x_movement,
268                size.read().inner_sizes.width,
269                size.read().area.width(),
270                corrected_scrolled_x,
271            );
272            scroll_controller.scroll_to_x(scroll_position_x).then(|| {
273                e.stop_propagation();
274            });
275            timeout.reset();
276        };
277
278        let on_mouse_move = move |_| {
279            timeout.reset();
280        };
281
282        let on_capture_global_pointer_move = move |e: Event<PointerEventData>| {
283            if drag_scrolling {
284                if let Some(prev) = dragging_content() {
285                    let coords = e.global_location();
286                    let delta = prev - coords;
287
288                    scroll_controller.scroll_to_y((corrected_scrolled_y - delta.y as f32) as i32);
289                    scroll_controller.scroll_to_x((corrected_scrolled_x - delta.x as f32) as i32);
290
291                    dragging_content.set(Some(coords));
292                    e.prevent_default();
293                    timeout.reset();
294                    a11y_id.request_focus();
295                    return;
296                } else if let Some(origin) = drag_origin() {
297                    let coords = e.global_location();
298                    let distance = (origin - coords).abs();
299
300                    // Small threshold so taps can reach children (e.g. hover on buttons)
301                    // without being immediately consumed by drag scrolling.
302                    const DRAG_THRESHOLD: f64 = 2.0;
303
304                    if distance.x > DRAG_THRESHOLD || distance.y > DRAG_THRESHOLD {
305                        let delta = origin - coords;
306
307                        scroll_controller
308                            .scroll_to_y((corrected_scrolled_y - delta.y as f32) as i32);
309                        scroll_controller
310                            .scroll_to_x((corrected_scrolled_x - delta.x as f32) as i32);
311
312                        dragging_content.set(Some(coords));
313                        e.prevent_default();
314                        timeout.reset();
315                        a11y_id.request_focus();
316                    }
317                    return;
318                }
319            }
320
321            let clicking_scrollbar = clicking_scrollbar.peek();
322
323            if let Some((Axis::Y, y)) = *clicking_scrollbar {
324                let coordinates = e.element_location();
325                let cursor_y = coordinates.y - y - size.read().area.min_y() as f64;
326
327                let scroll_position = get_scroll_position_from_cursor(
328                    cursor_y as f32,
329                    size.read().inner_sizes.height,
330                    size.read().area.height(),
331                );
332
333                scroll_controller.scroll_to_y(scroll_position);
334            } else if let Some((Axis::X, x)) = *clicking_scrollbar {
335                let coordinates = e.element_location();
336                let cursor_x = coordinates.x - x - size.read().area.min_x() as f64;
337
338                let scroll_position = get_scroll_position_from_cursor(
339                    cursor_x as f32,
340                    size.read().inner_sizes.width,
341                    size.read().area.width(),
342                );
343
344                scroll_controller.scroll_to_x(scroll_position);
345            }
346
347            if clicking_scrollbar.is_some() {
348                e.prevent_default();
349                timeout.reset();
350                a11y_id.request_focus();
351            }
352        };
353
354        let on_key_down = move |e: Event<KeyboardEventData>| {
355            if !scroll_with_arrows
356                && (e.key == Key::Named(NamedKey::ArrowUp)
357                    || e.key == Key::Named(NamedKey::ArrowRight)
358                    || e.key == Key::Named(NamedKey::ArrowDown)
359                    || e.key == Key::Named(NamedKey::ArrowLeft))
360            {
361                return;
362            }
363            let x = corrected_scrolled_x;
364            let y = corrected_scrolled_y;
365            let inner_height = size.read().inner_sizes.height;
366            let inner_width = size.read().inner_sizes.width;
367            let viewport_height = size.read().area.height();
368            let viewport_width = size.read().area.width();
369            if let Some((x, y)) = handle_key_event(
370                &e.key,
371                (x, y),
372                inner_height,
373                inner_width,
374                viewport_height,
375                viewport_width,
376                direction,
377            ) {
378                scroll_controller.scroll_to_x(x as i32);
379                scroll_controller.scroll_to_y(y as i32);
380                e.stop_propagation();
381                timeout.reset();
382            }
383        };
384
385        let on_global_key_down = move |e: Event<KeyboardEventData>| {
386            let data = e;
387            if data.key == Key::Named(NamedKey::Shift) {
388                pressing_shift.set(true);
389            }
390        };
391
392        let on_global_key_up = move |e: Event<KeyboardEventData>| {
393            let data = e;
394            if data.key == Key::Named(NamedKey::Shift) {
395                pressing_shift.set(false);
396            }
397        };
398
399        let on_pointer_down = move |e: Event<PointerEventData>| {
400            if drag_scrolling && matches!(e.data(), PointerEventData::Touch(_)) {
401                drag_origin.set(Some(e.global_location()));
402            }
403        };
404
405        rect()
406            .width(layout.width.clone())
407            .height(layout.height.clone())
408            .max_width(layout.maximum_width.clone())
409            .max_height(layout.maximum_height.clone())
410            .a11y_id(a11y_id)
411            .a11y_focusable(false)
412            .a11y_role(AccessibilityRole::ScrollView)
413            .a11y_builder(move |node| {
414                node.set_scroll_x(corrected_scrolled_x as f64);
415                node.set_scroll_y(corrected_scrolled_y as f64)
416            })
417            .scrollable(true)
418            .on_wheel(on_wheel)
419            .on_capture_global_pointer_press(on_capture_global_pointer_press)
420            .on_mouse_move(on_mouse_move)
421            .on_capture_global_pointer_move(on_capture_global_pointer_move)
422            .on_key_down(on_key_down)
423            .on_global_key_up(on_global_key_up)
424            .on_global_key_down(on_global_key_down)
425            .on_pointer_down(on_pointer_down)
426            .child(
427                rect()
428                    .width(container_width)
429                    .height(container_height)
430                    .horizontal()
431                    .child(
432                        rect()
433                            .direction(direction)
434                            .width(content_width)
435                            .height(content_height)
436                            .max_width(layout.maximum_width.clone())
437                            .max_height(layout.maximum_height.clone())
438                            .offset_x(corrected_scrolled_x)
439                            .offset_y(corrected_scrolled_y)
440                            .spacing(layout.spacing.get())
441                            .overflow(Overflow::Clip)
442                            .on_sized(move |e: Event<SizedEventData>| {
443                                size.set_if_modified(e.clone())
444                            })
445                            .children(self.children.clone()),
446                    )
447                    .maybe_child(vertical_scrollbar_is_visible.then_some({
448                        rect().child(ScrollBar {
449                            theme: None,
450                            clicking_scrollbar,
451                            axis: Axis::Y,
452                            offset: scrollbar_y,
453                            size: Size::px(size.read().area.height()),
454                            thumb: ScrollThumb {
455                                theme: None,
456                                clicking_scrollbar,
457                                axis: Axis::Y,
458                                size: scrollbar_height,
459                            },
460                        })
461                    })),
462            )
463            .maybe_child(horizontal_scrollbar_is_visible.then_some({
464                rect().child(ScrollBar {
465                    theme: None,
466                    clicking_scrollbar,
467                    axis: Axis::X,
468                    offset: scrollbar_x,
469                    size: Size::px(size.read().area.width()),
470                    thumb: ScrollThumb {
471                        theme: None,
472                        clicking_scrollbar,
473                        axis: Axis::X,
474                        size: scrollbar_width,
475                    },
476                })
477            }))
478    }
479
480    fn render_key(&self) -> DiffKey {
481        self.key.clone().or(self.default_key())
482    }
483}