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: cfg!(target_os = "android"),
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 {}
165
166impl Component for ScrollView {
167    fn render(self: &ScrollView) -> impl IntoElement {
168        let focus = use_focus();
169        let mut timeout = use_timeout(|| Duration::from_millis(800));
170        let mut pressing_shift = use_state(|| false);
171        let mut clicking_scrollbar = use_state::<Option<(Axis, f64)>>(|| None);
172        let mut size = use_state(SizedEventData::default);
173        let mut scroll_controller = self
174            .scroll_controller
175            .unwrap_or_else(|| use_scroll_controller(ScrollConfig::default));
176        let mut dragging_content = use_state::<Option<CursorPoint>>(|| None);
177        let mut drag_origin = use_state::<Option<CursorPoint>>(|| None);
178        let (scrolled_x, scrolled_y) = scroll_controller.into();
179        let layout = &self.layout.layout;
180        let direction = layout.direction;
181        let drag_scrolling = self.drag_scrolling;
182
183        scroll_controller.use_apply(
184            size.read().inner_sizes.width,
185            size.read().inner_sizes.height,
186        );
187
188        let corrected_scrolled_x = get_corrected_scroll_position(
189            size.read().inner_sizes.width,
190            size.read().area.width(),
191            scrolled_x as f32,
192        );
193
194        let corrected_scrolled_y = get_corrected_scroll_position(
195            size.read().inner_sizes.height,
196            size.read().area.height(),
197            scrolled_y as f32,
198        );
199        let horizontal_scrollbar_is_visible = !timeout.elapsed()
200            && is_scrollbar_visible(
201                self.show_scrollbar,
202                size.read().inner_sizes.width,
203                size.read().area.width(),
204            );
205        let vertical_scrollbar_is_visible = !timeout.elapsed()
206            && is_scrollbar_visible(
207                self.show_scrollbar,
208                size.read().inner_sizes.height,
209                size.read().area.height(),
210            );
211
212        let (scrollbar_x, scrollbar_width) = get_scrollbar_pos_and_size(
213            size.read().inner_sizes.width,
214            size.read().area.width(),
215            corrected_scrolled_x,
216        );
217        let (scrollbar_y, scrollbar_height) = get_scrollbar_pos_and_size(
218            size.read().inner_sizes.height,
219            size.read().area.height(),
220            corrected_scrolled_y,
221        );
222
223        let (container_width, content_width) = get_container_sizes(layout.width.clone());
224        let (container_height, content_height) = get_container_sizes(layout.height.clone());
225
226        let scroll_with_arrows = self.scroll_with_arrows;
227        let invert_scroll_wheel = self.invert_scroll_wheel;
228
229        let on_capture_global_pointer_press = move |e: Event<PointerEventData>| {
230            if clicking_scrollbar.read().is_some() {
231                e.prevent_default();
232                clicking_scrollbar.set(None);
233            }
234
235            if drag_scrolling && (dragging_content().is_some() || drag_origin().is_some()) {
236                dragging_content.set(None);
237                drag_origin.set(None);
238            }
239        };
240
241        let on_wheel = move |e: Event<WheelEventData>| {
242            // Only invert direction on deviced-sourced wheel events
243            let invert_direction = e.source == WheelSource::Device
244                && (*pressing_shift.read() || invert_scroll_wheel)
245                && (!*pressing_shift.read() || !invert_scroll_wheel);
246
247            let (x_movement, y_movement) = if invert_direction {
248                (e.delta_y as f32, e.delta_x as f32)
249            } else {
250                (e.delta_x as f32, e.delta_y as f32)
251            };
252
253            // Vertical scroll
254            let scroll_position_y = get_scroll_position_from_wheel(
255                y_movement,
256                size.read().inner_sizes.height,
257                size.read().area.height(),
258                corrected_scrolled_y,
259            );
260            scroll_controller.scroll_to_y(scroll_position_y).then(|| {
261                e.stop_propagation();
262            });
263
264            // Horizontal scroll
265            let scroll_position_x = get_scroll_position_from_wheel(
266                x_movement,
267                size.read().inner_sizes.width,
268                size.read().area.width(),
269                corrected_scrolled_x,
270            );
271            scroll_controller.scroll_to_x(scroll_position_x).then(|| {
272                e.stop_propagation();
273            });
274            timeout.reset();
275        };
276
277        let on_mouse_move = move |_| {
278            timeout.reset();
279        };
280
281        let on_capture_global_pointer_move = move |e: Event<PointerEventData>| {
282            if drag_scrolling {
283                if let Some(prev) = dragging_content() {
284                    let coords = e.global_location();
285                    let delta = prev - coords;
286
287                    scroll_controller.scroll_to_y((corrected_scrolled_y - delta.y as f32) as i32);
288                    scroll_controller.scroll_to_x((corrected_scrolled_x - delta.x as f32) as i32);
289
290                    dragging_content.set(Some(coords));
291                    e.prevent_default();
292                    timeout.reset();
293                    return;
294                } else if let Some(origin) = drag_origin() {
295                    let coords = e.global_location();
296                    let distance = (origin - coords).abs();
297
298                    // Small threshold so taps can reach children (e.g. hover on buttons)
299                    // without being immediately consumed by drag scrolling.
300                    const DRAG_THRESHOLD: f64 = 2.0;
301
302                    if distance.x > DRAG_THRESHOLD || distance.y > DRAG_THRESHOLD {
303                        let delta = origin - coords;
304
305                        scroll_controller
306                            .scroll_to_y((corrected_scrolled_y - delta.y as f32) as i32);
307                        scroll_controller
308                            .scroll_to_x((corrected_scrolled_x - delta.x as f32) as i32);
309
310                        dragging_content.set(Some(coords));
311                        e.prevent_default();
312                        timeout.reset();
313                    }
314                    return;
315                }
316            }
317
318            let clicking_scrollbar = clicking_scrollbar.peek();
319
320            if let Some((Axis::Y, y)) = *clicking_scrollbar {
321                let coordinates = e.element_location();
322                let cursor_y = coordinates.y - y - size.read().area.min_y() as f64;
323
324                let scroll_position = get_scroll_position_from_cursor(
325                    cursor_y as f32,
326                    size.read().inner_sizes.height,
327                    size.read().area.height(),
328                );
329
330                scroll_controller.scroll_to_y(scroll_position);
331            } else if let Some((Axis::X, x)) = *clicking_scrollbar {
332                let coordinates = e.element_location();
333                let cursor_x = coordinates.x - x - size.read().area.min_x() as f64;
334
335                let scroll_position = get_scroll_position_from_cursor(
336                    cursor_x as f32,
337                    size.read().inner_sizes.width,
338                    size.read().area.width(),
339                );
340
341                scroll_controller.scroll_to_x(scroll_position);
342            }
343
344            if clicking_scrollbar.is_some() {
345                e.prevent_default();
346                timeout.reset();
347                if !focus.is_focused() {
348                    focus.request_focus();
349                }
350            }
351        };
352
353        let on_key_down = move |e: Event<KeyboardEventData>| {
354            if !scroll_with_arrows
355                && (e.key == Key::Named(NamedKey::ArrowUp)
356                    || e.key == Key::Named(NamedKey::ArrowRight)
357                    || e.key == Key::Named(NamedKey::ArrowDown)
358                    || e.key == Key::Named(NamedKey::ArrowLeft))
359            {
360                return;
361            }
362            let x = corrected_scrolled_x;
363            let y = corrected_scrolled_y;
364            let inner_height = size.read().inner_sizes.height;
365            let inner_width = size.read().inner_sizes.width;
366            let viewport_height = size.read().area.height();
367            let viewport_width = size.read().area.width();
368            if let Some((x, y)) = handle_key_event(
369                &e.key,
370                (x, y),
371                inner_height,
372                inner_width,
373                viewport_height,
374                viewport_width,
375                direction,
376            ) {
377                scroll_controller.scroll_to_x(x as i32);
378                scroll_controller.scroll_to_y(y as i32);
379                e.stop_propagation();
380                timeout.reset();
381            }
382        };
383
384        let on_global_key_down = move |e: Event<KeyboardEventData>| {
385            let data = e;
386            if data.key == Key::Named(NamedKey::Shift) {
387                pressing_shift.set(true);
388            }
389        };
390
391        let on_global_key_up = move |e: Event<KeyboardEventData>| {
392            let data = e;
393            if data.key == Key::Named(NamedKey::Shift) {
394                pressing_shift.set(false);
395            }
396        };
397
398        let on_pointer_down = move |e: Event<PointerEventData>| {
399            if drag_scrolling {
400                drag_origin.set(Some(e.global_location()));
401                focus.request_focus();
402                timeout.reset();
403            }
404        };
405
406        rect()
407            .width(layout.width.clone())
408            .height(layout.height.clone())
409            .max_width(layout.maximum_width.clone())
410            .max_height(layout.maximum_height.clone())
411            .a11y_id(focus.a11y_id())
412            .a11y_focusable(false)
413            .a11y_role(AccessibilityRole::ScrollView)
414            .a11y_builder(move |node| {
415                node.set_scroll_x(corrected_scrolled_x as f64);
416                node.set_scroll_y(corrected_scrolled_y as f64)
417            })
418            .scrollable(true)
419            .on_wheel(on_wheel)
420            .on_capture_global_pointer_press(on_capture_global_pointer_press)
421            .on_mouse_move(on_mouse_move)
422            .on_capture_global_pointer_move(on_capture_global_pointer_move)
423            .on_key_down(on_key_down)
424            .on_global_key_up(on_global_key_up)
425            .on_global_key_down(on_global_key_down)
426            .on_pointer_down(on_pointer_down)
427            .child(
428                rect()
429                    .width(container_width)
430                    .height(container_height)
431                    .horizontal()
432                    .child(
433                        rect()
434                            .direction(direction)
435                            .width(content_width)
436                            .height(content_height)
437                            .max_width(layout.maximum_width.clone())
438                            .max_height(layout.maximum_height.clone())
439                            .offset_x(corrected_scrolled_x)
440                            .offset_y(corrected_scrolled_y)
441                            .spacing(layout.spacing.get())
442                            .overflow(Overflow::Clip)
443                            .on_sized(move |e: Event<SizedEventData>| {
444                                size.set_if_modified(e.clone())
445                            })
446                            .children(self.children.clone()),
447                    )
448                    .maybe_child(vertical_scrollbar_is_visible.then_some({
449                        rect().child(ScrollBar {
450                            theme: None,
451                            clicking_scrollbar,
452                            axis: Axis::Y,
453                            offset: scrollbar_y,
454                            size: Size::px(size.read().area.height()),
455                            thumb: ScrollThumb {
456                                theme: None,
457                                clicking_scrollbar,
458                                axis: Axis::Y,
459                                size: scrollbar_height,
460                            },
461                        })
462                    })),
463            )
464            .maybe_child(horizontal_scrollbar_is_visible.then_some({
465                rect().child(ScrollBar {
466                    theme: None,
467                    clicking_scrollbar,
468                    axis: Axis::X,
469                    offset: scrollbar_x,
470                    size: Size::px(size.read().area.width()),
471                    thumb: ScrollThumb {
472                        theme: None,
473                        clicking_scrollbar,
474                        axis: Axis::X,
475                        size: scrollbar_width,
476                    },
477                })
478            }))
479    }
480
481    fn render_key(&self) -> DiffKey {
482        self.key.clone().or(self.default_key())
483    }
484}