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",
189 doc = embed_doc_image::embed_image!("virtual_scrollview", "images/gallery_virtual_scrollview.png")
190)]
191#[derive(Clone)]
192pub struct VirtualScrollView<D, B: Fn(VirtualItem, &D) -> Element> {
193 builder: B,
194 builder_data: D,
195 item_size: ItemSize,
196 length: usize,
197 layout: LayoutData,
198 show_scrollbar: bool,
199 scroll_with_arrows: bool,
200 scroll_controller: Option<ScrollController>,
201 invert_scroll_wheel: bool,
202 drag_scrolling: bool,
203 key: DiffKey,
204}
205
206impl<D: PartialEq, B: Fn(VirtualItem, &D) -> Element> LayoutExt for VirtualScrollView<D, B> {
207 fn get_layout(&mut self) -> &mut LayoutData {
208 &mut self.layout
209 }
210}
211
212impl<D: PartialEq, B: Fn(VirtualItem, &D) -> Element> ContainerSizeExt for VirtualScrollView<D, B> {}
213
214impl<D: PartialEq, B: Fn(VirtualItem, &D) -> Element> KeyExt for VirtualScrollView<D, B> {
215 fn write_key(&mut self) -> &mut DiffKey {
216 &mut self.key
217 }
218}
219
220impl<D: PartialEq, B: Fn(VirtualItem, &D) -> Element> PartialEq for VirtualScrollView<D, B> {
221 fn eq(&self, other: &Self) -> bool {
222 self.builder_data == other.builder_data
223 && self.item_size == other.item_size
224 && self.length == other.length
225 && self.layout == other.layout
226 && self.show_scrollbar == other.show_scrollbar
227 && self.scroll_with_arrows == other.scroll_with_arrows
228 && self.scroll_controller == other.scroll_controller
229 && self.invert_scroll_wheel == other.invert_scroll_wheel
230 }
231}
232
233impl<B: Fn(VirtualItem, &()) -> Element> VirtualScrollView<(), B> {
234 pub fn new(builder: B) -> Self {
236 Self {
237 builder,
238 builder_data: (),
239 item_size: ItemSize::default(),
240 length: 0,
241 layout: {
242 let mut l = LayoutData::default();
243 l.layout.width = Size::fill();
244 l.layout.height = Size::fill();
245 l
246 },
247 show_scrollbar: true,
248 scroll_with_arrows: true,
249 scroll_controller: None,
250 invert_scroll_wheel: false,
251 drag_scrolling: true,
252 key: DiffKey::None,
253 }
254 }
255
256 pub fn new_controlled(builder: B, scroll_controller: ScrollController) -> Self {
258 Self {
259 builder,
260 builder_data: (),
261 item_size: ItemSize::default(),
262 length: 0,
263 layout: {
264 let mut l = LayoutData::default();
265 l.layout.width = Size::fill();
266 l.layout.height = Size::fill();
267 l
268 },
269 show_scrollbar: true,
270 scroll_with_arrows: true,
271 scroll_controller: Some(scroll_controller),
272 invert_scroll_wheel: false,
273 drag_scrolling: true,
274 key: DiffKey::None,
275 }
276 }
277}
278
279impl<D, B: Fn(VirtualItem, &D) -> Element> VirtualScrollView<D, B> {
280 pub fn new_with_data(builder_data: D, builder: B) -> Self {
286 Self {
287 builder,
288 builder_data,
289 item_size: ItemSize::default(),
290 length: 0,
291 layout: Node {
292 width: Size::fill(),
293 height: Size::fill(),
294 ..Default::default()
295 }
296 .into(),
297 show_scrollbar: true,
298 scroll_with_arrows: true,
299 scroll_controller: None,
300 invert_scroll_wheel: false,
301 drag_scrolling: true,
302 key: DiffKey::None,
303 }
304 }
305
306 pub fn new_with_data_controlled(
308 builder_data: D,
309 builder: B,
310 scroll_controller: ScrollController,
311 ) -> Self {
312 Self {
313 builder,
314 builder_data,
315 item_size: ItemSize::default(),
316 length: 0,
317
318 layout: Node {
319 width: Size::fill(),
320 height: Size::fill(),
321 ..Default::default()
322 }
323 .into(),
324 show_scrollbar: true,
325 scroll_with_arrows: true,
326 scroll_controller: Some(scroll_controller),
327 invert_scroll_wheel: false,
328 drag_scrolling: true,
329 key: DiffKey::None,
330 }
331 }
332
333 pub fn show_scrollbar(mut self, show_scrollbar: bool) -> Self {
334 self.show_scrollbar = show_scrollbar;
335 self
336 }
337
338 pub fn direction(mut self, direction: Direction) -> Self {
339 self.layout.direction = direction;
340 self
341 }
342
343 pub fn scroll_with_arrows(mut self, scroll_with_arrows: impl Into<bool>) -> Self {
344 self.scroll_with_arrows = scroll_with_arrows.into();
345 self
346 }
347
348 pub fn item_size(mut self, item_size: impl Into<ItemSize>) -> Self {
349 self.item_size = item_size.into();
350 self
351 }
352
353 pub fn length(mut self, length: impl Into<usize>) -> Self {
354 self.length = length.into();
355 self
356 }
357
358 pub fn invert_scroll_wheel(mut self, invert_scroll_wheel: impl Into<bool>) -> Self {
359 self.invert_scroll_wheel = invert_scroll_wheel.into();
360 self
361 }
362
363 pub fn drag_scrolling(mut self, drag_scrolling: bool) -> Self {
364 self.drag_scrolling = drag_scrolling;
365 self
366 }
367
368 pub fn scroll_controller(
369 mut self,
370 scroll_controller: impl Into<Option<ScrollController>>,
371 ) -> Self {
372 self.scroll_controller = scroll_controller.into();
373 self
374 }
375
376 pub fn max_width(mut self, max_width: impl Into<Size>) -> Self {
377 self.layout.maximum_width = max_width.into();
378 self
379 }
380
381 pub fn max_height(mut self, max_height: impl Into<Size>) -> Self {
382 self.layout.maximum_height = max_height.into();
383 self
384 }
385}
386
387impl<D: PartialEq + 'static, B: Fn(VirtualItem, &D) -> Element + 'static> Component
388 for VirtualScrollView<D, B>
389{
390 fn render(self: &VirtualScrollView<D, B>) -> impl IntoElement {
391 let a11y_id = use_a11y();
392 let mut timeout = use_timeout(|| Duration::from_millis(800));
393 let mut pressing_shift = use_state(|| false);
394 let mut clicking_scrollbar = use_state::<Option<(Axis, f64)>>(|| None);
395 let mut size = use_state(SizedEventData::default);
396 let mut scroll_controller = self
397 .scroll_controller
398 .unwrap_or_else(|| use_scroll_controller(ScrollConfig::default));
399 let mut dragging_content = use_state::<Option<CursorPoint>>(|| None);
400 let mut drag_origin = use_state::<Option<CursorPoint>>(|| None);
401 let (scrolled_x, scrolled_y) = scroll_controller.into();
402 let layout = &self.layout.layout;
403 let direction = layout.direction;
404 let drag_scrolling = self.drag_scrolling;
405
406 let viewport_width = size.read().area.width();
407 let viewport_height = size.read().area.height();
408
409 let (inner_width, inner_height) = match direction {
410 Direction::Vertical => (
411 size.read().inner_sizes.width,
412 self.item_size
413 .total_size(viewport_height, scrolled_y as f32, self.length),
414 ),
415 Direction::Horizontal => (
416 self.item_size
417 .total_size(viewport_width, scrolled_x as f32, self.length),
418 size.read().inner_sizes.height,
419 ),
420 };
421
422 scroll_controller.use_apply(inner_width, inner_height);
423
424 let corrected_scrolled_x =
425 get_corrected_scroll_position(inner_width, size.read().area.width(), scrolled_x as f32);
426
427 let corrected_scrolled_y = get_corrected_scroll_position(
428 inner_height,
429 size.read().area.height(),
430 scrolled_y as f32,
431 );
432 let horizontal_scrollbar_is_visible = !timeout.elapsed()
433 && is_scrollbar_visible(self.show_scrollbar, inner_width, size.read().area.width());
434 let vertical_scrollbar_is_visible = !timeout.elapsed()
435 && is_scrollbar_visible(self.show_scrollbar, inner_height, size.read().area.height());
436
437 let (scrollbar_x, scrollbar_width) =
438 get_scrollbar_pos_and_size(inner_width, size.read().area.width(), corrected_scrolled_x);
439 let (scrollbar_y, scrollbar_height) = get_scrollbar_pos_and_size(
440 inner_height,
441 size.read().area.height(),
442 corrected_scrolled_y,
443 );
444
445 let (container_width, content_width) = get_container_sizes(self.layout.width.clone());
446 let (container_height, content_height) = get_container_sizes(self.layout.height.clone());
447
448 let scroll_with_arrows = self.scroll_with_arrows;
449 let invert_scroll_wheel = self.invert_scroll_wheel;
450
451 let on_capture_global_pointer_press = move |e: Event<PointerEventData>| {
452 if clicking_scrollbar.read().is_some() {
453 e.prevent_default();
454 clicking_scrollbar.set(None);
455 }
456
457 if drag_scrolling && (dragging_content().is_some() || drag_origin().is_some()) {
458 dragging_content.set(None);
459 drag_origin.set(None);
460 }
461 };
462
463 let on_wheel = move |e: Event<WheelEventData>| {
464 let invert_direction = e.source == WheelSource::Device
466 && (*pressing_shift.read() || invert_scroll_wheel)
467 && (!*pressing_shift.read() || !invert_scroll_wheel);
468
469 let (x_movement, y_movement) = if invert_direction {
470 (e.delta_y as f32, e.delta_x as f32)
471 } else {
472 (e.delta_x as f32, e.delta_y as f32)
473 };
474
475 let scroll_position_y = get_scroll_position_from_wheel(
477 y_movement,
478 inner_height,
479 size.read().area.height(),
480 corrected_scrolled_y,
481 );
482 scroll_controller.scroll_to_y(scroll_position_y).then(|| {
483 e.stop_propagation();
484 });
485
486 let scroll_position_x = get_scroll_position_from_wheel(
488 x_movement,
489 inner_width,
490 size.read().area.width(),
491 corrected_scrolled_x,
492 );
493 scroll_controller.scroll_to_x(scroll_position_x).then(|| {
494 e.stop_propagation();
495 });
496 timeout.reset();
497 };
498
499 let on_mouse_move = move |_| {
500 timeout.reset();
501 };
502
503 let on_capture_global_pointer_move = move |e: Event<PointerEventData>| {
504 if drag_scrolling {
505 if let Some(prev) = dragging_content() {
506 let coords = e.global_location();
507 let delta = prev - coords;
508
509 scroll_controller.scroll_to_y((corrected_scrolled_y - delta.y as f32) as i32);
510 scroll_controller.scroll_to_x((corrected_scrolled_x - delta.x as f32) as i32);
511
512 dragging_content.set(Some(coords));
513 e.prevent_default();
514 timeout.reset();
515 a11y_id.request_focus();
516 return;
517 } else if let Some(origin) = drag_origin() {
518 let coords = e.global_location();
519 let distance = (origin - coords).abs();
520
521 const DRAG_THRESHOLD: f64 = 2.0;
524
525 if distance.x > DRAG_THRESHOLD || distance.y > DRAG_THRESHOLD {
526 let delta = origin - coords;
527
528 scroll_controller
529 .scroll_to_y((corrected_scrolled_y - delta.y as f32) as i32);
530 scroll_controller
531 .scroll_to_x((corrected_scrolled_x - delta.x as f32) as i32);
532
533 dragging_content.set(Some(coords));
534 e.prevent_default();
535 timeout.reset();
536 a11y_id.request_focus();
537 }
538 return;
539 }
540 }
541
542 let clicking_scrollbar = clicking_scrollbar.peek();
543
544 if let Some((Axis::Y, y)) = *clicking_scrollbar {
545 let coordinates = e.element_location();
546 let cursor_y = coordinates.y - y - size.read().area.min_y() as f64;
547
548 let scroll_position = get_scroll_position_from_cursor(
549 cursor_y as f32,
550 inner_height,
551 size.read().area.height(),
552 );
553
554 scroll_controller.scroll_to_y(scroll_position);
555 } else if let Some((Axis::X, x)) = *clicking_scrollbar {
556 let coordinates = e.element_location();
557 let cursor_x = coordinates.x - x - size.read().area.min_x() as f64;
558
559 let scroll_position = get_scroll_position_from_cursor(
560 cursor_x as f32,
561 inner_width,
562 size.read().area.width(),
563 );
564
565 scroll_controller.scroll_to_x(scroll_position);
566 }
567
568 if clicking_scrollbar.is_some() {
569 e.prevent_default();
570 timeout.reset();
571 a11y_id.request_focus();
572 }
573 };
574
575 let on_key_down = move |e: Event<KeyboardEventData>| {
576 if !scroll_with_arrows
577 && (e.key == Key::Named(NamedKey::ArrowUp)
578 || e.key == Key::Named(NamedKey::ArrowRight)
579 || e.key == Key::Named(NamedKey::ArrowDown)
580 || e.key == Key::Named(NamedKey::ArrowLeft))
581 {
582 return;
583 }
584 let x = corrected_scrolled_x;
585 let y = corrected_scrolled_y;
586 let inner_height = inner_height;
587 let inner_width = inner_width;
588 let viewport_height = size.read().area.height();
589 let viewport_width = size.read().area.width();
590 if let Some((x, y)) = handle_key_event(
591 &e.key,
592 (x, y),
593 inner_height,
594 inner_width,
595 viewport_height,
596 viewport_width,
597 direction,
598 ) {
599 scroll_controller.scroll_to_x(x as i32);
600 scroll_controller.scroll_to_y(y as i32);
601 e.stop_propagation();
602 timeout.reset();
603 }
604 };
605
606 let on_global_key_down = move |e: Event<KeyboardEventData>| {
607 let data = e;
608 if data.key == Key::Named(NamedKey::Shift) {
609 pressing_shift.set(true);
610 }
611 };
612
613 let on_global_key_up = move |e: Event<KeyboardEventData>| {
614 let data = e;
615 if data.key == Key::Named(NamedKey::Shift) {
616 pressing_shift.set(false);
617 }
618 };
619
620 let (viewport_size, scroll_position) = if direction == Direction::vertical() {
621 (viewport_height, corrected_scrolled_y)
622 } else {
623 (viewport_width, corrected_scrolled_x)
624 };
625
626 let (render_range, item_offset) =
627 self.item_size
628 .visible_range(viewport_size, scroll_position, self.length);
629
630 let children = render_range
631 .map(|i| {
632 let item = VirtualItem {
633 index: i,
634 size: self.item_size.at(i),
635 };
636 (self.builder)(item, &self.builder_data)
637 })
638 .collect::<Vec<Element>>();
639
640 let (offset_x, offset_y) = match direction {
641 Direction::Vertical => (corrected_scrolled_x, item_offset),
642 Direction::Horizontal => (item_offset, corrected_scrolled_y),
643 };
644
645 let on_pointer_down = move |e: Event<PointerEventData>| {
646 if drag_scrolling && matches!(e.data(), PointerEventData::Touch(_)) {
647 drag_origin.set(Some(e.global_location()));
648 }
649 };
650
651 rect()
652 .width(layout.width.clone())
653 .height(layout.height.clone())
654 .a11y_id(a11y_id)
655 .a11y_focusable(false)
656 .a11y_role(AccessibilityRole::ScrollView)
657 .a11y_builder(move |node| {
658 node.set_scroll_x(corrected_scrolled_x as f64);
659 node.set_scroll_y(corrected_scrolled_y as f64)
660 })
661 .scrollable(true)
662 .on_wheel(on_wheel)
663 .on_capture_global_pointer_press(on_capture_global_pointer_press)
664 .on_mouse_move(on_mouse_move)
665 .on_capture_global_pointer_move(on_capture_global_pointer_move)
666 .on_key_down(on_key_down)
667 .on_global_key_up(on_global_key_up)
668 .on_global_key_down(on_global_key_down)
669 .on_pointer_down(on_pointer_down)
670 .child(
671 rect()
672 .width(container_width)
673 .height(container_height)
674 .horizontal()
675 .child(
676 rect()
677 .direction(direction)
678 .width(content_width)
679 .height(content_height)
680 .offset_x(offset_x)
681 .offset_y(offset_y)
682 .overflow(Overflow::Clip)
683 .on_sized(move |e: Event<SizedEventData>| {
684 size.set_if_modified(e.clone())
685 })
686 .children(children),
687 )
688 .maybe_child(vertical_scrollbar_is_visible.then_some({
689 rect().child(ScrollBar {
690 theme: None,
691 clicking_scrollbar,
692 axis: Axis::Y,
693 offset: scrollbar_y,
694 size: Size::px(size.read().area.height()),
695 thumb: ScrollThumb {
696 theme: None,
697 clicking_scrollbar,
698 axis: Axis::Y,
699 size: scrollbar_height,
700 },
701 })
702 })),
703 )
704 .maybe_child(horizontal_scrollbar_is_visible.then_some({
705 rect().child(ScrollBar {
706 theme: None,
707 clicking_scrollbar,
708 axis: Axis::X,
709 offset: scrollbar_x,
710 size: Size::px(size.read().area.width()),
711 thumb: ScrollThumb {
712 theme: None,
713 clicking_scrollbar,
714 axis: Axis::X,
715 size: scrollbar_width,
716 },
717 })
718 }))
719 }
720
721 fn render_key(&self) -> DiffKey {
722 self.key.clone().or(self.default_key())
723 }
724}