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#[cfg_attr(feature = "docs",
75 doc = embed_doc_image::embed_image!("virtual_scrollview", "images/gallery_virtual_scrollview.png")
76)]
77#[derive(Clone)]
78pub struct VirtualScrollView<D, B: Fn(usize, &D) -> Element> {
79 builder: B,
80 builder_data: D,
81 item_size: f32,
82 length: usize,
83 layout: LayoutData,
84 show_scrollbar: bool,
85 scroll_with_arrows: bool,
86 scroll_controller: Option<ScrollController>,
87 invert_scroll_wheel: bool,
88 drag_scrolling: bool,
89 key: DiffKey,
90}
91
92impl<D: PartialEq, B: Fn(usize, &D) -> Element> LayoutExt for VirtualScrollView<D, B> {
93 fn get_layout(&mut self) -> &mut LayoutData {
94 &mut self.layout
95 }
96}
97
98impl<D: PartialEq, B: Fn(usize, &D) -> Element> ContainerSizeExt for VirtualScrollView<D, B> {}
99
100impl<D: PartialEq, B: Fn(usize, &D) -> Element> KeyExt for VirtualScrollView<D, B> {
101 fn write_key(&mut self) -> &mut DiffKey {
102 &mut self.key
103 }
104}
105
106impl<D: PartialEq, B: Fn(usize, &D) -> Element> PartialEq for VirtualScrollView<D, B> {
107 fn eq(&self, other: &Self) -> bool {
108 self.builder_data == other.builder_data
109 && self.item_size == other.item_size
110 && self.length == other.length
111 && self.layout == other.layout
112 && self.show_scrollbar == other.show_scrollbar
113 && self.scroll_with_arrows == other.scroll_with_arrows
114 && self.scroll_controller == other.scroll_controller
115 && self.invert_scroll_wheel == other.invert_scroll_wheel
116 }
117}
118
119impl<B: Fn(usize, &()) -> Element> VirtualScrollView<(), B> {
120 pub fn new(builder: B) -> Self {
122 Self {
123 builder,
124 builder_data: (),
125 item_size: 0.,
126 length: 0,
127 layout: {
128 let mut l = LayoutData::default();
129 l.layout.width = Size::fill();
130 l.layout.height = Size::fill();
131 l
132 },
133 show_scrollbar: true,
134 scroll_with_arrows: true,
135 scroll_controller: None,
136 invert_scroll_wheel: false,
137 drag_scrolling: true,
138 key: DiffKey::None,
139 }
140 }
141
142 pub fn new_controlled(builder: B, scroll_controller: ScrollController) -> Self {
144 Self {
145 builder,
146 builder_data: (),
147 item_size: 0.,
148 length: 0,
149 layout: {
150 let mut l = LayoutData::default();
151 l.layout.width = Size::fill();
152 l.layout.height = Size::fill();
153 l
154 },
155 show_scrollbar: true,
156 scroll_with_arrows: true,
157 scroll_controller: Some(scroll_controller),
158 invert_scroll_wheel: false,
159 drag_scrolling: true,
160 key: DiffKey::None,
161 }
162 }
163}
164
165impl<D, B: Fn(usize, &D) -> Element> VirtualScrollView<D, B> {
166 pub fn new_with_data(builder_data: D, builder: B) -> Self {
190 Self {
191 builder,
192 builder_data,
193 item_size: 0.,
194 length: 0,
195 layout: Node {
196 width: Size::fill(),
197 height: Size::fill(),
198 ..Default::default()
199 }
200 .into(),
201 show_scrollbar: true,
202 scroll_with_arrows: true,
203 scroll_controller: None,
204 invert_scroll_wheel: false,
205 drag_scrolling: true,
206 key: DiffKey::None,
207 }
208 }
209
210 pub fn new_with_data_controlled(
212 builder_data: D,
213 builder: B,
214 scroll_controller: ScrollController,
215 ) -> Self {
216 Self {
217 builder,
218 builder_data,
219 item_size: 0.,
220 length: 0,
221
222 layout: Node {
223 width: Size::fill(),
224 height: Size::fill(),
225 ..Default::default()
226 }
227 .into(),
228 show_scrollbar: true,
229 scroll_with_arrows: true,
230 scroll_controller: Some(scroll_controller),
231 invert_scroll_wheel: false,
232 drag_scrolling: true,
233 key: DiffKey::None,
234 }
235 }
236
237 pub fn show_scrollbar(mut self, show_scrollbar: bool) -> Self {
239 self.show_scrollbar = show_scrollbar;
240 self
241 }
242
243 pub fn direction(mut self, direction: Direction) -> Self {
245 self.layout.direction = direction;
246 self
247 }
248
249 pub fn scroll_with_arrows(mut self, scroll_with_arrows: impl Into<bool>) -> Self {
251 self.scroll_with_arrows = scroll_with_arrows.into();
252 self
253 }
254
255 pub fn item_size(mut self, item_size: impl Into<f32>) -> Self {
257 self.item_size = item_size.into();
258 self
259 }
260
261 pub fn length(mut self, length: impl Into<usize>) -> Self {
263 self.length = length.into();
264 self
265 }
266
267 pub fn invert_scroll_wheel(mut self, invert_scroll_wheel: impl Into<bool>) -> Self {
269 self.invert_scroll_wheel = invert_scroll_wheel.into();
270 self
271 }
272
273 pub fn drag_scrolling(mut self, drag_scrolling: bool) -> Self {
275 self.drag_scrolling = drag_scrolling;
276 self
277 }
278
279 pub fn scroll_controller(
281 mut self,
282 scroll_controller: impl Into<Option<ScrollController>>,
283 ) -> Self {
284 self.scroll_controller = scroll_controller.into();
285 self
286 }
287
288 pub fn max_width(mut self, max_width: impl Into<Size>) -> Self {
290 self.layout.maximum_width = max_width.into();
291 self
292 }
293
294 pub fn max_height(mut self, max_height: impl Into<Size>) -> Self {
296 self.layout.maximum_height = max_height.into();
297 self
298 }
299}
300
301impl<D: PartialEq + 'static, B: Fn(usize, &D) -> Element + 'static> Component
302 for VirtualScrollView<D, B>
303{
304 fn render(self: &VirtualScrollView<D, B>) -> impl IntoElement {
305 let a11y_id = use_a11y();
306 let mut timeout = use_timeout(|| Duration::from_millis(800));
307 let mut pressing_shift = use_state(|| false);
308 let mut clicking_scrollbar = use_state::<Option<(Axis, f64)>>(|| None);
309 let mut size = use_state(SizedEventData::default);
310 let mut scroll_controller = self
311 .scroll_controller
312 .unwrap_or_else(|| use_scroll_controller(ScrollConfig::default));
313 let mut dragging_content = use_state::<Option<CursorPoint>>(|| None);
314 let mut drag_origin = use_state::<Option<CursorPoint>>(|| None);
315 let (scrolled_x, scrolled_y) = scroll_controller.into();
316 let layout = &self.layout.layout;
317 let direction = layout.direction;
318 let drag_scrolling = self.drag_scrolling;
319
320 let (inner_width, inner_height) = match direction {
321 Direction::Vertical => (
322 size.read().inner_sizes.width,
323 self.item_size * self.length as f32,
324 ),
325 Direction::Horizontal => (
326 self.item_size * self.length as f32,
327 size.read().inner_sizes.height,
328 ),
329 };
330
331 scroll_controller.use_apply(inner_width, inner_height);
332
333 let corrected_scrolled_x =
334 get_corrected_scroll_position(inner_width, size.read().area.width(), scrolled_x as f32);
335
336 let corrected_scrolled_y = get_corrected_scroll_position(
337 inner_height,
338 size.read().area.height(),
339 scrolled_y as f32,
340 );
341 let horizontal_scrollbar_is_visible = !timeout.elapsed()
342 && is_scrollbar_visible(self.show_scrollbar, inner_width, size.read().area.width());
343 let vertical_scrollbar_is_visible = !timeout.elapsed()
344 && is_scrollbar_visible(self.show_scrollbar, inner_height, size.read().area.height());
345
346 let (scrollbar_x, scrollbar_width) =
347 get_scrollbar_pos_and_size(inner_width, size.read().area.width(), corrected_scrolled_x);
348 let (scrollbar_y, scrollbar_height) = get_scrollbar_pos_and_size(
349 inner_height,
350 size.read().area.height(),
351 corrected_scrolled_y,
352 );
353
354 let (container_width, content_width) = get_container_sizes(self.layout.width.clone());
355 let (container_height, content_height) = get_container_sizes(self.layout.height.clone());
356
357 let scroll_with_arrows = self.scroll_with_arrows;
358 let invert_scroll_wheel = self.invert_scroll_wheel;
359
360 let on_capture_global_pointer_press = move |e: Event<PointerEventData>| {
361 if clicking_scrollbar.read().is_some() {
362 e.prevent_default();
363 clicking_scrollbar.set(None);
364 }
365
366 if drag_scrolling && (dragging_content().is_some() || drag_origin().is_some()) {
367 dragging_content.set(None);
368 drag_origin.set(None);
369 }
370 };
371
372 let on_wheel = move |e: Event<WheelEventData>| {
373 let invert_direction = e.source == WheelSource::Device
375 && (*pressing_shift.read() || invert_scroll_wheel)
376 && (!*pressing_shift.read() || !invert_scroll_wheel);
377
378 let (x_movement, y_movement) = if invert_direction {
379 (e.delta_y as f32, e.delta_x as f32)
380 } else {
381 (e.delta_x as f32, e.delta_y as f32)
382 };
383
384 let scroll_position_y = get_scroll_position_from_wheel(
386 y_movement,
387 inner_height,
388 size.read().area.height(),
389 corrected_scrolled_y,
390 );
391 scroll_controller.scroll_to_y(scroll_position_y).then(|| {
392 e.stop_propagation();
393 });
394
395 let scroll_position_x = get_scroll_position_from_wheel(
397 x_movement,
398 inner_width,
399 size.read().area.width(),
400 corrected_scrolled_x,
401 );
402 scroll_controller.scroll_to_x(scroll_position_x).then(|| {
403 e.stop_propagation();
404 });
405 timeout.reset();
406 };
407
408 let on_mouse_move = move |_| {
409 timeout.reset();
410 };
411
412 let on_capture_global_pointer_move = move |e: Event<PointerEventData>| {
413 if drag_scrolling {
414 if let Some(prev) = dragging_content() {
415 let coords = e.global_location();
416 let delta = prev - coords;
417
418 scroll_controller.scroll_to_y((corrected_scrolled_y - delta.y as f32) as i32);
419 scroll_controller.scroll_to_x((corrected_scrolled_x - delta.x as f32) as i32);
420
421 dragging_content.set(Some(coords));
422 e.prevent_default();
423 timeout.reset();
424 a11y_id.request_focus();
425 return;
426 } else if let Some(origin) = drag_origin() {
427 let coords = e.global_location();
428 let distance = (origin - coords).abs();
429
430 const DRAG_THRESHOLD: f64 = 2.0;
433
434 if distance.x > DRAG_THRESHOLD || distance.y > DRAG_THRESHOLD {
435 let delta = origin - coords;
436
437 scroll_controller
438 .scroll_to_y((corrected_scrolled_y - delta.y as f32) as i32);
439 scroll_controller
440 .scroll_to_x((corrected_scrolled_x - delta.x as f32) as i32);
441
442 dragging_content.set(Some(coords));
443 e.prevent_default();
444 timeout.reset();
445 a11y_id.request_focus();
446 }
447 return;
448 }
449 }
450
451 let clicking_scrollbar = clicking_scrollbar.peek();
452
453 if let Some((Axis::Y, y)) = *clicking_scrollbar {
454 let coordinates = e.element_location();
455 let cursor_y = coordinates.y - y - size.read().area.min_y() as f64;
456
457 let scroll_position = get_scroll_position_from_cursor(
458 cursor_y as f32,
459 inner_height,
460 size.read().area.height(),
461 );
462
463 scroll_controller.scroll_to_y(scroll_position);
464 } else if let Some((Axis::X, x)) = *clicking_scrollbar {
465 let coordinates = e.element_location();
466 let cursor_x = coordinates.x - x - size.read().area.min_x() as f64;
467
468 let scroll_position = get_scroll_position_from_cursor(
469 cursor_x as f32,
470 inner_width,
471 size.read().area.width(),
472 );
473
474 scroll_controller.scroll_to_x(scroll_position);
475 }
476
477 if clicking_scrollbar.is_some() {
478 e.prevent_default();
479 timeout.reset();
480 a11y_id.request_focus();
481 }
482 };
483
484 let on_key_down = move |e: Event<KeyboardEventData>| {
485 if !scroll_with_arrows
486 && (e.key == Key::Named(NamedKey::ArrowUp)
487 || e.key == Key::Named(NamedKey::ArrowRight)
488 || e.key == Key::Named(NamedKey::ArrowDown)
489 || e.key == Key::Named(NamedKey::ArrowLeft))
490 {
491 return;
492 }
493 let x = corrected_scrolled_x;
494 let y = corrected_scrolled_y;
495 let inner_height = inner_height;
496 let inner_width = inner_width;
497 let viewport_height = size.read().area.height();
498 let viewport_width = size.read().area.width();
499 if let Some((x, y)) = handle_key_event(
500 &e.key,
501 (x, y),
502 inner_height,
503 inner_width,
504 viewport_height,
505 viewport_width,
506 direction,
507 ) {
508 scroll_controller.scroll_to_x(x as i32);
509 scroll_controller.scroll_to_y(y as i32);
510 e.stop_propagation();
511 timeout.reset();
512 }
513 };
514
515 let on_global_key_down = move |e: Event<KeyboardEventData>| {
516 let data = e;
517 if data.key == Key::Named(NamedKey::Shift) {
518 pressing_shift.set(true);
519 }
520 };
521
522 let on_global_key_up = move |e: Event<KeyboardEventData>| {
523 let data = e;
524 if data.key == Key::Named(NamedKey::Shift) {
525 pressing_shift.set(false);
526 }
527 };
528
529 let (viewport_size, scroll_position) = if direction == Direction::vertical() {
530 (size.read().area.height(), corrected_scrolled_y)
531 } else {
532 (size.read().area.width(), corrected_scrolled_x)
533 };
534
535 let render_range = get_render_range(
536 viewport_size,
537 scroll_position,
538 self.item_size,
539 self.length as f32,
540 );
541
542 let children = render_range
543 .map(|i| (self.builder)(i, &self.builder_data))
544 .collect::<Vec<Element>>();
545
546 let (offset_x, offset_y) = match direction {
547 Direction::Vertical => {
548 let offset_y_min =
549 (-corrected_scrolled_y / self.item_size).floor() * self.item_size;
550 let offset_y = -(-corrected_scrolled_y - offset_y_min);
551
552 (corrected_scrolled_x, offset_y)
553 }
554 Direction::Horizontal => {
555 let offset_x_min =
556 (-corrected_scrolled_x / self.item_size).floor() * self.item_size;
557 let offset_x = -(-corrected_scrolled_x - offset_x_min);
558
559 (offset_x, corrected_scrolled_y)
560 }
561 };
562
563 let on_pointer_down = move |e: Event<PointerEventData>| {
564 if drag_scrolling && matches!(e.data(), PointerEventData::Touch(_)) {
565 drag_origin.set(Some(e.global_location()));
566 }
567 };
568
569 rect()
570 .width(layout.width.clone())
571 .height(layout.height.clone())
572 .a11y_id(a11y_id)
573 .a11y_focusable(false)
574 .a11y_role(AccessibilityRole::ScrollView)
575 .a11y_builder(move |node| {
576 node.set_scroll_x(corrected_scrolled_x as f64);
577 node.set_scroll_y(corrected_scrolled_y as f64)
578 })
579 .scrollable(true)
580 .on_wheel(on_wheel)
581 .on_capture_global_pointer_press(on_capture_global_pointer_press)
582 .on_mouse_move(on_mouse_move)
583 .on_capture_global_pointer_move(on_capture_global_pointer_move)
584 .on_key_down(on_key_down)
585 .on_global_key_up(on_global_key_up)
586 .on_global_key_down(on_global_key_down)
587 .on_pointer_down(on_pointer_down)
588 .child(
589 rect()
590 .width(container_width)
591 .height(container_height)
592 .horizontal()
593 .child(
594 rect()
595 .direction(direction)
596 .width(content_width)
597 .height(content_height)
598 .offset_x(offset_x)
599 .offset_y(offset_y)
600 .overflow(Overflow::Clip)
601 .on_sized(move |e: Event<SizedEventData>| {
602 size.set_if_modified(e.clone())
603 })
604 .children(children),
605 )
606 .maybe_child(vertical_scrollbar_is_visible.then_some({
607 rect().child(ScrollBar {
608 theme: None,
609 clicking_scrollbar,
610 axis: Axis::Y,
611 offset: scrollbar_y,
612 size: Size::px(size.read().area.height()),
613 thumb: ScrollThumb {
614 theme: None,
615 clicking_scrollbar,
616 axis: Axis::Y,
617 size: scrollbar_height,
618 },
619 })
620 })),
621 )
622 .maybe_child(horizontal_scrollbar_is_visible.then_some({
623 rect().child(ScrollBar {
624 theme: None,
625 clicking_scrollbar,
626 axis: Axis::X,
627 offset: scrollbar_x,
628 size: Size::px(size.read().area.width()),
629 thumb: ScrollThumb {
630 theme: None,
631 clicking_scrollbar,
632 axis: Axis::X,
633 size: scrollbar_width,
634 },
635 })
636 }))
637 }
638
639 fn render_key(&self) -> DiffKey {
640 self.key.clone().or(self.default_key())
641 }
642}
643
644fn get_render_range(
645 viewport_size: f32,
646 scroll_position: f32,
647 item_size: f32,
648 item_length: f32,
649) -> Range<usize> {
650 let render_index_start = (-scroll_position) / item_size;
651 let potentially_visible_length = (viewport_size / item_size) + 1.0;
652 let remaining_length = item_length - render_index_start;
653
654 let render_index_end = if remaining_length <= potentially_visible_length {
655 item_length
656 } else {
657 render_index_start + potentially_visible_length
658 };
659
660 render_index_start as usize..(render_index_end as usize)
661}