freya_components/scrollviews/
scrollview.rs

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