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 {}
165
166impl Component for ScrollView {
167    fn render(self: &ScrollView) -> impl IntoElement {
168        let a11y_id = use_a11y();
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                    a11y_id.request_focus();
294                    return;
295                } else if let Some(origin) = drag_origin() {
296                    let coords = e.global_location();
297                    let distance = (origin - coords).abs();
298
299                    // Small threshold so taps can reach children (e.g. hover on buttons)
300                    // without being immediately consumed by drag scrolling.
301                    const DRAG_THRESHOLD: f64 = 2.0;
302
303                    if distance.x > DRAG_THRESHOLD || distance.y > DRAG_THRESHOLD {
304                        let delta = origin - coords;
305
306                        scroll_controller
307                            .scroll_to_y((corrected_scrolled_y - delta.y as f32) as i32);
308                        scroll_controller
309                            .scroll_to_x((corrected_scrolled_x - delta.x as f32) as i32);
310
311                        dragging_content.set(Some(coords));
312                        e.prevent_default();
313                        timeout.reset();
314                        a11y_id.request_focus();
315                    }
316                    return;
317                }
318            }
319
320            let clicking_scrollbar = clicking_scrollbar.peek();
321
322            if let Some((Axis::Y, y)) = *clicking_scrollbar {
323                let coordinates = e.element_location();
324                let cursor_y = coordinates.y - y - size.read().area.min_y() as f64;
325
326                let scroll_position = get_scroll_position_from_cursor(
327                    cursor_y as f32,
328                    size.read().inner_sizes.height,
329                    size.read().area.height(),
330                );
331
332                scroll_controller.scroll_to_y(scroll_position);
333            } else if let Some((Axis::X, x)) = *clicking_scrollbar {
334                let coordinates = e.element_location();
335                let cursor_x = coordinates.x - x - size.read().area.min_x() as f64;
336
337                let scroll_position = get_scroll_position_from_cursor(
338                    cursor_x as f32,
339                    size.read().inner_sizes.width,
340                    size.read().area.width(),
341                );
342
343                scroll_controller.scroll_to_x(scroll_position);
344            }
345
346            if clicking_scrollbar.is_some() {
347                e.prevent_default();
348                timeout.reset();
349                a11y_id.request_focus();
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 && matches!(e.data(), PointerEventData::Touch(_)) {
400                drag_origin.set(Some(e.global_location()));
401            }
402        };
403
404        rect()
405            .width(layout.width.clone())
406            .height(layout.height.clone())
407            .max_width(layout.maximum_width.clone())
408            .max_height(layout.maximum_height.clone())
409            .a11y_id(a11y_id)
410            .a11y_focusable(false)
411            .a11y_role(AccessibilityRole::ScrollView)
412            .a11y_builder(move |node| {
413                node.set_scroll_x(corrected_scrolled_x as f64);
414                node.set_scroll_y(corrected_scrolled_y as f64)
415            })
416            .scrollable(true)
417            .on_wheel(on_wheel)
418            .on_capture_global_pointer_press(on_capture_global_pointer_press)
419            .on_mouse_move(on_mouse_move)
420            .on_capture_global_pointer_move(on_capture_global_pointer_move)
421            .on_key_down(on_key_down)
422            .on_global_key_up(on_global_key_up)
423            .on_global_key_down(on_global_key_down)
424            .on_pointer_down(on_pointer_down)
425            .child(
426                rect()
427                    .width(container_width)
428                    .height(container_height)
429                    .horizontal()
430                    .child(
431                        rect()
432                            .direction(direction)
433                            .width(content_width)
434                            .height(content_height)
435                            .max_width(layout.maximum_width.clone())
436                            .max_height(layout.maximum_height.clone())
437                            .offset_x(corrected_scrolled_x)
438                            .offset_y(corrected_scrolled_y)
439                            .spacing(layout.spacing.get())
440                            .overflow(Overflow::Clip)
441                            .on_sized(move |e: Event<SizedEventData>| {
442                                size.set_if_modified(e.clone())
443                            })
444                            .children(self.children.clone()),
445                    )
446                    .maybe_child(vertical_scrollbar_is_visible.then_some({
447                        rect().child(ScrollBar {
448                            theme: None,
449                            clicking_scrollbar,
450                            axis: Axis::Y,
451                            offset: scrollbar_y,
452                            size: Size::px(size.read().area.height()),
453                            thumb: ScrollThumb {
454                                theme: None,
455                                clicking_scrollbar,
456                                axis: Axis::Y,
457                                size: scrollbar_height,
458                            },
459                        })
460                    })),
461            )
462            .maybe_child(horizontal_scrollbar_is_visible.then_some({
463                rect().child(ScrollBar {
464                    theme: None,
465                    clicking_scrollbar,
466                    axis: Axis::X,
467                    offset: scrollbar_x,
468                    size: Size::px(size.read().area.width()),
469                    thumb: ScrollThumb {
470                        theme: None,
471                        clicking_scrollbar,
472                        axis: Axis::X,
473                        size: scrollbar_width,
474                    },
475                })
476            }))
477    }
478
479    fn render_key(&self) -> DiffKey {
480        self.key.clone().or(self.default_key())
481    }
482}