freya_components/scrollviews/
scrollview.rs1use 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#[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 {}
165impl ContainerPositionExt for ScrollView {}
166
167impl Component for ScrollView {
168 fn render(self: &ScrollView) -> impl IntoElement {
169 let a11y_id = use_a11y();
170 let mut timeout = use_timeout(|| Duration::from_millis(800));
171 let mut pressing_shift = use_state(|| false);
172 let mut clicking_scrollbar = use_state::<Option<(Axis, f64)>>(|| None);
173 let mut size = use_state(SizedEventData::default);
174 let mut scroll_controller = self
175 .scroll_controller
176 .unwrap_or_else(|| use_scroll_controller(ScrollConfig::default));
177 let mut dragging_content = use_state::<Option<CursorPoint>>(|| None);
178 let mut drag_origin = use_state::<Option<CursorPoint>>(|| None);
179 let (scrolled_x, scrolled_y) = scroll_controller.into();
180 let layout = &self.layout.layout;
181 let direction = layout.direction;
182 let drag_scrolling = self.drag_scrolling;
183
184 scroll_controller.use_apply(
185 size.read().inner_sizes.width,
186 size.read().inner_sizes.height,
187 );
188
189 let corrected_scrolled_x = get_corrected_scroll_position(
190 size.read().inner_sizes.width,
191 size.read().area.width(),
192 scrolled_x as f32,
193 );
194
195 let corrected_scrolled_y = get_corrected_scroll_position(
196 size.read().inner_sizes.height,
197 size.read().area.height(),
198 scrolled_y as f32,
199 );
200 let horizontal_scrollbar_is_visible = !timeout.elapsed()
201 && is_scrollbar_visible(
202 self.show_scrollbar,
203 size.read().inner_sizes.width,
204 size.read().area.width(),
205 );
206 let vertical_scrollbar_is_visible = !timeout.elapsed()
207 && is_scrollbar_visible(
208 self.show_scrollbar,
209 size.read().inner_sizes.height,
210 size.read().area.height(),
211 );
212
213 let (scrollbar_x, scrollbar_width) = get_scrollbar_pos_and_size(
214 size.read().inner_sizes.width,
215 size.read().area.width(),
216 corrected_scrolled_x,
217 );
218 let (scrollbar_y, scrollbar_height) = get_scrollbar_pos_and_size(
219 size.read().inner_sizes.height,
220 size.read().area.height(),
221 corrected_scrolled_y,
222 );
223
224 let (container_width, content_width) = get_container_sizes(layout.width.clone());
225 let (container_height, content_height) = get_container_sizes(layout.height.clone());
226
227 let scroll_with_arrows = self.scroll_with_arrows;
228 let invert_scroll_wheel = self.invert_scroll_wheel;
229
230 let on_capture_global_pointer_press = move |e: Event<PointerEventData>| {
231 if clicking_scrollbar.read().is_some() {
232 e.prevent_default();
233 clicking_scrollbar.set(None);
234 }
235
236 if drag_scrolling && (dragging_content().is_some() || drag_origin().is_some()) {
237 dragging_content.set(None);
238 drag_origin.set(None);
239 }
240 };
241
242 let on_wheel = move |e: Event<WheelEventData>| {
243 let invert_direction = e.source == WheelSource::Device
245 && (*pressing_shift.read() || invert_scroll_wheel)
246 && (!*pressing_shift.read() || !invert_scroll_wheel);
247
248 let (x_movement, y_movement) = if invert_direction {
249 (e.delta_y as f32, e.delta_x as f32)
250 } else {
251 (e.delta_x as f32, e.delta_y as f32)
252 };
253
254 let scroll_position_y = get_scroll_position_from_wheel(
256 y_movement,
257 size.read().inner_sizes.height,
258 size.read().area.height(),
259 corrected_scrolled_y,
260 );
261 scroll_controller.scroll_to_y(scroll_position_y).then(|| {
262 e.stop_propagation();
263 });
264
265 let scroll_position_x = get_scroll_position_from_wheel(
267 x_movement,
268 size.read().inner_sizes.width,
269 size.read().area.width(),
270 corrected_scrolled_x,
271 );
272 scroll_controller.scroll_to_x(scroll_position_x).then(|| {
273 e.stop_propagation();
274 });
275 timeout.reset();
276 };
277
278 let on_mouse_move = move |_| {
279 timeout.reset();
280 };
281
282 let on_capture_global_pointer_move = move |e: Event<PointerEventData>| {
283 if drag_scrolling {
284 if let Some(prev) = dragging_content() {
285 let coords = e.global_location();
286 let delta = prev - coords;
287
288 scroll_controller.scroll_to_y((corrected_scrolled_y - delta.y as f32) as i32);
289 scroll_controller.scroll_to_x((corrected_scrolled_x - delta.x as f32) as i32);
290
291 dragging_content.set(Some(coords));
292 e.prevent_default();
293 timeout.reset();
294 a11y_id.request_focus();
295 return;
296 } else if let Some(origin) = drag_origin() {
297 let coords = e.global_location();
298 let distance = (origin - coords).abs();
299
300 const DRAG_THRESHOLD: f64 = 2.0;
303
304 if distance.x > DRAG_THRESHOLD || distance.y > DRAG_THRESHOLD {
305 let delta = origin - coords;
306
307 scroll_controller
308 .scroll_to_y((corrected_scrolled_y - delta.y as f32) as i32);
309 scroll_controller
310 .scroll_to_x((corrected_scrolled_x - delta.x as f32) as i32);
311
312 dragging_content.set(Some(coords));
313 e.prevent_default();
314 timeout.reset();
315 a11y_id.request_focus();
316 }
317 return;
318 }
319 }
320
321 let clicking_scrollbar = clicking_scrollbar.peek();
322
323 if let Some((Axis::Y, y)) = *clicking_scrollbar {
324 let coordinates = e.element_location();
325 let cursor_y = coordinates.y - y - size.read().area.min_y() as f64;
326
327 let scroll_position = get_scroll_position_from_cursor(
328 cursor_y as f32,
329 size.read().inner_sizes.height,
330 size.read().area.height(),
331 );
332
333 scroll_controller.scroll_to_y(scroll_position);
334 } else if let Some((Axis::X, x)) = *clicking_scrollbar {
335 let coordinates = e.element_location();
336 let cursor_x = coordinates.x - x - size.read().area.min_x() as f64;
337
338 let scroll_position = get_scroll_position_from_cursor(
339 cursor_x as f32,
340 size.read().inner_sizes.width,
341 size.read().area.width(),
342 );
343
344 scroll_controller.scroll_to_x(scroll_position);
345 }
346
347 if clicking_scrollbar.is_some() {
348 e.prevent_default();
349 timeout.reset();
350 a11y_id.request_focus();
351 }
352 };
353
354 let on_key_down = move |e: Event<KeyboardEventData>| {
355 if !scroll_with_arrows
356 && (e.key == Key::Named(NamedKey::ArrowUp)
357 || e.key == Key::Named(NamedKey::ArrowRight)
358 || e.key == Key::Named(NamedKey::ArrowDown)
359 || e.key == Key::Named(NamedKey::ArrowLeft))
360 {
361 return;
362 }
363 let x = corrected_scrolled_x;
364 let y = corrected_scrolled_y;
365 let inner_height = size.read().inner_sizes.height;
366 let inner_width = size.read().inner_sizes.width;
367 let viewport_height = size.read().area.height();
368 let viewport_width = size.read().area.width();
369 if let Some((x, y)) = handle_key_event(
370 &e.key,
371 (x, y),
372 inner_height,
373 inner_width,
374 viewport_height,
375 viewport_width,
376 direction,
377 ) {
378 scroll_controller.scroll_to_x(x as i32);
379 scroll_controller.scroll_to_y(y as i32);
380 e.stop_propagation();
381 timeout.reset();
382 }
383 };
384
385 let on_global_key_down = move |e: Event<KeyboardEventData>| {
386 let data = e;
387 if data.key == Key::Named(NamedKey::Shift) {
388 pressing_shift.set(true);
389 }
390 };
391
392 let on_global_key_up = move |e: Event<KeyboardEventData>| {
393 let data = e;
394 if data.key == Key::Named(NamedKey::Shift) {
395 pressing_shift.set(false);
396 }
397 };
398
399 let on_pointer_down = move |e: Event<PointerEventData>| {
400 if drag_scrolling && matches!(e.data(), PointerEventData::Touch(_)) {
401 drag_origin.set(Some(e.global_location()));
402 }
403 };
404
405 rect()
406 .width(layout.width.clone())
407 .height(layout.height.clone())
408 .max_width(layout.maximum_width.clone())
409 .max_height(layout.maximum_height.clone())
410 .a11y_id(a11y_id)
411 .a11y_focusable(false)
412 .a11y_role(AccessibilityRole::ScrollView)
413 .a11y_builder(move |node| {
414 node.set_scroll_x(corrected_scrolled_x as f64);
415 node.set_scroll_y(corrected_scrolled_y as f64)
416 })
417 .scrollable(true)
418 .on_wheel(on_wheel)
419 .on_capture_global_pointer_press(on_capture_global_pointer_press)
420 .on_mouse_move(on_mouse_move)
421 .on_capture_global_pointer_move(on_capture_global_pointer_move)
422 .on_key_down(on_key_down)
423 .on_global_key_up(on_global_key_up)
424 .on_global_key_down(on_global_key_down)
425 .on_pointer_down(on_pointer_down)
426 .child(
427 rect()
428 .width(container_width)
429 .height(container_height)
430 .horizontal()
431 .child(
432 rect()
433 .direction(direction)
434 .width(content_width)
435 .height(content_height)
436 .max_width(layout.maximum_width.clone())
437 .max_height(layout.maximum_height.clone())
438 .offset_x(corrected_scrolled_x)
439 .offset_y(corrected_scrolled_y)
440 .spacing(layout.spacing.get())
441 .overflow(Overflow::Clip)
442 .on_sized(move |e: Event<SizedEventData>| {
443 size.set_if_modified(e.clone())
444 })
445 .children(self.children.clone()),
446 )
447 .maybe_child(vertical_scrollbar_is_visible.then_some({
448 rect().child(ScrollBar {
449 theme: None,
450 clicking_scrollbar,
451 axis: Axis::Y,
452 offset: scrollbar_y,
453 size: Size::px(size.read().area.height()),
454 thumb: ScrollThumb {
455 theme: None,
456 clicking_scrollbar,
457 axis: Axis::Y,
458 size: scrollbar_height,
459 },
460 })
461 })),
462 )
463 .maybe_child(horizontal_scrollbar_is_visible.then_some({
464 rect().child(ScrollBar {
465 theme: None,
466 clicking_scrollbar,
467 axis: Axis::X,
468 offset: scrollbar_x,
469 size: Size::px(size.read().area.width()),
470 thumb: ScrollThumb {
471 theme: None,
472 clicking_scrollbar,
473 axis: Axis::X,
474 size: scrollbar_width,
475 },
476 })
477 }))
478 }
479
480 fn render_key(&self) -> DiffKey {
481 self.key.clone().or(self.default_key())
482 }
483}