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