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