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