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