1#![allow(clippy::type_complexity)]
2
3use std::ops::Range;
4
5use dioxus::prelude::*;
6use freya_elements::{
7 self as dioxus_elements,
8 events::{
9 keyboard::Key,
10 KeyboardEvent,
11 MouseEvent,
12 WheelEvent,
13 },
14};
15use freya_hooks::{
16 use_applied_theme,
17 use_focus,
18 use_node,
19 ScrollBarThemeWith,
20};
21
22use crate::{
23 get_container_size,
24 get_corrected_scroll_position,
25 get_scroll_position_from_cursor,
26 get_scroll_position_from_wheel,
27 get_scrollbar_pos_and_size,
28 is_scrollbar_visible,
29 manage_key_event,
30 scroll_views::use_scroll_controller,
31 Axis,
32 ScrollBar,
33 ScrollConfig,
34 ScrollController,
35 ScrollThumb,
36 SCROLL_SPEED_MULTIPLIER,
37};
38
39#[derive(Props, Clone)]
41pub struct VirtualScrollViewProps<
42 Builder: 'static + Clone + Fn(usize, &Option<BuilderArgs>) -> Element,
43 BuilderArgs: Clone + 'static + PartialEq = (),
44> {
45 #[props(default = "fill".into())]
47 pub width: String,
48 #[props(default = "fill".into())]
50 pub height: String,
51 #[props(default = "0".to_string())]
53 pub padding: String,
54 pub scrollbar_theme: Option<ScrollBarThemeWith>,
56 pub length: usize,
58 pub item_size: f32,
60 pub builder: Builder,
62 #[props(into)]
64 pub builder_args: Option<BuilderArgs>,
65 #[props(default = "vertical".to_string(), into)]
67 pub direction: String,
68 #[props(default = true, into)]
70 pub show_scrollbar: bool,
71 #[props(default = true, into)]
73 pub scroll_with_arrows: bool,
74 #[props(default = true, into)]
77 pub cache_elements: bool,
78 pub scroll_controller: Option<ScrollController>,
80 #[props(default = false)]
83 pub invert_scroll_wheel: bool,
84}
85
86impl<
87 BuilderArgs: Clone + PartialEq,
88 Builder: Clone + Fn(usize, &Option<BuilderArgs>) -> Element,
89 > PartialEq for VirtualScrollViewProps<Builder, BuilderArgs>
90{
91 fn eq(&self, other: &Self) -> bool {
92 self.width == other.width
93 && self.height == other.height
94 && self.padding == other.padding
95 && self.length == other.length
96 && self.item_size == other.item_size
97 && self.direction == other.direction
98 && self.show_scrollbar == other.show_scrollbar
99 && self.scroll_with_arrows == other.scroll_with_arrows
100 && self.builder_args == other.builder_args
101 && self.scroll_controller == other.scroll_controller
102 && self.invert_scroll_wheel == other.invert_scroll_wheel
103 }
104}
105
106fn get_render_range(
107 viewport_size: f32,
108 scroll_position: f32,
109 item_size: f32,
110 item_length: f32,
111) -> Range<usize> {
112 let render_index_start = (-scroll_position) / item_size;
113 let potentially_visible_length = (viewport_size / item_size) + 1.0;
114 let remaining_length = item_length - render_index_start;
115
116 let render_index_end = if remaining_length <= potentially_visible_length {
117 item_length
118 } else {
119 render_index_start + potentially_visible_length
120 };
121
122 render_index_start as usize..(render_index_end as usize)
123}
124
125#[cfg_attr(feature = "docs",
191 doc = embed_doc_image::embed_image!("virtual_scroll_view", "images/gallery_virtual_scroll_view.png")
192)]
193#[allow(non_snake_case)]
194pub fn VirtualScrollView<
195 Builder: Clone + Fn(usize, &Option<BuilderArgs>) -> Element,
196 BuilderArgs: Clone + PartialEq,
197>(
198 VirtualScrollViewProps {
199 width,
200 height,
201 padding,
202 scrollbar_theme,
203 length,
204 item_size,
205 builder,
206 builder_args,
207 direction,
208 show_scrollbar,
209 scroll_with_arrows,
210 cache_elements,
211 scroll_controller,
212 invert_scroll_wheel,
213 }: VirtualScrollViewProps<Builder, BuilderArgs>,
214) -> Element {
215 let mut clicking_scrollbar = use_signal::<Option<(Axis, f64)>>(|| None);
216 let mut clicking_shift = use_signal(|| false);
217 let mut clicking_alt = use_signal(|| false);
218 let mut scroll_controller =
219 scroll_controller.unwrap_or_else(|| use_scroll_controller(ScrollConfig::default));
220 let (mut scrolled_x, mut scrolled_y) = scroll_controller.into();
221 let (node_ref, size) = use_node();
222 let mut focus = use_focus();
223 let applied_scrollbar_theme = use_applied_theme!(&scrollbar_theme, scroll_bar);
224
225 let direction_is_vertical = direction == "vertical";
226
227 let inner_size = item_size + (item_size * length as f32);
228
229 scroll_controller.use_apply(inner_size, inner_size);
230
231 let vertical_scrollbar_is_visible = direction != "horizontal"
232 && is_scrollbar_visible(show_scrollbar, inner_size, size.area.height());
233 let horizontal_scrollbar_is_visible = direction != "vertical"
234 && is_scrollbar_visible(show_scrollbar, inner_size, size.area.width());
235
236 let (container_width, content_width) =
237 get_container_size(&width, direction_is_vertical, Axis::X);
238 let (container_height, content_height) =
239 get_container_size(&height, direction_is_vertical, Axis::Y);
240
241 let corrected_scrolled_y =
242 get_corrected_scroll_position(inner_size, size.area.height(), *scrolled_y.read() as f32);
243 let corrected_scrolled_x =
244 get_corrected_scroll_position(inner_size, size.area.width(), *scrolled_x.read() as f32);
245
246 let (scrollbar_y, scrollbar_height) =
247 get_scrollbar_pos_and_size(inner_size, size.area.height(), corrected_scrolled_y);
248 let (scrollbar_x, scrollbar_width) =
249 get_scrollbar_pos_and_size(inner_size, size.area.width(), corrected_scrolled_x);
250
251 let onwheel = move |e: WheelEvent| {
253 let speed_multiplier = if *clicking_alt.peek() {
254 SCROLL_SPEED_MULTIPLIER
255 } else {
256 1.0
257 };
258
259 let invert_direction = (clicking_shift() || invert_scroll_wheel)
260 && (!clicking_shift() || !invert_scroll_wheel);
261
262 let (x_movement, y_movement) = if invert_direction {
263 (
264 e.get_delta_y() as f32 * speed_multiplier,
265 e.get_delta_x() as f32 * speed_multiplier,
266 )
267 } else {
268 (
269 e.get_delta_x() as f32 * speed_multiplier,
270 e.get_delta_y() as f32 * speed_multiplier,
271 )
272 };
273
274 let scroll_position_y = get_scroll_position_from_wheel(
275 y_movement,
276 inner_size,
277 size.area.height(),
278 corrected_scrolled_y,
279 );
280
281 if *scrolled_y.peek() != scroll_position_y {
283 e.stop_propagation();
284 *scrolled_y.write() = scroll_position_y;
285 focus.request_focus();
286 }
287
288 let scroll_position_x = get_scroll_position_from_wheel(
289 x_movement,
290 inner_size,
291 size.area.width(),
292 corrected_scrolled_x,
293 );
294
295 if *scrolled_x.peek() != scroll_position_x {
297 e.stop_propagation();
298 *scrolled_x.write() = scroll_position_x;
299 focus.request_focus();
300 }
301 };
302
303 let onmousemove = move |e: MouseEvent| {
305 let clicking_scrollbar = clicking_scrollbar.peek();
306
307 if let Some((Axis::Y, y)) = *clicking_scrollbar {
308 let coordinates = e.get_element_coordinates();
309 let cursor_y = coordinates.y - y - size.area.min_y() as f64;
310
311 let scroll_position =
312 get_scroll_position_from_cursor(cursor_y as f32, inner_size, size.area.height());
313
314 *scrolled_y.write() = scroll_position;
315 } else if let Some((Axis::X, x)) = *clicking_scrollbar {
316 let coordinates = e.get_element_coordinates();
317 let cursor_x = coordinates.x - x - size.area.min_x() as f64;
318
319 let scroll_position =
320 get_scroll_position_from_cursor(cursor_x as f32, inner_size, size.area.width());
321
322 *scrolled_x.write() = scroll_position;
323 }
324
325 if clicking_scrollbar.is_some() {
326 focus.request_focus();
327 }
328 };
329
330 let onglobalkeydown = move |e: KeyboardEvent| {
331 match &e.key {
332 Key::Shift => {
333 clicking_shift.set(true);
334 }
335 Key::Alt => {
336 clicking_alt.set(true);
337 }
338 k => {
339 if !focus.is_focused() {
340 return;
341 }
342
343 if !scroll_with_arrows
344 && (k == &Key::ArrowUp
345 || k == &Key::ArrowRight
346 || k == &Key::ArrowDown
347 || k == &Key::ArrowLeft)
348 {
349 return;
350 }
351
352 let x = corrected_scrolled_x;
353 let y = corrected_scrolled_y;
354 let inner_height = inner_size;
355 let inner_width = inner_size;
356 let viewport_height = size.area.height();
357 let viewport_width = size.area.width();
358
359 let (x, y) = manage_key_event(
360 e,
361 (x, y),
362 inner_height,
363 inner_width,
364 viewport_height,
365 viewport_width,
366 );
367
368 scrolled_x.set(x as i32);
369 scrolled_y.set(y as i32);
370 }
371 };
372 };
373
374 let onglobalkeyup = move |e: KeyboardEvent| {
375 if e.key == Key::Shift {
376 clicking_shift.set(false);
377 } else if e.key == Key::Alt {
378 clicking_alt.set(false);
379 }
380 };
381
382 let onmousedown_y = move |e: MouseEvent| {
384 let coordinates = e.get_element_coordinates();
385 *clicking_scrollbar.write() = Some((Axis::Y, coordinates.y));
386 };
387
388 let onmousedown_x = move |e: MouseEvent| {
390 let coordinates = e.get_element_coordinates();
391 *clicking_scrollbar.write() = Some((Axis::X, coordinates.x));
392 };
393
394 let onclick = move |_: MouseEvent| {
396 if clicking_scrollbar.peek().is_some() {
397 *clicking_scrollbar.write() = None;
398 }
399 };
400
401 let (viewport_size, scroll_position) = if direction == "vertical" {
402 (size.area.height(), corrected_scrolled_y)
403 } else {
404 (size.area.width(), corrected_scrolled_x)
405 };
406
407 let render_range = get_render_range(viewport_size, scroll_position, item_size, length as f32);
409
410 let children = if cache_elements {
411 let children = use_memo(use_reactive(
412 &(render_range, builder_args),
413 move |(render_range, builder_args)| {
414 render_range
415 .clone()
416 .map(|i| (builder)(i, &builder_args))
417 .collect::<Vec<Element>>()
418 },
419 ));
420 rsx!({ children.read().iter() })
421 } else {
422 let children = render_range.map(|i| (builder)(i, &builder_args));
423 rsx!({ children })
424 };
425
426 let is_scrolling_x = clicking_scrollbar
427 .read()
428 .as_ref()
429 .map(|f| f.0 == Axis::X)
430 .unwrap_or_default();
431 let is_scrolling_y = clicking_scrollbar
432 .read()
433 .as_ref()
434 .map(|f| f.0 == Axis::Y)
435 .unwrap_or_default();
436
437 let offset_y_min = (-corrected_scrolled_y / item_size).floor() * item_size;
438 let offset_y = -corrected_scrolled_y - offset_y_min;
439
440 let a11y_id = focus.attribute();
441
442 rsx!(
443 rect {
444 a11y_role: "scroll-view",
445 overflow: "clip",
446 direction: "horizontal",
447 width: "{width}",
448 height: "{height}",
449 onglobalclick: onclick,
450 onglobalmousemove: onmousemove,
451 onglobalkeydown,
452 onglobalkeyup,
453 a11y_id,
454 rect {
455 direction: "vertical",
456 width: "{container_width}",
457 height: "{container_height}",
458 rect {
459 overflow: "clip",
460 padding: "{padding}",
461 height: "{content_height}",
462 width: "{content_width}",
463 direction: "{direction}",
464 offset_y: "{-offset_y}",
465 reference: node_ref,
466 onwheel: onwheel,
467 {children}
468 }
469 if show_scrollbar && horizontal_scrollbar_is_visible {
470 ScrollBar {
471 size: &applied_scrollbar_theme.size,
472 offset_x: scrollbar_x,
473 clicking_scrollbar: is_scrolling_x,
474 theme: scrollbar_theme.clone(),
475 ScrollThumb {
476 clicking_scrollbar: is_scrolling_x,
477 onmousedown: onmousedown_x,
478 width: "{scrollbar_width}",
479 height: "100%",
480 theme: scrollbar_theme.clone(),
481 }
482 }
483 }
484
485 }
486 if show_scrollbar && vertical_scrollbar_is_visible {
487 ScrollBar {
488 is_vertical: true,
489 size: &applied_scrollbar_theme.size,
490 offset_y: scrollbar_y,
491 clicking_scrollbar: is_scrolling_y,
492 theme: scrollbar_theme.clone(),
493 ScrollThumb {
494 clicking_scrollbar: is_scrolling_y,
495 onmousedown: onmousedown_y,
496 width: "100%",
497 height: "{scrollbar_height}",
498 theme: scrollbar_theme,
499 }
500 }
501 }
502 }
503 )
504}
505
506#[cfg(test)]
507mod test {
508 use freya::prelude::*;
509 use freya_testing::prelude::*;
510
511 #[tokio::test]
512 pub async fn virtual_scroll_view_wheel() {
513 fn virtual_scroll_view_wheel_app() -> Element {
514 let values = use_signal(|| ["Hello, World!"].repeat(30));
515
516 rsx!(VirtualScrollView {
517 length: values.read().len(),
518 item_size: 50.0,
519 direction: "vertical",
520 builder: move |index, _: &Option<()>| {
521 let value = values.read()[index];
522 rsx! {
523 label {
524 key: "{index}",
525 height: "50",
526 "{index} {value}"
527 }
528 }
529 }
530 })
531 }
532
533 let mut utils = launch_test(virtual_scroll_view_wheel_app);
534 let root = utils.root();
535
536 utils.wait_for_update().await;
537 utils.wait_for_update().await;
538
539 let content = root.get(0).get(0).get(0);
540 assert_eq!(content.children_ids().len(), 11);
541
542 for (n, i) in (0..11).enumerate() {
544 let child = content.get(n);
545 assert_eq!(
546 child.get(0).text(),
547 Some(format!("{i} Hello, World!").as_str())
548 );
549 }
550
551 utils.push_event(TestEvent::Wheel {
552 name: EventName::Wheel,
553 scroll: (0., -300.).into(),
554 cursor: (5., 5.).into(),
555 });
556
557 utils.wait_for_update().await;
558 utils.wait_for_update().await;
559
560 let content = root.get(0).get(0).get(0);
561 assert_eq!(content.children_ids().len(), 11);
562
563 for (n, i) in (6..17).enumerate() {
566 let child = content.get(n);
567 assert_eq!(
568 child.get(0).text(),
569 Some(format!("{i} Hello, World!").as_str())
570 );
571 }
572 }
573
574 #[tokio::test]
575 pub async fn virtual_scroll_view_scrollbar() {
576 fn virtual_scroll_view_scrollar_app() -> Element {
577 let values = use_signal(|| ["Hello, World!"].repeat(30));
578
579 rsx!(VirtualScrollView {
580 length: values.read().len(),
581 item_size: 50.0,
582 direction: "vertical",
583 builder: move |index, _: &Option<()>| {
584 let value = values.read()[index];
585 rsx! {
586 label {
587 key: "{index}",
588 height: "50",
589 "{index} {value}"
590 }
591 }
592 }
593 })
594 }
595
596 let mut utils = launch_test(virtual_scroll_view_scrollar_app);
597 let root = utils.root();
598
599 utils.wait_for_update().await;
600 utils.wait_for_update().await;
601 utils.wait_for_update().await;
602
603 let content = root.get(0).get(0).get(0);
604 assert_eq!(content.children_ids().len(), 11);
605
606 for (n, i) in (0..11).enumerate() {
608 let child = content.get(n);
609 assert_eq!(
610 child.get(0).text(),
611 Some(format!("{i} Hello, World!").as_str())
612 );
613 }
614
615 utils.push_event(TestEvent::Mouse {
617 name: EventName::MouseMove,
618 cursor: (495., 20.).into(),
619 button: Some(MouseButton::Left),
620 });
621 utils.push_event(TestEvent::Mouse {
622 name: EventName::MouseDown,
623 cursor: (495., 20.).into(),
624 button: Some(MouseButton::Left),
625 });
626 utils.push_event(TestEvent::Mouse {
627 name: EventName::MouseMove,
628 cursor: (495., 320.).into(),
629 button: Some(MouseButton::Left),
630 });
631 utils.push_event(TestEvent::Mouse {
632 name: EventName::MouseUp,
633 cursor: (495., 320.).into(),
634 button: Some(MouseButton::Left),
635 });
636
637 utils.wait_for_update().await;
638 utils.wait_for_update().await;
639
640 let content = root.get(0).get(0).get(0);
641 assert_eq!(content.children_ids().len(), 11);
642
643 for (n, i) in (18..29).enumerate() {
645 let child = content.get(n);
646 assert_eq!(
647 child.get(0).text(),
648 Some(format!("{i} Hello, World!").as_str())
649 );
650 }
651
652 for _ in 0..11 {
654 utils.push_event(TestEvent::Keyboard {
655 name: EventName::KeyDown,
656 key: Key::ArrowUp,
657 code: Code::ArrowUp,
658 modifiers: Modifiers::default(),
659 });
660 utils.wait_for_update().await;
661 }
662
663 let content = root.get(0).get(0).get(0);
664 assert_eq!(content.children_ids().len(), 11);
665
666 for (n, i) in (0..11).enumerate() {
667 let child = content.get(n);
668 assert_eq!(
669 child.get(0).text(),
670 Some(format!("{i} Hello, World!").as_str())
671 );
672 }
673
674 utils.push_event(TestEvent::Keyboard {
676 name: EventName::KeyDown,
677 key: Key::End,
678 code: Code::End,
679 modifiers: Modifiers::default(),
680 });
681 utils.wait_for_update().await;
682 utils.wait_for_update().await;
683
684 let content = root.get(0).get(0).get(0);
685 assert_eq!(content.children_ids().len(), 9);
686
687 for (n, i) in (21..30).enumerate() {
688 let child = content.get(n);
689 assert_eq!(
690 child.get(0).text(),
691 Some(format!("{i} Hello, World!").as_str())
692 );
693 }
694 }
695}