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
236impl<D: PartialEq + 'static, B: Fn(usize, &D) -> Element + 'static> Component
237 for VirtualScrollView<D, B>
238{
239 fn render(self: &VirtualScrollView<D, B>) -> impl IntoElement {
240 let focus = use_focus();
241 let mut timeout = use_timeout(|| Duration::from_millis(800));
242 let mut pressing_shift = use_state(|| false);
243 let mut pressing_alt = use_state(|| false);
244 let mut clicking_scrollbar = use_state::<Option<(Axis, f64)>>(|| None);
245 let mut size = use_state(SizedEventData::default);
246 let mut scroll_controller = self
247 .scroll_controller
248 .unwrap_or_else(|| use_scroll_controller(ScrollConfig::default));
249 let (scrolled_x, scrolled_y) = scroll_controller.into();
250 let layout = &self.layout.layout;
251 let direction = layout.direction;
252
253 let (inner_width, inner_height) = match direction {
254 Direction::Vertical => (
255 size.read().inner_sizes.width,
256 self.item_size * self.length as f32,
257 ),
258 Direction::Horizontal => (
259 self.item_size * self.length as f32,
260 size.read().inner_sizes.height,
261 ),
262 };
263
264 scroll_controller.use_apply(inner_width, inner_height);
265
266 let corrected_scrolled_x =
267 get_corrected_scroll_position(inner_width, size.read().area.width(), scrolled_x as f32);
268
269 let corrected_scrolled_y = get_corrected_scroll_position(
270 inner_height,
271 size.read().area.height(),
272 scrolled_y as f32,
273 );
274 let horizontal_scrollbar_is_visible = !timeout.elapsed()
275 && is_scrollbar_visible(self.show_scrollbar, inner_width, size.read().area.width());
276 let vertical_scrollbar_is_visible = !timeout.elapsed()
277 && is_scrollbar_visible(self.show_scrollbar, inner_height, size.read().area.height());
278
279 let (scrollbar_x, scrollbar_width) =
280 get_scrollbar_pos_and_size(inner_width, size.read().area.width(), corrected_scrolled_x);
281 let (scrollbar_y, scrollbar_height) = get_scrollbar_pos_and_size(
282 inner_height,
283 size.read().area.height(),
284 corrected_scrolled_y,
285 );
286
287 let (container_width, content_width) = get_container_sizes(self.layout.width.clone());
288 let (container_height, content_height) = get_container_sizes(self.layout.height.clone());
289
290 let scroll_with_arrows = self.scroll_with_arrows;
291 let invert_scroll_wheel = self.invert_scroll_wheel;
292
293 let on_global_mouse_up = move |_| {
294 clicking_scrollbar.set_if_modified(None);
295 };
296
297 let on_wheel = move |e: Event<WheelEventData>| {
298 let invert_direction = e.source == WheelSource::Device
300 && (*pressing_shift.read() || invert_scroll_wheel)
301 && (!*pressing_shift.read() || !invert_scroll_wheel);
302
303 let (x_movement, y_movement) = if invert_direction {
304 (e.delta_y as f32, e.delta_x as f32)
305 } else {
306 (e.delta_x as f32, e.delta_y as f32)
307 };
308
309 let scroll_position_y = get_scroll_position_from_wheel(
311 y_movement,
312 inner_height,
313 size.read().area.height(),
314 corrected_scrolled_y,
315 );
316 scroll_controller.scroll_to_y(scroll_position_y).then(|| {
317 e.stop_propagation();
318 });
319
320 let scroll_position_x = get_scroll_position_from_wheel(
322 x_movement,
323 inner_width,
324 size.read().area.width(),
325 corrected_scrolled_x,
326 );
327 scroll_controller.scroll_to_x(scroll_position_x).then(|| {
328 e.stop_propagation();
329 });
330 timeout.reset();
331 };
332
333 let on_mouse_move = move |_| {
334 timeout.reset();
335 };
336
337 let on_capture_global_mouse_move = move |e: Event<MouseEventData>| {
338 let clicking_scrollbar = clicking_scrollbar.peek();
339
340 if let Some((Axis::Y, y)) = *clicking_scrollbar {
341 let coordinates = e.element_location;
342 let cursor_y = coordinates.y - y - size.read().area.min_y() as f64;
343
344 let scroll_position = get_scroll_position_from_cursor(
345 cursor_y as f32,
346 inner_height,
347 size.read().area.height(),
348 );
349
350 scroll_controller.scroll_to_y(scroll_position);
351 } else if let Some((Axis::X, x)) = *clicking_scrollbar {
352 let coordinates = e.element_location;
353 let cursor_x = coordinates.x - x - size.read().area.min_x() as f64;
354
355 let scroll_position = get_scroll_position_from_cursor(
356 cursor_x as f32,
357 inner_width,
358 size.read().area.width(),
359 );
360
361 scroll_controller.scroll_to_x(scroll_position);
362 }
363
364 if clicking_scrollbar.is_some() {
365 e.prevent_default();
366 timeout.reset();
367 if !focus.is_focused() {
368 focus.request_focus();
369 }
370 }
371 };
372
373 let on_key_down = move |e: Event<KeyboardEventData>| {
374 if !scroll_with_arrows
375 && (e.key == Key::Named(NamedKey::ArrowUp)
376 || e.key == Key::Named(NamedKey::ArrowRight)
377 || e.key == Key::Named(NamedKey::ArrowDown)
378 || e.key == Key::Named(NamedKey::ArrowLeft))
379 {
380 return;
381 }
382 let x = corrected_scrolled_x;
383 let y = corrected_scrolled_y;
384 let inner_height = inner_height;
385 let inner_width = inner_width;
386 let viewport_height = size.read().area.height();
387 let viewport_width = size.read().area.width();
388 if let Some((x, y)) = handle_key_event(
389 &e.key,
390 (x, y),
391 inner_height,
392 inner_width,
393 viewport_height,
394 viewport_width,
395 direction,
396 ) {
397 scroll_controller.scroll_to_x(x as i32);
398 scroll_controller.scroll_to_y(y as i32);
399 e.stop_propagation();
400 timeout.reset();
401 }
402 };
403
404 let on_global_key_down = move |e: Event<KeyboardEventData>| {
405 let data = e;
406 if data.key == Key::Named(NamedKey::Shift) {
407 pressing_shift.set(true);
408 } else if data.key == Key::Named(NamedKey::Alt) {
409 pressing_alt.set(true);
410 }
411 };
412
413 let on_global_key_up = move |e: Event<KeyboardEventData>| {
414 let data = e;
415 if data.key == Key::Named(NamedKey::Shift) {
416 pressing_shift.set(false);
417 } else if data.key == Key::Named(NamedKey::Alt) {
418 pressing_alt.set(false);
419 }
420 };
421
422 let (viewport_size, scroll_position) = if direction == Direction::vertical() {
423 (size.read().area.height(), corrected_scrolled_y)
424 } else {
425 (size.read().area.width(), corrected_scrolled_x)
426 };
427
428 let render_range = get_render_range(
429 viewport_size,
430 scroll_position,
431 self.item_size,
432 self.length as f32,
433 );
434
435 let children = render_range
436 .clone()
437 .map(|i| (self.builder)(i, &self.builder_data))
438 .collect::<Vec<Element>>();
439
440 let (offset_x, offset_y) = match direction {
441 Direction::Vertical => {
442 let offset_y_min =
443 (-corrected_scrolled_y / self.item_size).floor() * self.item_size;
444 let offset_y = -(-corrected_scrolled_y - offset_y_min);
445
446 (corrected_scrolled_x, offset_y)
447 }
448 Direction::Horizontal => {
449 let offset_x_min =
450 (-corrected_scrolled_x / self.item_size).floor() * self.item_size;
451 let offset_x = -(-corrected_scrolled_x - offset_x_min);
452
453 (offset_x, corrected_scrolled_y)
454 }
455 };
456
457 rect()
458 .width(layout.width.clone())
459 .height(layout.height.clone())
460 .a11y_id(focus.a11y_id())
461 .a11y_focusable(false)
462 .a11y_role(AccessibilityRole::ScrollView)
463 .a11y_builder(move |node| {
464 node.set_scroll_x(corrected_scrolled_x as f64);
465 node.set_scroll_y(corrected_scrolled_y as f64)
466 })
467 .scrollable(true)
468 .on_wheel(on_wheel)
469 .on_global_mouse_up(on_global_mouse_up)
470 .on_mouse_move(on_mouse_move)
471 .on_capture_global_mouse_move(on_capture_global_mouse_move)
472 .on_key_down(on_key_down)
473 .on_global_key_up(on_global_key_up)
474 .on_global_key_down(on_global_key_down)
475 .child(
476 rect()
477 .width(container_width)
478 .height(container_height)
479 .horizontal()
480 .child(
481 rect()
482 .direction(direction)
483 .width(content_width)
484 .height(content_height)
485 .offset_x(offset_x)
486 .offset_y(offset_y)
487 .overflow(Overflow::Clip)
488 .on_sized(move |e: Event<SizedEventData>| {
489 size.set_if_modified(e.clone())
490 })
491 .children(children),
492 )
493 .maybe_child(vertical_scrollbar_is_visible.then_some({
494 rect().child(ScrollBar {
495 theme: None,
496 clicking_scrollbar,
497 axis: Axis::Y,
498 offset: scrollbar_y,
499 thumb: ScrollThumb {
500 theme: None,
501 clicking_scrollbar,
502 axis: Axis::Y,
503 size: scrollbar_height,
504 },
505 })
506 })),
507 )
508 .maybe_child(horizontal_scrollbar_is_visible.then_some({
509 rect().child(ScrollBar {
510 theme: None,
511 clicking_scrollbar,
512 axis: Axis::X,
513 offset: scrollbar_x,
514 thumb: ScrollThumb {
515 theme: None,
516 clicking_scrollbar,
517 axis: Axis::X,
518 size: scrollbar_width,
519 },
520 })
521 }))
522 }
523
524 fn render_key(&self) -> DiffKey {
525 self.key.clone().or(self.default_key())
526 }
527}
528
529fn get_render_range(
530 viewport_size: f32,
531 scroll_position: f32,
532 item_size: f32,
533 item_length: f32,
534) -> Range<usize> {
535 let render_index_start = (-scroll_position) / item_size;
536 let potentially_visible_length = (viewport_size / item_size) + 1.0;
537 let remaining_length = item_length - render_index_start;
538
539 let render_index_end = if remaining_length <= potentially_visible_length {
540 item_length
541 } else {
542 render_index_start + potentially_visible_length
543 };
544
545 render_index_start as usize..(render_index_end as usize)
546}