1use std::{
2 ops::Range,
3 time::Duration,
4};
5
6use freya_core::prelude::*;
7use freya_sdk::timeout::use_timeout;
8use torin::{
9 geometry::CursorPoint,
10 node::Node,
11 prelude::Direction,
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#[derive(Clone, PartialEq)]
45pub enum ItemSize {
46 Fixed(f32),
48 Dynamic(Callback<usize, f32>),
50}
51
52impl ItemSize {
53 fn at(&self, index: usize) -> f32 {
55 match self {
56 Self::Fixed(size) => *size,
57 Self::Dynamic(callback) => callback.call(index),
58 }
59 }
60
61 fn visible_range(
64 &self,
65 viewport_size: f32,
66 scroll_position: f32,
67 length: usize,
68 ) -> (Range<usize>, f32) {
69 let scroll_distance = (-scroll_position).max(0.0);
70 match self {
71 Self::Fixed(size) => {
72 if *size <= 0.0 {
73 return (0..0, 0.0);
74 }
75 let start = scroll_distance / size;
76 let potentially_visible = (viewport_size / size) + 1.0;
77 let start_index = (start as usize).min(length);
78 let end_index = ((start + potentially_visible) as usize).min(length);
79 (
80 start_index..end_index,
81 start.floor() * size - scroll_distance,
82 )
83 }
84 Self::Dynamic(callback) => {
85 let mut start = 0;
86 let mut cumulative = 0.0;
87 while start < length {
88 let item = callback.call(start);
89 if cumulative + item > scroll_distance {
90 break;
91 }
92 cumulative += item;
93 start += 1;
94 }
95 let offset = cumulative - scroll_distance;
96 let mut end = start;
97 while end < length && cumulative < scroll_distance + viewport_size {
98 cumulative += callback.call(end);
99 end += 1;
100 }
101 (start..end, offset)
102 }
103 }
104 }
105
106 fn total_size(&self, viewport_size: f32, scroll_position: f32, length: usize) -> f32 {
111 match self {
112 Self::Fixed(size) => size * length as f32,
113 Self::Dynamic(callback) => {
114 if length == 0 {
115 return 0.0;
116 }
117 let viewport_bottom = (-scroll_position).max(0.0) + viewport_size;
118 let mut measured = callback.call(0);
119 let mut count = 1;
120 while count < length && measured < viewport_bottom {
121 measured += callback.call(count);
122 count += 1;
123 }
124 (measured / count as f32) * length as f32
125 }
126 }
127 }
128}
129
130impl Default for ItemSize {
131 fn default() -> Self {
132 Self::Fixed(0.)
133 }
134}
135
136impl From<f32> for ItemSize {
137 fn from(size: f32) -> Self {
138 Self::Fixed(size)
139 }
140}
141
142impl<F: Fn(usize) -> f32 + 'static> From<F> for ItemSize {
143 fn from(callback: F) -> Self {
144 Self::Dynamic(Callback::new(callback))
145 }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq)]
150pub struct VirtualItem {
151 pub index: usize,
152 pub size: f32,
153}
154
155#[cfg_attr(feature = "docs",
197 doc = embed_doc_image::embed_image!("virtual_scrollview", "images/gallery_virtual_scrollview.png")
198)]
199#[derive(Clone)]
200pub struct VirtualScrollView<D, B: Fn(VirtualItem, &D) -> Element> {
201 builder: B,
202 builder_data: D,
203 item_size: ItemSize,
204 length: usize,
205 layout: LayoutData,
206 show_scrollbar: bool,
207 scroll_with_arrows: bool,
208 scroll_controller: Option<ScrollController>,
209 invert_scroll_wheel: bool,
210 drag_scrolling: bool,
211 key: DiffKey,
212}
213
214impl<D: PartialEq, B: Fn(VirtualItem, &D) -> Element> LayoutExt for VirtualScrollView<D, B> {
215 fn get_layout(&mut self) -> &mut LayoutData {
216 &mut self.layout
217 }
218}
219
220impl<D: PartialEq, B: Fn(VirtualItem, &D) -> Element> ContainerSizeExt for VirtualScrollView<D, B> {}
221
222impl<D: PartialEq, B: Fn(VirtualItem, &D) -> Element> KeyExt for VirtualScrollView<D, B> {
223 fn write_key(&mut self) -> &mut DiffKey {
224 &mut self.key
225 }
226}
227
228impl<D: PartialEq, B: Fn(VirtualItem, &D) -> Element> PartialEq for VirtualScrollView<D, B> {
229 fn eq(&self, other: &Self) -> bool {
230 self.builder_data == other.builder_data
231 && self.item_size == other.item_size
232 && self.length == other.length
233 && self.layout == other.layout
234 && self.show_scrollbar == other.show_scrollbar
235 && self.scroll_with_arrows == other.scroll_with_arrows
236 && self.scroll_controller == other.scroll_controller
237 && self.invert_scroll_wheel == other.invert_scroll_wheel
238 }
239}
240
241impl<B: Fn(VirtualItem, &()) -> Element> VirtualScrollView<(), B> {
242 pub fn new(builder: B) -> Self {
244 Self {
245 builder,
246 builder_data: (),
247 item_size: ItemSize::default(),
248 length: 0,
249 layout: {
250 let mut l = LayoutData::default();
251 l.layout.width = Size::fill();
252 l.layout.height = Size::fill();
253 l
254 },
255 show_scrollbar: true,
256 scroll_with_arrows: true,
257 scroll_controller: None,
258 invert_scroll_wheel: false,
259 drag_scrolling: true,
260 key: DiffKey::None,
261 }
262 }
263
264 pub fn new_controlled(builder: B, scroll_controller: ScrollController) -> Self {
266 Self {
267 builder,
268 builder_data: (),
269 item_size: ItemSize::default(),
270 length: 0,
271 layout: {
272 let mut l = LayoutData::default();
273 l.layout.width = Size::fill();
274 l.layout.height = Size::fill();
275 l
276 },
277 show_scrollbar: true,
278 scroll_with_arrows: true,
279 scroll_controller: Some(scroll_controller),
280 invert_scroll_wheel: false,
281 drag_scrolling: true,
282 key: DiffKey::None,
283 }
284 }
285}
286
287impl<D, B: Fn(VirtualItem, &D) -> Element> VirtualScrollView<D, B> {
288 pub fn new_with_data(builder_data: D, builder: B) -> Self {
312 Self {
313 builder,
314 builder_data,
315 item_size: ItemSize::default(),
316 length: 0,
317 layout: Node {
318 width: Size::fill(),
319 height: Size::fill(),
320 ..Default::default()
321 }
322 .into(),
323 show_scrollbar: true,
324 scroll_with_arrows: true,
325 scroll_controller: None,
326 invert_scroll_wheel: false,
327 drag_scrolling: true,
328 key: DiffKey::None,
329 }
330 }
331
332 pub fn new_with_data_controlled(
334 builder_data: D,
335 builder: B,
336 scroll_controller: ScrollController,
337 ) -> Self {
338 Self {
339 builder,
340 builder_data,
341 item_size: ItemSize::default(),
342 length: 0,
343
344 layout: Node {
345 width: Size::fill(),
346 height: Size::fill(),
347 ..Default::default()
348 }
349 .into(),
350 show_scrollbar: true,
351 scroll_with_arrows: true,
352 scroll_controller: Some(scroll_controller),
353 invert_scroll_wheel: false,
354 drag_scrolling: true,
355 key: DiffKey::None,
356 }
357 }
358
359 pub fn show_scrollbar(mut self, show_scrollbar: bool) -> Self {
361 self.show_scrollbar = show_scrollbar;
362 self
363 }
364
365 pub fn direction(mut self, direction: Direction) -> Self {
367 self.layout.direction = direction;
368 self
369 }
370
371 pub fn scroll_with_arrows(mut self, scroll_with_arrows: impl Into<bool>) -> Self {
373 self.scroll_with_arrows = scroll_with_arrows.into();
374 self
375 }
376
377 pub fn item_size(mut self, item_size: impl Into<ItemSize>) -> Self {
382 self.item_size = item_size.into();
383 self
384 }
385
386 pub fn length(mut self, length: impl Into<usize>) -> Self {
388 self.length = length.into();
389 self
390 }
391
392 pub fn invert_scroll_wheel(mut self, invert_scroll_wheel: impl Into<bool>) -> Self {
394 self.invert_scroll_wheel = invert_scroll_wheel.into();
395 self
396 }
397
398 pub fn drag_scrolling(mut self, drag_scrolling: bool) -> Self {
400 self.drag_scrolling = drag_scrolling;
401 self
402 }
403
404 pub fn scroll_controller(
406 mut self,
407 scroll_controller: impl Into<Option<ScrollController>>,
408 ) -> Self {
409 self.scroll_controller = scroll_controller.into();
410 self
411 }
412
413 pub fn min_width(mut self, min_width: impl Into<Size>) -> Self {
415 self.layout.minimum_width = min_width.into();
416 self
417 }
418
419 pub fn min_height(mut self, min_height: impl Into<Size>) -> Self {
421 self.layout.minimum_height = min_height.into();
422 self
423 }
424
425 pub fn max_width(mut self, max_width: impl Into<Size>) -> Self {
427 self.layout.maximum_width = max_width.into();
428 self
429 }
430
431 pub fn max_height(mut self, max_height: impl Into<Size>) -> Self {
433 self.layout.maximum_height = max_height.into();
434 self
435 }
436}
437
438impl<D: PartialEq + 'static, B: Fn(VirtualItem, &D) -> Element + 'static> Component
439 for VirtualScrollView<D, B>
440{
441 fn render(self: &VirtualScrollView<D, B>) -> impl IntoElement {
442 let a11y_id = use_a11y();
443 let mut timeout = use_timeout(|| Duration::from_millis(800));
444 let mut pressing_shift = use_state(|| false);
445 let mut clicking_scrollbar = use_state::<Option<(Axis, f64)>>(|| None);
446 let mut size = use_state(SizedEventData::default);
447 let mut scroll_controller = self
448 .scroll_controller
449 .unwrap_or_else(|| use_scroll_controller(ScrollConfig::default));
450 let mut dragging_content = use_state::<Option<CursorPoint>>(|| None);
451 let mut drag_origin = use_state::<Option<CursorPoint>>(|| None);
452 let (scrolled_x, scrolled_y) = scroll_controller.into();
453 let layout = &self.layout.layout;
454 let direction = layout.direction;
455 let drag_scrolling = self.drag_scrolling;
456
457 let viewport_width = size.read().area.width();
458 let viewport_height = size.read().area.height();
459
460 let (inner_width, inner_height) = match direction {
461 Direction::Vertical => (
462 size.read().inner_sizes.width,
463 self.item_size
464 .total_size(viewport_height, scrolled_y as f32, self.length),
465 ),
466 Direction::Horizontal => (
467 self.item_size
468 .total_size(viewport_width, scrolled_x as f32, self.length),
469 size.read().inner_sizes.height,
470 ),
471 };
472
473 scroll_controller.use_apply(inner_width, inner_height);
474
475 let corrected_scrolled_x =
476 get_corrected_scroll_position(inner_width, size.read().area.width(), scrolled_x as f32);
477
478 let corrected_scrolled_y = get_corrected_scroll_position(
479 inner_height,
480 size.read().area.height(),
481 scrolled_y as f32,
482 );
483 let horizontal_scrollbar_is_visible = !timeout.elapsed()
484 && is_scrollbar_visible(self.show_scrollbar, inner_width, size.read().area.width());
485 let vertical_scrollbar_is_visible = !timeout.elapsed()
486 && is_scrollbar_visible(self.show_scrollbar, inner_height, size.read().area.height());
487
488 let (scrollbar_x, scrollbar_width) =
489 get_scrollbar_pos_and_size(inner_width, size.read().area.width(), corrected_scrolled_x);
490 let (scrollbar_y, scrollbar_height) = get_scrollbar_pos_and_size(
491 inner_height,
492 size.read().area.height(),
493 corrected_scrolled_y,
494 );
495
496 let (container_width, content_width) = get_container_sizes(self.layout.width.clone());
497 let (container_height, content_height) = get_container_sizes(self.layout.height.clone());
498
499 let scroll_with_arrows = self.scroll_with_arrows;
500 let invert_scroll_wheel = self.invert_scroll_wheel;
501
502 let on_capture_global_pointer_press = move |e: Event<PointerEventData>| {
503 if clicking_scrollbar.read().is_some() {
504 e.prevent_default();
505 clicking_scrollbar.set(None);
506 }
507
508 if drag_scrolling && (dragging_content().is_some() || drag_origin().is_some()) {
509 dragging_content.set(None);
510 drag_origin.set(None);
511 }
512 };
513
514 let on_wheel = move |e: Event<WheelEventData>| {
515 let invert_direction = e.source == WheelSource::Device
517 && (*pressing_shift.read() || invert_scroll_wheel)
518 && (!*pressing_shift.read() || !invert_scroll_wheel);
519
520 let (x_movement, y_movement) = if invert_direction {
521 (e.delta_y as f32, e.delta_x as f32)
522 } else {
523 (e.delta_x as f32, e.delta_y as f32)
524 };
525
526 let scroll_position_y = get_scroll_position_from_wheel(
528 y_movement,
529 inner_height,
530 size.read().area.height(),
531 corrected_scrolled_y,
532 );
533 scroll_controller.scroll_to_y(scroll_position_y).then(|| {
534 e.stop_propagation();
535 });
536
537 let scroll_position_x = get_scroll_position_from_wheel(
539 x_movement,
540 inner_width,
541 size.read().area.width(),
542 corrected_scrolled_x,
543 );
544 scroll_controller.scroll_to_x(scroll_position_x).then(|| {
545 e.stop_propagation();
546 });
547 timeout.reset();
548 };
549
550 let on_mouse_move = move |_| {
551 timeout.reset();
552 };
553
554 let on_capture_global_pointer_move = move |e: Event<PointerEventData>| {
555 if drag_scrolling {
556 if let Some(prev) = dragging_content() {
557 let coords = e.global_location();
558 let delta = prev - coords;
559
560 scroll_controller.scroll_to_y((corrected_scrolled_y - delta.y as f32) as i32);
561 scroll_controller.scroll_to_x((corrected_scrolled_x - delta.x as f32) as i32);
562
563 dragging_content.set(Some(coords));
564 e.prevent_default();
565 timeout.reset();
566 a11y_id.request_focus();
567 return;
568 } else if let Some(origin) = drag_origin() {
569 let coords = e.global_location();
570 let distance = (origin - coords).abs();
571
572 const DRAG_THRESHOLD: f64 = 2.0;
575
576 if distance.x > DRAG_THRESHOLD || distance.y > DRAG_THRESHOLD {
577 let delta = origin - coords;
578
579 scroll_controller
580 .scroll_to_y((corrected_scrolled_y - delta.y as f32) as i32);
581 scroll_controller
582 .scroll_to_x((corrected_scrolled_x - delta.x as f32) as i32);
583
584 dragging_content.set(Some(coords));
585 e.prevent_default();
586 timeout.reset();
587 a11y_id.request_focus();
588 }
589 return;
590 }
591 }
592
593 let clicking_scrollbar = clicking_scrollbar.peek();
594
595 if let Some((Axis::Y, y)) = *clicking_scrollbar {
596 let coordinates = e.element_location();
597 let cursor_y = coordinates.y - y - size.read().area.min_y() as f64;
598
599 let scroll_position = get_scroll_position_from_cursor(
600 cursor_y as f32,
601 inner_height,
602 size.read().area.height(),
603 );
604
605 scroll_controller.scroll_to_y(scroll_position);
606 } else if let Some((Axis::X, x)) = *clicking_scrollbar {
607 let coordinates = e.element_location();
608 let cursor_x = coordinates.x - x - size.read().area.min_x() as f64;
609
610 let scroll_position = get_scroll_position_from_cursor(
611 cursor_x as f32,
612 inner_width,
613 size.read().area.width(),
614 );
615
616 scroll_controller.scroll_to_x(scroll_position);
617 }
618
619 if clicking_scrollbar.is_some() {
620 e.prevent_default();
621 timeout.reset();
622 a11y_id.request_focus();
623 }
624 };
625
626 let on_key_down = move |e: Event<KeyboardEventData>| {
627 if !scroll_with_arrows
628 && (e.key == Key::Named(NamedKey::ArrowUp)
629 || e.key == Key::Named(NamedKey::ArrowRight)
630 || e.key == Key::Named(NamedKey::ArrowDown)
631 || e.key == Key::Named(NamedKey::ArrowLeft))
632 {
633 return;
634 }
635 let x = corrected_scrolled_x;
636 let y = corrected_scrolled_y;
637 let inner_height = inner_height;
638 let inner_width = inner_width;
639 let viewport_height = size.read().area.height();
640 let viewport_width = size.read().area.width();
641 if let Some((x, y)) = handle_key_event(
642 &e.key,
643 (x, y),
644 inner_height,
645 inner_width,
646 viewport_height,
647 viewport_width,
648 direction,
649 ) {
650 scroll_controller.scroll_to_x(x as i32);
651 scroll_controller.scroll_to_y(y as i32);
652 e.stop_propagation();
653 timeout.reset();
654 }
655 };
656
657 let on_global_key_down = move |e: Event<KeyboardEventData>| {
658 let data = e;
659 if data.key == Key::Named(NamedKey::Shift) {
660 pressing_shift.set(true);
661 }
662 };
663
664 let on_global_key_up = move |e: Event<KeyboardEventData>| {
665 let data = e;
666 if data.key == Key::Named(NamedKey::Shift) {
667 pressing_shift.set(false);
668 }
669 };
670
671 let (viewport_size, scroll_position) = if direction == Direction::vertical() {
672 (viewport_height, corrected_scrolled_y)
673 } else {
674 (viewport_width, corrected_scrolled_x)
675 };
676
677 let (render_range, item_offset) =
678 self.item_size
679 .visible_range(viewport_size, scroll_position, self.length);
680
681 let children = render_range
682 .map(|i| {
683 let item = VirtualItem {
684 index: i,
685 size: self.item_size.at(i),
686 };
687 (self.builder)(item, &self.builder_data)
688 })
689 .collect::<Vec<Element>>();
690
691 let (offset_x, offset_y) = match direction {
692 Direction::Vertical => (corrected_scrolled_x, item_offset),
693 Direction::Horizontal => (item_offset, corrected_scrolled_y),
694 };
695
696 let on_pointer_down = move |e: Event<PointerEventData>| {
697 if drag_scrolling && matches!(e.data(), PointerEventData::Touch(_)) {
698 drag_origin.set(Some(e.global_location()));
699 }
700 };
701
702 rect()
703 .width(layout.width.clone())
704 .height(layout.height.clone())
705 .min_width(layout.minimum_width.clone())
706 .min_height(layout.minimum_height.clone())
707 .max_width(layout.maximum_width.clone())
708 .max_height(layout.maximum_height.clone())
709 .a11y_id(a11y_id)
710 .a11y_focusable(false)
711 .a11y_role(AccessibilityRole::ScrollView)
712 .a11y_builder(move |node| {
713 node.set_scroll_x(corrected_scrolled_x as f64);
714 node.set_scroll_y(corrected_scrolled_y as f64)
715 })
716 .scrollable(true)
717 .on_wheel(on_wheel)
718 .on_capture_global_pointer_press(on_capture_global_pointer_press)
719 .on_mouse_move(on_mouse_move)
720 .on_capture_global_pointer_move(on_capture_global_pointer_move)
721 .on_key_down(on_key_down)
722 .on_global_key_up(on_global_key_up)
723 .on_global_key_down(on_global_key_down)
724 .on_pointer_down(on_pointer_down)
725 .child(
726 rect()
727 .width(container_width)
728 .height(container_height)
729 .horizontal()
730 .child(
731 rect()
732 .direction(direction)
733 .width(content_width)
734 .height(content_height)
735 .offset_x(offset_x)
736 .offset_y(offset_y)
737 .overflow(Overflow::Clip)
738 .on_sized(move |e: Event<SizedEventData>| {
739 size.set_if_modified(e.clone())
740 })
741 .children(children),
742 )
743 .maybe_child(vertical_scrollbar_is_visible.then_some({
744 rect().child(ScrollBar {
745 theme: None,
746 clicking_scrollbar,
747 axis: Axis::Y,
748 offset: scrollbar_y,
749 size: Size::px(size.read().area.height()),
750 thumb: ScrollThumb {
751 theme: None,
752 clicking_scrollbar,
753 axis: Axis::Y,
754 size: scrollbar_height,
755 },
756 })
757 })),
758 )
759 .maybe_child(horizontal_scrollbar_is_visible.then_some({
760 rect().child(ScrollBar {
761 theme: None,
762 clicking_scrollbar,
763 axis: Axis::X,
764 offset: scrollbar_x,
765 size: Size::px(size.read().area.width()),
766 thumb: ScrollThumb {
767 theme: None,
768 clicking_scrollbar,
769 axis: Axis::X,
770 size: scrollbar_width,
771 },
772 })
773 }))
774 }
775
776 fn render_key(&self) -> DiffKey {
777 self.key.clone().or(self.default_key())
778 }
779}