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