freya_components/scrollviews/
scrollview.rs1use std::time::Duration;
2
3use freya_core::prelude::*;
4use freya_sdk::timeout::use_timeout;
5use torin::{
6 geometry::{
7 CursorPoint,
8 Point2D,
9 },
10 node::Node,
11 prelude::{
12 Direction,
13 Length,
14 },
15 size::Size,
16};
17
18use crate::scrollviews::{
19 ScrollBar,
20 ScrollBarContext,
21 ScrollBarThumbEvents,
22 ScrollConfig,
23 ScrollController,
24 shared::{
25 Axis,
26 get_container_sizes,
27 get_corrected_scroll_position,
28 get_scroll_position_from_cursor,
29 get_scroll_position_from_wheel,
30 get_scrollbar_pos_and_size,
31 handle_key_event,
32 is_scrollbar_visible,
33 },
34 use_scroll_controller,
35};
36
37#[derive(Clone, PartialEq)]
58pub struct ScrollView {
59 children: Vec<Element>,
60 layout: LayoutData,
61 show_scrollbar: bool,
62 auto_hide_scrollbars: bool,
63 scroll_with_arrows: bool,
64 scroll_controller: Option<ScrollController>,
65 invert_scroll_wheel: bool,
66 drag_scrolling: bool,
67 on_sized: Option<EventHandler<Event<SizedEventData>>>,
68 scrollbar: Callback<ScrollBarContext, Element>,
69 key: DiffKey,
70}
71
72impl ChildrenExt for ScrollView {
73 fn children_mut(&mut self) -> &mut Vec<Element> {
74 &mut self.children
75 }
76}
77
78impl KeyExt for ScrollView {
79 fn key_mut(&mut self) -> &mut DiffKey {
80 &mut self.key
81 }
82}
83
84impl Default for ScrollView {
85 fn default() -> Self {
86 Self {
87 children: Vec::default(),
88 layout: Node {
89 width: Size::fill(),
90 height: Size::fill(),
91 ..Default::default()
92 }
93 .into(),
94 show_scrollbar: true,
95 auto_hide_scrollbars: true,
96 scroll_with_arrows: true,
97 scroll_controller: None,
98 invert_scroll_wheel: false,
99 drag_scrolling: true,
100 on_sized: None,
101 scrollbar: ScrollBar::default_renderer(),
102 key: DiffKey::None,
103 }
104 }
105}
106
107impl ScrollView {
108 pub fn new() -> Self {
110 Self::default()
111 }
112
113 pub fn new_controlled(scroll_controller: ScrollController) -> Self {
115 Self {
116 scroll_controller: Some(scroll_controller),
117 ..Default::default()
118 }
119 }
120
121 pub fn show_scrollbar(mut self, show_scrollbar: bool) -> Self {
123 self.show_scrollbar = show_scrollbar;
124 self
125 }
126
127 pub fn auto_hide_scrollbars(mut self, auto_hide_scrollbars: bool) -> Self {
130 self.auto_hide_scrollbars = auto_hide_scrollbars;
131 self
132 }
133
134 pub fn direction(mut self, direction: Direction) -> Self {
136 self.layout.direction = direction;
137 self
138 }
139
140 pub fn spacing(mut self, spacing: f32) -> Self {
142 self.layout.spacing = Length::new(spacing);
143 self
144 }
145
146 pub fn scroll_with_arrows(mut self, scroll_with_arrows: impl Into<bool>) -> Self {
148 self.scroll_with_arrows = scroll_with_arrows.into();
149 self
150 }
151
152 pub fn invert_scroll_wheel(mut self, invert_scroll_wheel: impl Into<bool>) -> Self {
154 self.invert_scroll_wheel = invert_scroll_wheel.into();
155 self
156 }
157
158 pub fn drag_scrolling(mut self, drag_scrolling: bool) -> Self {
160 self.drag_scrolling = drag_scrolling;
161 self
162 }
163
164 pub fn scrollbar(mut self, scrollbar: impl Into<Callback<ScrollBarContext, Element>>) -> Self {
166 self.scrollbar = scrollbar.into();
167 self
168 }
169
170 pub fn on_sized(mut self, on_sized: impl Into<EventHandler<Event<SizedEventData>>>) -> Self {
172 self.on_sized = Some(on_sized.into());
173 self
174 }
175
176 pub fn max_width(mut self, max_width: impl Into<Size>) -> Self {
178 self.layout.maximum_width = max_width.into();
179 self
180 }
181
182 pub fn max_height(mut self, max_height: impl Into<Size>) -> Self {
184 self.layout.maximum_height = max_height.into();
185 self
186 }
187}
188
189impl LayoutExt for ScrollView {
190 fn layout_mut(&mut self) -> &mut LayoutData {
191 &mut self.layout
192 }
193}
194
195impl ContainerSizeExt for ScrollView {}
196impl ContainerPositionExt for ScrollView {}
197
198impl Component for ScrollView {
199 fn render(self: &ScrollView) -> impl IntoElement {
200 let a11y_id = use_a11y();
201 let mut timeout = use_timeout(|| Duration::from_millis(800));
202 let mut pressing_shift = use_state(|| false);
203 let mut clicking_scrollbar = use_state::<Option<(Axis, f64)>>(|| None);
204 let mut size = use_state(SizedEventData::default);
205 let mut scroll_controller = self
206 .scroll_controller
207 .unwrap_or_else(|| use_scroll_controller(ScrollConfig::default));
208 let mut dragging_content = use_state::<Option<CursorPoint>>(|| None);
209 let mut drag_origin = use_state::<Option<CursorPoint>>(|| None);
210 let (scrolled_x, scrolled_y) = scroll_controller.into();
211 let layout = &self.layout.layout;
212 let direction = layout.direction;
213 let drag_scrolling = self.drag_scrolling;
214
215 let corrected_scrolled_x = get_corrected_scroll_position(
216 size.read().inner_sizes.width,
217 size.read().area.width(),
218 scrolled_x as f32,
219 );
220
221 let corrected_scrolled_y = get_corrected_scroll_position(
222 size.read().inner_sizes.height,
223 size.read().area.height(),
224 scrolled_y as f32,
225 );
226
227 let smooth_position = scroll_controller
228 .animated_position(Point2D::new(corrected_scrolled_x, corrected_scrolled_y));
229 let rendered_position = Point2D::new(
230 get_corrected_scroll_position(
231 size.read().inner_sizes.width,
232 size.read().area.width(),
233 smooth_position.x,
234 ),
235 get_corrected_scroll_position(
236 size.read().inner_sizes.height,
237 size.read().area.height(),
238 smooth_position.y,
239 ),
240 );
241
242 let horizontal_scrollbar_is_visible = is_scrollbar_visible(
243 self.show_scrollbar,
244 size.read().inner_sizes.width,
245 size.read().area.width(),
246 );
247 let vertical_scrollbar_is_visible = is_scrollbar_visible(
248 self.show_scrollbar,
249 size.read().inner_sizes.height,
250 size.read().area.height(),
251 );
252
253 let (scrollbar_x, scrollbar_width) = get_scrollbar_pos_and_size(
254 size.read().inner_sizes.width,
255 size.read().area.width(),
256 rendered_position.x,
257 );
258 let (scrollbar_y, scrollbar_height) = get_scrollbar_pos_and_size(
259 size.read().inner_sizes.height,
260 size.read().area.height(),
261 rendered_position.y,
262 );
263
264 let (container_width, content_width) = get_container_sizes(layout.width.clone());
265 let (container_height, content_height) = get_container_sizes(layout.height.clone());
266
267 let scroll_with_arrows = self.scroll_with_arrows;
268 let invert_scroll_wheel = self.invert_scroll_wheel;
269
270 let on_capture_global_pointer_up = move |e: Event<PointerEventData>| {
271 if clicking_scrollbar.read().is_some() {
272 e.prevent_default();
273 clicking_scrollbar.set(None);
274 }
275
276 if drag_scrolling && (dragging_content().is_some() || drag_origin().is_some()) {
277 if dragging_content().is_some() {
278 e.prevent_default();
279 let content = size.read().inner_sizes;
280 let viewport = size.read().area.size;
281 scroll_controller.release_drag(rendered_position, content, viewport);
282 }
283 dragging_content.set(None);
284 drag_origin.set(None);
285 }
286 };
287
288 let on_wheel = move |e: Event<WheelEventData>| {
289 let invert_direction = e.source.is_device()
291 && (*pressing_shift.read() || invert_scroll_wheel)
292 && (!*pressing_shift.read() || !invert_scroll_wheel);
293
294 let (x_movement, y_movement) = if invert_direction {
295 (e.delta_y as f32, e.delta_x as f32)
296 } else {
297 (e.delta_x as f32, e.delta_y as f32)
298 };
299
300 let scroll_gesture =
302 TargetPlatform::get() == TargetPlatform::Linux && e.source == WheelSource::Pixel;
303 if scroll_gesture
304 && scroll_controller.scroll_gesture_phase(e.phase, (x_movement, y_movement).into())
305 {
306 return;
307 }
308
309 let animate = e.source == WheelSource::Line || scroll_gesture;
310 if animate {
311 scroll_controller.animate_from(rendered_position);
312 } else {
313 scroll_controller.stop();
314 }
315 let (base_x, base_y) = if animate {
316 let position = scroll_controller.position();
317 (position.x, position.y)
318 } else {
319 (rendered_position.x, rendered_position.y)
320 };
321
322 let scroll_position_y = get_scroll_position_from_wheel(
324 y_movement,
325 size.read().inner_sizes.height,
326 size.read().area.height(),
327 base_y,
328 );
329 scroll_controller.scroll_to_y(scroll_position_y).then(|| {
330 e.stop_propagation();
331 });
332
333 let scroll_position_x = get_scroll_position_from_wheel(
335 x_movement,
336 size.read().inner_sizes.width,
337 size.read().area.width(),
338 base_x,
339 );
340 scroll_controller.scroll_to_x(scroll_position_x).then(|| {
341 e.stop_propagation();
342 });
343 timeout.reset();
344 };
345
346 let on_mouse_move = move |_| {
347 timeout.reset();
348 };
349
350 let on_capture_global_pointer_move = move |e: Event<PointerEventData>| {
351 if drag_scrolling {
352 if let Some(prev) = dragging_content() {
353 let coords = e.global_location();
354 let delta = prev - coords;
355
356 scroll_controller.drag(delta.to_f32());
357 scroll_controller.scroll_to_y((rendered_position.y - delta.y as f32) as i32);
358 scroll_controller.scroll_to_x((rendered_position.x - delta.x as f32) as i32);
359
360 dragging_content.set(Some(coords));
361 e.prevent_default();
362 timeout.reset();
363 a11y_id.request_focus();
364 return;
365 } else if let Some(origin) = drag_origin() {
366 let coords = e.global_location();
367 let distance = (origin - coords).abs();
368
369 const DRAG_THRESHOLD: f64 = 2.0;
372
373 if distance.x > DRAG_THRESHOLD || distance.y > DRAG_THRESHOLD {
374 let delta = origin - coords;
375
376 scroll_controller.drag(delta.to_f32());
377 scroll_controller
378 .scroll_to_y((rendered_position.y - delta.y as f32) as i32);
379 scroll_controller
380 .scroll_to_x((rendered_position.x - delta.x as f32) as i32);
381
382 dragging_content.set(Some(coords));
383 e.prevent_default();
384 timeout.reset();
385 a11y_id.request_focus();
386 }
387 return;
388 }
389 }
390
391 let clicking_scrollbar = clicking_scrollbar.peek();
392
393 if clicking_scrollbar.is_some() {
394 scroll_controller.stop();
395 }
396
397 if let Some((Axis::Y, y)) = *clicking_scrollbar {
398 let coordinates = e.element_location();
399 let cursor_y = coordinates.y - y - size.read().area.min_y() as f64;
400
401 let scroll_position = get_scroll_position_from_cursor(
402 cursor_y as f32,
403 size.read().inner_sizes.height,
404 size.read().area.height(),
405 );
406
407 scroll_controller.scroll_to_y(scroll_position);
408 } else if let Some((Axis::X, x)) = *clicking_scrollbar {
409 let coordinates = e.element_location();
410 let cursor_x = coordinates.x - x - size.read().area.min_x() as f64;
411
412 let scroll_position = get_scroll_position_from_cursor(
413 cursor_x as f32,
414 size.read().inner_sizes.width,
415 size.read().area.width(),
416 );
417
418 scroll_controller.scroll_to_x(scroll_position);
419 }
420
421 if clicking_scrollbar.is_some() {
422 e.prevent_default();
423 timeout.reset();
424 a11y_id.request_focus();
425 }
426 };
427
428 let on_key_down = move |e: Event<KeyboardEventData>| {
429 if !scroll_with_arrows
430 && (e.key == Key::Named(NamedKey::ArrowUp)
431 || e.key == Key::Named(NamedKey::ArrowRight)
432 || e.key == Key::Named(NamedKey::ArrowDown)
433 || e.key == Key::Named(NamedKey::ArrowLeft))
434 {
435 return;
436 }
437 let x = corrected_scrolled_x;
438 let y = corrected_scrolled_y;
439 let inner_height = size.read().inner_sizes.height;
440 let inner_width = size.read().inner_sizes.width;
441 let viewport_height = size.read().area.height();
442 let viewport_width = size.read().area.width();
443 if let Some((x, y)) = handle_key_event(
444 &e.key,
445 (x, y),
446 inner_height,
447 inner_width,
448 viewport_height,
449 viewport_width,
450 direction,
451 ) {
452 scroll_controller.animate_from(rendered_position);
453 scroll_controller.scroll_to_x(x as i32);
454 scroll_controller.scroll_to_y(y as i32);
455 e.stop_propagation();
456 timeout.reset();
457 }
458 };
459
460 let on_global_key_down = move |e: Event<KeyboardEventData>| {
461 let data = e;
462 if data.key == Key::Named(NamedKey::Shift) {
463 pressing_shift.set(true);
464 }
465 };
466
467 let on_global_key_up = move |e: Event<KeyboardEventData>| {
468 let data = e;
469 if data.key == Key::Named(NamedKey::Shift) {
470 pressing_shift.set(false);
471 }
472 };
473
474 let on_pointer_down = move |e: Event<PointerEventData>| {
475 if drag_scrolling && matches!(e.data(), PointerEventData::Touch(_)) {
476 scroll_controller.begin_drag();
477 drag_origin.set(Some(e.global_location()));
478 }
479 };
480
481 rect()
482 .width(layout.width.clone())
483 .height(layout.height.clone())
484 .max_width(layout.maximum_width.clone())
485 .max_height(layout.maximum_height.clone())
486 .a11y_id(a11y_id)
487 .a11y_role(AccessibilityRole::ScrollView)
488 .a11y_builder(move |node| {
489 node.set_scroll_x(rendered_position.x as f64);
490 node.set_scroll_y(rendered_position.y as f64)
491 })
492 .scrollable(true)
493 .map(self.on_sized.clone(), |el, on_sized| el.on_sized(on_sized))
494 .on_wheel(on_wheel)
495 .on_capture_global_pointer_up(on_capture_global_pointer_up)
496 .on_mouse_move(on_mouse_move)
497 .on_capture_global_pointer_move(on_capture_global_pointer_move)
498 .on_key_down(on_key_down)
499 .on_global_key_up(on_global_key_up)
500 .on_global_key_down(on_global_key_down)
501 .on_pointer_down(on_pointer_down)
502 .child(
503 rect()
504 .width(container_width)
505 .height(container_height)
506 .horizontal()
507 .child(
508 rect()
509 .direction(direction)
510 .width(content_width)
511 .height(content_height)
512 .max_width(layout.maximum_width.clone())
513 .max_height(layout.maximum_height.clone())
514 .offset_x(rendered_position.x)
515 .offset_y(rendered_position.y)
516 .spacing(layout.spacing.get())
517 .overflow(Overflow::Clip)
518 .on_sized(move |e: Event<SizedEventData>| {
519 size.set_if_modified(e.clone());
520 scroll_controller.apply_layout(e.inner_sizes, e.area.size);
521 })
522 .children(self.children.clone()),
523 )
524 .maybe_child(vertical_scrollbar_is_visible.then_some({
525 rect().child(self.scrollbar.call(ScrollBarContext {
526 axis: Axis::Y,
527 scroll_position: rendered_position,
528 viewport_size: size.read().area.size,
529 content_size: size.read().inner_sizes,
530 scroll_controller,
531 timeout,
532 auto_hide_scrollbars: self.auto_hide_scrollbars,
533 clicking_scrollbar,
534 thumb_events: ScrollBarThumbEvents::new(Axis::Y, clicking_scrollbar),
535 thumb_offset: scrollbar_y,
536 track_size: Size::px(size.read().area.height()),
537 thumb_length: scrollbar_height,
538 }))
539 })),
540 )
541 .maybe_child(horizontal_scrollbar_is_visible.then_some({
542 rect().child(self.scrollbar.call(ScrollBarContext {
543 axis: Axis::X,
544 scroll_position: rendered_position,
545 viewport_size: size.read().area.size,
546 content_size: size.read().inner_sizes,
547 scroll_controller,
548 timeout,
549 auto_hide_scrollbars: self.auto_hide_scrollbars,
550 clicking_scrollbar,
551 thumb_events: ScrollBarThumbEvents::new(Axis::X, clicking_scrollbar),
552 thumb_offset: scrollbar_x,
553 track_size: Size::px(size.read().area.width()),
554 thumb_length: scrollbar_width,
555 }))
556 }))
557 }
558
559 fn render_key(&self) -> DiffKey {
560 self.key.clone().or(self.default_key())
561 }
562}