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