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 key: DiffKey,
78}
79
80impl ChildrenExt for ScrollView {
81 fn get_children(&mut self) -> &mut Vec<Element> {
82 &mut self.children
83 }
84}
85
86impl KeyExt for ScrollView {
87 fn write_key(&mut self) -> &mut DiffKey {
88 &mut self.key
89 }
90}
91
92impl Default for ScrollView {
93 fn default() -> Self {
94 Self {
95 children: Vec::default(),
96 layout: Node {
97 width: Size::fill(),
98 height: Size::fill(),
99 ..Default::default()
100 }
101 .into(),
102 show_scrollbar: true,
103 scroll_with_arrows: true,
104 scroll_controller: None,
105 invert_scroll_wheel: false,
106 drag_scrolling: true,
107 key: DiffKey::None,
108 }
109 }
110}
111
112impl ScrollView {
113 pub fn new() -> Self {
115 Self::default()
116 }
117
118 pub fn new_controlled(scroll_controller: ScrollController) -> Self {
120 Self {
121 scroll_controller: Some(scroll_controller),
122 ..Default::default()
123 }
124 }
125
126 pub fn show_scrollbar(mut self, show_scrollbar: bool) -> Self {
128 self.show_scrollbar = show_scrollbar;
129 self
130 }
131
132 pub fn direction(mut self, direction: Direction) -> Self {
134 self.layout.direction = direction;
135 self
136 }
137
138 pub fn spacing(mut self, spacing: impl Into<f32>) -> Self {
140 self.layout.spacing = Length::new(spacing.into());
141 self
142 }
143
144 pub fn scroll_with_arrows(mut self, scroll_with_arrows: impl Into<bool>) -> Self {
146 self.scroll_with_arrows = scroll_with_arrows.into();
147 self
148 }
149
150 pub fn invert_scroll_wheel(mut self, invert_scroll_wheel: impl Into<bool>) -> Self {
152 self.invert_scroll_wheel = invert_scroll_wheel.into();
153 self
154 }
155
156 pub fn drag_scrolling(mut self, drag_scrolling: bool) -> Self {
158 self.drag_scrolling = drag_scrolling;
159 self
160 }
161
162 pub fn max_width(mut self, max_width: impl Into<Size>) -> Self {
164 self.layout.maximum_width = max_width.into();
165 self
166 }
167
168 pub fn max_height(mut self, max_height: impl Into<Size>) -> Self {
170 self.layout.maximum_height = max_height.into();
171 self
172 }
173}
174
175impl LayoutExt for ScrollView {
176 fn get_layout(&mut self) -> &mut LayoutData {
177 &mut self.layout
178 }
179}
180
181impl ContainerSizeExt for ScrollView {}
182impl ContainerPositionExt for ScrollView {}
183
184impl Component for ScrollView {
185 fn render(self: &ScrollView) -> impl IntoElement {
186 let a11y_id = use_a11y();
187 let mut timeout = use_timeout(|| Duration::from_millis(800));
188 let mut pressing_shift = use_state(|| false);
189 let mut clicking_scrollbar = use_state::<Option<(Axis, f64)>>(|| None);
190 let mut size = use_state(SizedEventData::default);
191 let mut scroll_controller = self
192 .scroll_controller
193 .unwrap_or_else(|| use_scroll_controller(ScrollConfig::default));
194 let mut dragging_content = use_state::<Option<CursorPoint>>(|| None);
195 let mut drag_origin = use_state::<Option<CursorPoint>>(|| None);
196 let (scrolled_x, scrolled_y) = scroll_controller.into();
197 let layout = &self.layout.layout;
198 let direction = layout.direction;
199 let drag_scrolling = self.drag_scrolling;
200
201 scroll_controller.use_apply(
202 size.read().inner_sizes.width,
203 size.read().inner_sizes.height,
204 );
205
206 let corrected_scrolled_x = get_corrected_scroll_position(
207 size.read().inner_sizes.width,
208 size.read().area.width(),
209 scrolled_x as f32,
210 );
211
212 let corrected_scrolled_y = get_corrected_scroll_position(
213 size.read().inner_sizes.height,
214 size.read().area.height(),
215 scrolled_y as f32,
216 );
217 let horizontal_scrollbar_is_visible = !timeout.elapsed()
218 && is_scrollbar_visible(
219 self.show_scrollbar,
220 size.read().inner_sizes.width,
221 size.read().area.width(),
222 );
223 let vertical_scrollbar_is_visible = !timeout.elapsed()
224 && is_scrollbar_visible(
225 self.show_scrollbar,
226 size.read().inner_sizes.height,
227 size.read().area.height(),
228 );
229
230 let (scrollbar_x, scrollbar_width) = get_scrollbar_pos_and_size(
231 size.read().inner_sizes.width,
232 size.read().area.width(),
233 corrected_scrolled_x,
234 );
235 let (scrollbar_y, scrollbar_height) = get_scrollbar_pos_and_size(
236 size.read().inner_sizes.height,
237 size.read().area.height(),
238 corrected_scrolled_y,
239 );
240
241 let (container_width, content_width) = get_container_sizes(layout.width.clone());
242 let (container_height, content_height) = get_container_sizes(layout.height.clone());
243
244 let scroll_with_arrows = self.scroll_with_arrows;
245 let invert_scroll_wheel = self.invert_scroll_wheel;
246
247 let on_capture_global_pointer_press = move |e: Event<PointerEventData>| {
248 if clicking_scrollbar.read().is_some() {
249 e.prevent_default();
250 clicking_scrollbar.set(None);
251 }
252
253 if drag_scrolling && (dragging_content().is_some() || drag_origin().is_some()) {
254 dragging_content.set(None);
255 drag_origin.set(None);
256 }
257 };
258
259 let on_wheel = move |e: Event<WheelEventData>| {
260 let invert_direction = e.source == WheelSource::Device
262 && (*pressing_shift.read() || invert_scroll_wheel)
263 && (!*pressing_shift.read() || !invert_scroll_wheel);
264
265 let (x_movement, y_movement) = if invert_direction {
266 (e.delta_y as f32, e.delta_x as f32)
267 } else {
268 (e.delta_x as f32, e.delta_y as f32)
269 };
270
271 let scroll_position_y = get_scroll_position_from_wheel(
273 y_movement,
274 size.read().inner_sizes.height,
275 size.read().area.height(),
276 corrected_scrolled_y,
277 );
278 scroll_controller.scroll_to_y(scroll_position_y).then(|| {
279 e.stop_propagation();
280 });
281
282 let scroll_position_x = get_scroll_position_from_wheel(
284 x_movement,
285 size.read().inner_sizes.width,
286 size.read().area.width(),
287 corrected_scrolled_x,
288 );
289 scroll_controller.scroll_to_x(scroll_position_x).then(|| {
290 e.stop_propagation();
291 });
292 timeout.reset();
293 };
294
295 let on_mouse_move = move |_| {
296 timeout.reset();
297 };
298
299 let on_capture_global_pointer_move = move |e: Event<PointerEventData>| {
300 if drag_scrolling {
301 if let Some(prev) = dragging_content() {
302 let coords = e.global_location();
303 let delta = prev - coords;
304
305 scroll_controller.scroll_to_y((corrected_scrolled_y - delta.y as f32) as i32);
306 scroll_controller.scroll_to_x((corrected_scrolled_x - delta.x as f32) as i32);
307
308 dragging_content.set(Some(coords));
309 e.prevent_default();
310 timeout.reset();
311 a11y_id.request_focus();
312 return;
313 } else if let Some(origin) = drag_origin() {
314 let coords = e.global_location();
315 let distance = (origin - coords).abs();
316
317 const DRAG_THRESHOLD: f64 = 2.0;
320
321 if distance.x > DRAG_THRESHOLD || distance.y > DRAG_THRESHOLD {
322 let delta = origin - coords;
323
324 scroll_controller
325 .scroll_to_y((corrected_scrolled_y - delta.y as f32) as i32);
326 scroll_controller
327 .scroll_to_x((corrected_scrolled_x - delta.x as f32) as i32);
328
329 dragging_content.set(Some(coords));
330 e.prevent_default();
331 timeout.reset();
332 a11y_id.request_focus();
333 }
334 return;
335 }
336 }
337
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 size.read().inner_sizes.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 size.read().inner_sizes.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 a11y_id.request_focus();
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 = size.read().inner_sizes.height;
383 let inner_width = size.read().inner_sizes.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 }
407 };
408
409 let on_global_key_up = move |e: Event<KeyboardEventData>| {
410 let data = e;
411 if data.key == Key::Named(NamedKey::Shift) {
412 pressing_shift.set(false);
413 }
414 };
415
416 let on_pointer_down = move |e: Event<PointerEventData>| {
417 if drag_scrolling && matches!(e.data(), PointerEventData::Touch(_)) {
418 drag_origin.set(Some(e.global_location()));
419 }
420 };
421
422 rect()
423 .width(layout.width.clone())
424 .height(layout.height.clone())
425 .max_width(layout.maximum_width.clone())
426 .max_height(layout.maximum_height.clone())
427 .a11y_id(a11y_id)
428 .a11y_focusable(false)
429 .a11y_role(AccessibilityRole::ScrollView)
430 .a11y_builder(move |node| {
431 node.set_scroll_x(corrected_scrolled_x as f64);
432 node.set_scroll_y(corrected_scrolled_y as f64)
433 })
434 .scrollable(true)
435 .on_wheel(on_wheel)
436 .on_capture_global_pointer_press(on_capture_global_pointer_press)
437 .on_mouse_move(on_mouse_move)
438 .on_capture_global_pointer_move(on_capture_global_pointer_move)
439 .on_key_down(on_key_down)
440 .on_global_key_up(on_global_key_up)
441 .on_global_key_down(on_global_key_down)
442 .on_pointer_down(on_pointer_down)
443 .child(
444 rect()
445 .width(container_width)
446 .height(container_height)
447 .horizontal()
448 .child(
449 rect()
450 .direction(direction)
451 .width(content_width)
452 .height(content_height)
453 .max_width(layout.maximum_width.clone())
454 .max_height(layout.maximum_height.clone())
455 .offset_x(corrected_scrolled_x)
456 .offset_y(corrected_scrolled_y)
457 .spacing(layout.spacing.get())
458 .overflow(Overflow::Clip)
459 .on_sized(move |e: Event<SizedEventData>| {
460 size.set_if_modified(e.clone())
461 })
462 .children(self.children.clone()),
463 )
464 .maybe_child(vertical_scrollbar_is_visible.then_some({
465 rect().child(ScrollBar {
466 theme: None,
467 clicking_scrollbar,
468 axis: Axis::Y,
469 offset: scrollbar_y,
470 size: Size::px(size.read().area.height()),
471 thumb: ScrollThumb {
472 theme: None,
473 clicking_scrollbar,
474 axis: Axis::Y,
475 size: scrollbar_height,
476 },
477 })
478 })),
479 )
480 .maybe_child(horizontal_scrollbar_is_visible.then_some({
481 rect().child(ScrollBar {
482 theme: None,
483 clicking_scrollbar,
484 axis: Axis::X,
485 offset: scrollbar_x,
486 size: Size::px(size.read().area.width()),
487 thumb: ScrollThumb {
488 theme: None,
489 clicking_scrollbar,
490 axis: Axis::X,
491 size: scrollbar_width,
492 },
493 })
494 }))
495 }
496
497 fn render_key(&self) -> DiffKey {
498 self.key.clone().or(self.default_key())
499 }
500}