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