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