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_sizes,
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 inner_size = item_size * length as f32;
226
227 scroll_controller.use_apply(inner_size, inner_size);
228
229 let vertical_scrollbar_is_visible = direction != "horizontal"
230 && is_scrollbar_visible(show_scrollbar, inner_size, size.area.height());
231 let horizontal_scrollbar_is_visible = direction != "vertical"
232 && is_scrollbar_visible(show_scrollbar, inner_size, size.area.width());
233
234 let (container_width, content_width) = get_container_sizes(&width);
235 let (container_height, content_height) = get_container_sizes(&height);
236
237 let corrected_scrolled_y =
238 get_corrected_scroll_position(inner_size, size.area.height(), *scrolled_y.read() as f32);
239 let corrected_scrolled_x =
240 get_corrected_scroll_position(inner_size, size.area.width(), *scrolled_x.read() as f32);
241
242 let (scrollbar_y, scrollbar_height) =
243 get_scrollbar_pos_and_size(inner_size, size.area.height(), corrected_scrolled_y);
244 let (scrollbar_x, scrollbar_width) =
245 get_scrollbar_pos_and_size(inner_size, size.area.width(), corrected_scrolled_x);
246
247 let onwheel = move |e: WheelEvent| {
249 let speed_multiplier = if *clicking_alt.peek() {
250 SCROLL_SPEED_MULTIPLIER
251 } else {
252 1.0
253 };
254
255 let invert_direction = (clicking_shift() || invert_scroll_wheel)
256 && (!clicking_shift() || !invert_scroll_wheel);
257
258 let (x_movement, y_movement) = if invert_direction {
259 (
260 e.get_delta_y() as f32 * speed_multiplier,
261 e.get_delta_x() as f32 * speed_multiplier,
262 )
263 } else {
264 (
265 e.get_delta_x() as f32 * speed_multiplier,
266 e.get_delta_y() as f32 * speed_multiplier,
267 )
268 };
269
270 let scroll_position_y = get_scroll_position_from_wheel(
271 y_movement,
272 inner_size,
273 size.area.height(),
274 corrected_scrolled_y,
275 );
276
277 if *scrolled_y.peek() != scroll_position_y {
279 e.stop_propagation();
280 *scrolled_y.write() = scroll_position_y;
281 focus.request_focus();
282 }
283
284 let scroll_position_x = get_scroll_position_from_wheel(
285 x_movement,
286 inner_size,
287 size.area.width(),
288 corrected_scrolled_x,
289 );
290
291 if *scrolled_x.peek() != scroll_position_x {
293 e.stop_propagation();
294 *scrolled_x.write() = scroll_position_x;
295 focus.request_focus();
296 }
297 };
298
299 let onmousemove = move |e: MouseEvent| {
301 let clicking_scrollbar = clicking_scrollbar.peek();
302
303 if let Some((Axis::Y, y)) = *clicking_scrollbar {
304 let coordinates = e.get_element_coordinates();
305 let cursor_y = coordinates.y - y - size.area.min_y() as f64;
306
307 let scroll_position =
308 get_scroll_position_from_cursor(cursor_y as f32, inner_size, size.area.height());
309
310 *scrolled_y.write() = scroll_position;
311 } else if let Some((Axis::X, x)) = *clicking_scrollbar {
312 let coordinates = e.get_element_coordinates();
313 let cursor_x = coordinates.x - x - size.area.min_x() as f64;
314
315 let scroll_position =
316 get_scroll_position_from_cursor(cursor_x as f32, inner_size, size.area.width());
317
318 *scrolled_x.write() = scroll_position;
319 }
320
321 if clicking_scrollbar.is_some() {
322 focus.request_focus();
323 }
324 };
325
326 let onglobalkeydown = move |e: KeyboardEvent| {
327 match &e.key {
328 Key::Shift => {
329 clicking_shift.set(true);
330 }
331 Key::Alt => {
332 clicking_alt.set(true);
333 }
334 k => {
335 if !focus.is_focused() {
336 return;
337 }
338
339 if !scroll_with_arrows
340 && (k == &Key::ArrowUp
341 || k == &Key::ArrowRight
342 || k == &Key::ArrowDown
343 || k == &Key::ArrowLeft)
344 {
345 return;
346 }
347
348 let x = corrected_scrolled_x;
349 let y = corrected_scrolled_y;
350 let inner_height = inner_size;
351 let inner_width = inner_size;
352 let viewport_height = size.area.height();
353 let viewport_width = size.area.width();
354
355 let (x, y) = manage_key_event(
356 e,
357 (x, y),
358 inner_height,
359 inner_width,
360 viewport_height,
361 viewport_width,
362 );
363
364 scrolled_x.set(x as i32);
365 scrolled_y.set(y as i32);
366 }
367 };
368 };
369
370 let onglobalkeyup = move |e: KeyboardEvent| {
371 if e.key == Key::Shift {
372 clicking_shift.set(false);
373 } else if e.key == Key::Alt {
374 clicking_alt.set(false);
375 }
376 };
377
378 let onmousedown_y = move |e: MouseEvent| {
380 let coordinates = e.get_element_coordinates();
381 *clicking_scrollbar.write() = Some((Axis::Y, coordinates.y));
382 };
383
384 let onmousedown_x = move |e: MouseEvent| {
386 let coordinates = e.get_element_coordinates();
387 *clicking_scrollbar.write() = Some((Axis::X, coordinates.x));
388 };
389
390 let onclick = move |_: MouseEvent| {
392 if clicking_scrollbar.peek().is_some() {
393 *clicking_scrollbar.write() = None;
394 }
395 };
396
397 let (viewport_size, scroll_position) = if direction == "vertical" {
398 (size.area.height(), corrected_scrolled_y)
399 } else {
400 (size.area.width(), corrected_scrolled_x)
401 };
402
403 let render_range = get_render_range(viewport_size, scroll_position, item_size, length as f32);
405
406 let children = if cache_elements {
407 let children = use_memo(use_reactive(
408 &(render_range, builder_args),
409 move |(render_range, builder_args)| {
410 render_range
411 .clone()
412 .map(|i| (builder)(i, &builder_args))
413 .collect::<Vec<Element>>()
414 },
415 ));
416 rsx!({ children.read().iter() })
417 } else {
418 let children = render_range.map(|i| (builder)(i, &builder_args));
419 rsx!({ children })
420 };
421
422 let is_scrolling_x = clicking_scrollbar
423 .read()
424 .as_ref()
425 .map(|f| f.0 == Axis::X)
426 .unwrap_or_default();
427 let is_scrolling_y = clicking_scrollbar
428 .read()
429 .as_ref()
430 .map(|f| f.0 == Axis::Y)
431 .unwrap_or_default();
432
433 let offset_y_min = (-corrected_scrolled_y / item_size).floor() * item_size;
434 let offset_y = -corrected_scrolled_y - offset_y_min;
435
436 let a11y_id = focus.attribute();
437
438 rsx!(
439 rect {
440 a11y_role: "scroll-view",
441 overflow: "clip",
442 direction: "horizontal",
443 width: "{width}",
444 height: "{height}",
445 onglobalclick: onclick,
446 onglobalmousemove: onmousemove,
447 onglobalkeydown,
448 onglobalkeyup,
449 a11y_id,
450 rect {
451 direction: "vertical",
452 width: "{container_width}",
453 height: "{container_height}",
454 rect {
455 overflow: "clip",
456 padding: "{padding}",
457 height: "{content_height}",
458 width: "{content_width}",
459 direction: "{direction}",
460 offset_y: "{-offset_y}",
461 reference: node_ref,
462 onwheel: onwheel,
463 {children}
464 }
465 if show_scrollbar && horizontal_scrollbar_is_visible {
466 ScrollBar {
467 size: &applied_scrollbar_theme.size,
468 offset_x: scrollbar_x,
469 clicking_scrollbar: is_scrolling_x,
470 theme: scrollbar_theme.clone(),
471 ScrollThumb {
472 clicking_scrollbar: is_scrolling_x,
473 onmousedown: onmousedown_x,
474 width: "{scrollbar_width}",
475 height: "100%",
476 theme: scrollbar_theme.clone(),
477 }
478 }
479 }
480
481 }
482 if show_scrollbar && vertical_scrollbar_is_visible {
483 ScrollBar {
484 is_vertical: true,
485 size: &applied_scrollbar_theme.size,
486 offset_y: scrollbar_y,
487 clicking_scrollbar: is_scrolling_y,
488 theme: scrollbar_theme.clone(),
489 ScrollThumb {
490 clicking_scrollbar: is_scrolling_y,
491 onmousedown: onmousedown_y,
492 width: "100%",
493 height: "{scrollbar_height}",
494 theme: scrollbar_theme,
495 }
496 }
497 }
498 }
499 )
500}
501
502#[cfg(test)]
503mod test {
504 use freya::prelude::*;
505 use freya_testing::prelude::*;
506
507 #[tokio::test]
508 pub async fn virtual_scroll_view_wheel() {
509 fn virtual_scroll_view_wheel_app() -> Element {
510 let values = use_signal(|| ["Hello, World!"].repeat(30));
511
512 rsx!(VirtualScrollView {
513 length: values.read().len(),
514 item_size: 50.0,
515 direction: "vertical",
516 builder: move |index, _: &Option<()>| {
517 let value = values.read()[index];
518 rsx! {
519 label {
520 key: "{index}",
521 height: "50",
522 "{index} {value}"
523 }
524 }
525 }
526 })
527 }
528
529 let mut utils = launch_test(virtual_scroll_view_wheel_app);
530 let root = utils.root();
531
532 utils.wait_for_update().await;
533 utils.wait_for_update().await;
534
535 let content = root.get(0).get(0).get(0);
536 assert_eq!(content.children_ids().len(), 11);
537
538 for (n, i) in (0..11).enumerate() {
540 let child = content.get(n);
541 assert_eq!(
542 child.get(0).text(),
543 Some(format!("{i} Hello, World!").as_str())
544 );
545 }
546
547 utils.push_event(TestEvent::Wheel {
548 name: EventName::Wheel,
549 scroll: (0., -300.).into(),
550 cursor: (5., 5.).into(),
551 });
552
553 utils.wait_for_update().await;
554 utils.wait_for_update().await;
555
556 let content = root.get(0).get(0).get(0);
557 assert_eq!(content.children_ids().len(), 11);
558
559 for (n, i) in (6..17).enumerate() {
562 let child = content.get(n);
563 assert_eq!(
564 child.get(0).text(),
565 Some(format!("{i} Hello, World!").as_str())
566 );
567 }
568 }
569
570 #[tokio::test]
571 pub async fn virtual_scroll_view_scrollbar() {
572 fn virtual_scroll_view_scrollar_app() -> Element {
573 let values = use_signal(|| ["Hello, World!"].repeat(30));
574
575 rsx!(VirtualScrollView {
576 length: values.read().len(),
577 item_size: 50.0,
578 direction: "vertical",
579 builder: move |index, _: &Option<()>| {
580 let value = values.read()[index];
581 rsx! {
582 label {
583 key: "{index}",
584 height: "50",
585 "{index} {value}"
586 }
587 }
588 }
589 })
590 }
591
592 let mut utils = launch_test(virtual_scroll_view_scrollar_app);
593 let root = utils.root();
594
595 utils.wait_for_update().await;
596 utils.wait_for_update().await;
597 utils.wait_for_update().await;
598
599 let content = root.get(0).get(0).get(0);
600 assert_eq!(content.children_ids().len(), 11);
601
602 for (n, i) in (0..11).enumerate() {
604 let child = content.get(n);
605 assert_eq!(
606 child.get(0).text(),
607 Some(format!("{i} Hello, World!").as_str())
608 );
609 }
610
611 utils.push_event(TestEvent::Mouse {
613 name: EventName::MouseMove,
614 cursor: (495., 20.).into(),
615 button: Some(MouseButton::Left),
616 });
617 utils.push_event(TestEvent::Mouse {
618 name: EventName::MouseDown,
619 cursor: (495., 20.).into(),
620 button: Some(MouseButton::Left),
621 });
622 utils.push_event(TestEvent::Mouse {
623 name: EventName::MouseMove,
624 cursor: (495., 320.).into(),
625 button: Some(MouseButton::Left),
626 });
627 utils.push_event(TestEvent::Mouse {
628 name: EventName::MouseUp,
629 cursor: (495., 320.).into(),
630 button: Some(MouseButton::Left),
631 });
632
633 utils.wait_for_update().await;
634 utils.wait_for_update().await;
635
636 let content = root.get(0).get(0).get(0);
637 assert_eq!(content.children_ids().len(), 11);
638
639 for (n, i) in (18..29).enumerate() {
641 let child = content.get(n);
642 assert_eq!(
643 child.get(0).text(),
644 Some(format!("{i} Hello, World!").as_str())
645 );
646 }
647
648 for _ in 0..11 {
650 utils.push_event(TestEvent::Keyboard {
651 name: EventName::KeyDown,
652 key: Key::ArrowUp,
653 code: Code::ArrowUp,
654 modifiers: Modifiers::default(),
655 });
656 utils.wait_for_update().await;
657 }
658
659 let content = root.get(0).get(0).get(0);
660 assert_eq!(content.children_ids().len(), 11);
661
662 for (n, i) in (0..11).enumerate() {
663 let child = content.get(n);
664 assert_eq!(
665 child.get(0).text(),
666 Some(format!("{i} Hello, World!").as_str())
667 );
668 }
669
670 utils.push_event(TestEvent::Keyboard {
672 name: EventName::KeyDown,
673 key: Key::End,
674 code: Code::End,
675 modifiers: Modifiers::default(),
676 });
677 utils.wait_for_update().await;
678 utils.wait_for_update().await;
679
680 let content = root.get(0).get(0).get(0);
681 assert_eq!(content.children_ids().len(), 10);
682
683 for (n, i) in (20..30).enumerate() {
684 let child = content.get(n);
685 assert_eq!(
686 child.get(0).text(),
687 Some(format!("{i} Hello, World!").as_str())
688 );
689 }
690 }
691}