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