1use dioxus::prelude::*;
2use freya_elements::{
3 self as dioxus_elements,
4 events::{
5 keyboard::Key,
6 KeyboardEvent,
7 MouseEvent,
8 WheelEvent,
9 },
10};
11use freya_hooks::{
12 use_applied_theme,
13 use_focus,
14 use_node_from_signal,
15 ScrollBarThemeWith,
16};
17
18use super::use_scroll_controller::ScrollController;
19use crate::{
20 get_container_sizes,
21 get_corrected_scroll_position,
22 get_scroll_position_from_cursor,
23 get_scroll_position_from_wheel,
24 get_scrollbar_pos_and_size,
25 is_scrollbar_visible,
26 manage_key_event,
27 scroll_views::use_scroll_controller::{
28 use_scroll_controller,
29 ScrollConfig,
30 },
31 Axis,
32 ScrollBar,
33 ScrollThumb,
34 SCROLL_SPEED_MULTIPLIER,
35};
36
37#[derive(Props, Clone, PartialEq)]
39pub struct ScrollViewProps {
40 #[props(default = "fill".into())]
42 pub width: String,
43 #[props(default = "fill".into())]
45 pub height: String,
46 pub min_width: Option<f32>,
48 pub min_height: Option<f32>,
50 pub max_width: Option<f32>,
52 pub max_height: Option<f32>,
54 #[props(default = "0".to_string())]
56 pub padding: String,
57 #[props(default = "0".to_string())]
59 pub spacing: String,
60 pub scrollbar_theme: Option<ScrollBarThemeWith>,
62 pub children: Element,
64 #[props(default = "vertical".to_string(), into)]
66 pub direction: String,
67 #[props(default = true, into)]
69 pub show_scrollbar: bool,
70 #[props(default = true, into)]
72 pub scroll_with_arrows: bool,
73 pub scroll_controller: Option<ScrollController>,
75 #[props(default = false)]
78 pub invert_scroll_wheel: bool,
79}
80
81#[cfg_attr(feature = "docs",
155 doc = embed_doc_image::embed_image!("scroll_view", "images/gallery_scroll_view.png")
156)]
157#[allow(non_snake_case)]
158pub fn ScrollView(
159 ScrollViewProps {
160 width,
161 height,
162 min_width,
163 min_height,
164 max_width,
165 max_height,
166 padding,
167 spacing,
168 scrollbar_theme,
169 children,
170 direction,
171 show_scrollbar,
172 scroll_with_arrows,
173 scroll_controller,
174 invert_scroll_wheel,
175 }: ScrollViewProps,
176) -> Element {
177 let mut clicking_scrollbar = use_signal::<Option<(Axis, f64)>>(|| None);
178 let mut clicking_shift = use_signal(|| false);
179 let mut clicking_alt = use_signal(|| false);
180 let mut scroll_controller =
181 scroll_controller.unwrap_or_else(|| use_scroll_controller(ScrollConfig::default));
182 let (mut scrolled_x, mut scrolled_y) = scroll_controller.into();
183 let (node_ref, size) = use_node_from_signal(|| scroll_controller.layout());
184
185 let mut focus = use_focus();
186 let applied_scrollbar_theme = use_applied_theme!(&scrollbar_theme, scroll_bar);
187
188 scroll_controller.use_apply(size.inner.width, size.inner.height);
189
190 let vertical_scrollbar_is_visible = is_scrollbar_visible(
191 show_scrollbar,
192 size.inner.height.floor(),
193 size.area.height().floor(),
194 );
195 let horizontal_scrollbar_is_visible = is_scrollbar_visible(
196 show_scrollbar,
197 size.inner.width.floor(),
198 size.area.width().floor(),
199 );
200
201 let (container_width, content_width) = get_container_sizes(&width);
202 let (container_height, content_height) = get_container_sizes(&height);
203
204 let corrected_scrolled_y = get_corrected_scroll_position(
205 size.inner.height,
206 size.area.height(),
207 *scrolled_y.read() as f32,
208 );
209 let corrected_scrolled_x = get_corrected_scroll_position(
210 size.inner.width,
211 size.area.width(),
212 *scrolled_x.read() as f32,
213 );
214
215 let (scrollbar_y, scrollbar_height) =
216 get_scrollbar_pos_and_size(size.inner.height, size.area.height(), corrected_scrolled_y);
217 let (scrollbar_x, scrollbar_width) =
218 get_scrollbar_pos_and_size(size.inner.width, size.area.width(), corrected_scrolled_x);
219
220 let onwheel = move |e: WheelEvent| {
222 let speed_multiplier = if *clicking_alt.peek() {
223 SCROLL_SPEED_MULTIPLIER
224 } else {
225 1.0
226 };
227
228 let invert_direction = (clicking_shift() || invert_scroll_wheel)
229 && (!clicking_shift() || !invert_scroll_wheel);
230
231 let (x_movement, y_movement) = if invert_direction {
232 (
233 e.get_delta_y() as f32 * speed_multiplier,
234 e.get_delta_x() as f32 * speed_multiplier,
235 )
236 } else {
237 (
238 e.get_delta_x() as f32 * speed_multiplier,
239 e.get_delta_y() as f32 * speed_multiplier,
240 )
241 };
242
243 let scroll_position_y = get_scroll_position_from_wheel(
244 y_movement,
245 size.inner.height,
246 size.area.height(),
247 corrected_scrolled_y,
248 );
249
250 if *scrolled_y.peek() != scroll_position_y {
252 e.stop_propagation();
253 *scrolled_y.write() = scroll_position_y;
254 }
255
256 let scroll_position_x = get_scroll_position_from_wheel(
257 x_movement,
258 size.inner.width,
259 size.area.width(),
260 corrected_scrolled_x,
261 );
262
263 if *scrolled_x.peek() != scroll_position_x {
265 e.stop_propagation();
266 *scrolled_x.write() = scroll_position_x;
267 }
268 };
269
270 let onmousemove = move |e: MouseEvent| {
272 let clicking_scrollbar = clicking_scrollbar.peek();
273
274 if let Some((Axis::Y, y)) = *clicking_scrollbar {
275 let coordinates = e.get_element_coordinates();
276 let cursor_y = coordinates.y - y - size.area.min_y() as f64;
277
278 let scroll_position = get_scroll_position_from_cursor(
279 cursor_y as f32,
280 size.inner.height,
281 size.area.height(),
282 );
283
284 *scrolled_y.write() = scroll_position;
285 } else if let Some((Axis::X, x)) = *clicking_scrollbar {
286 let coordinates = e.get_element_coordinates();
287 let cursor_x = coordinates.x - x - size.area.min_x() as f64;
288
289 let scroll_position = get_scroll_position_from_cursor(
290 cursor_x as f32,
291 size.inner.width,
292 size.area.width(),
293 );
294
295 *scrolled_x.write() = scroll_position;
296 }
297
298 if clicking_scrollbar.is_some() {
299 focus.request_focus();
300 }
301 };
302
303 let onglobalkeydown = move |e: KeyboardEvent| {
304 match &e.key {
305 Key::Shift => {
306 clicking_shift.set(true);
307 }
308 Key::Alt => {
309 clicking_alt.set(true);
310 }
311 k => {
312 if !focus.is_focused() {
313 return;
314 }
315 if !scroll_with_arrows
316 && (k == &Key::ArrowUp
317 || k == &Key::ArrowRight
318 || k == &Key::ArrowDown
319 || k == &Key::ArrowLeft)
320 {
321 return;
322 }
323
324 let x = corrected_scrolled_x;
325 let y = corrected_scrolled_y;
326 let inner_height = size.inner.height;
327 let inner_width = size.inner.width;
328 let viewport_height = size.area.height();
329 let viewport_width = size.area.width();
330
331 let (x, y) = manage_key_event(
332 e,
333 (x, y),
334 inner_height,
335 inner_width,
336 viewport_height,
337 viewport_width,
338 );
339
340 scrolled_x.set(x as i32);
341 scrolled_y.set(y as i32);
342 }
343 };
344 };
345
346 let onglobalkeyup = move |e: KeyboardEvent| {
347 if e.key == Key::Shift {
348 clicking_shift.set(false);
349 } else if e.key == Key::Alt {
350 clicking_alt.set(false);
351 }
352 };
353
354 let onmousedown_y = move |e: MouseEvent| {
356 let coordinates = e.get_element_coordinates();
357 *clicking_scrollbar.write() = Some((Axis::Y, coordinates.y));
358 };
359
360 let onmousedown_x = move |e: MouseEvent| {
362 let coordinates = e.get_element_coordinates();
363 *clicking_scrollbar.write() = Some((Axis::X, coordinates.x));
364 };
365
366 let onclick = move |_: MouseEvent| {
368 if clicking_scrollbar.peek().is_some() {
369 *clicking_scrollbar.write() = None;
370 }
371 };
372
373 let is_scrolling_x = clicking_scrollbar
374 .read()
375 .as_ref()
376 .map(|f| f.0 == Axis::X)
377 .unwrap_or_default();
378 let is_scrolling_y = clicking_scrollbar
379 .read()
380 .as_ref()
381 .map(|f| f.0 == Axis::Y)
382 .unwrap_or_default();
383
384 let a11y_id = focus.attribute();
385
386 rsx!(
387 rect {
388 a11y_role:"scroll-view",
389 overflow: "clip",
390 direction: "horizontal",
391 width: width.clone(),
392 height: height.clone(),
393 min_width: min_width.map(|x| x.to_string()),
394 min_height: min_height.map(|x| x.to_string()),
395 max_width: max_width.map(|x| x.to_string()),
396 max_height: max_height.map(|x| x.to_string()),
397 onglobalclick: onclick,
398 onglobalmousemove: onmousemove,
399 onglobalkeydown,
400 onglobalkeyup,
401 a11y_id,
402 a11y_focusable: "false",
403 rect {
404 direction: "vertical",
405 width: container_width,
406 height: container_height,
407 rect {
408 overflow: "clip",
409 spacing,
410 padding,
411 width: content_width,
412 height: content_height,
413 min_width: min_width.map(|x| x.to_string()),
414 min_height: min_height.map(|x| x.to_string()),
415 max_width: max_width.map(|x| x.to_string()),
416 max_height: max_height.map(|x| x.to_string()),
417 direction: direction,
418 offset_y: "{corrected_scrolled_y}",
419 offset_x: "{corrected_scrolled_x}",
420 reference: node_ref,
421 onwheel,
422 {children}
423 }
424 if show_scrollbar && horizontal_scrollbar_is_visible {
425 ScrollBar {
426 size: &applied_scrollbar_theme.size,
427 offset_x: scrollbar_x,
428 clicking_scrollbar: is_scrolling_x,
429 theme: scrollbar_theme.clone(),
430 ScrollThumb {
431 clicking_scrollbar: is_scrolling_x,
432 onmousedown: onmousedown_x,
433 width: "{scrollbar_width}",
434 height: "100%",
435 theme: scrollbar_theme.clone()
436 }
437 }
438 }
439 }
440 if show_scrollbar && vertical_scrollbar_is_visible {
441 ScrollBar {
442 is_vertical: true,
443 size: &applied_scrollbar_theme.size,
444 offset_y: scrollbar_y,
445 clicking_scrollbar: is_scrolling_y,
446 theme: scrollbar_theme.clone(),
447 ScrollThumb {
448 clicking_scrollbar: is_scrolling_y,
449 onmousedown: onmousedown_y,
450 width: "100%",
451 height: "{scrollbar_height}",
452 theme: scrollbar_theme
453 }
454 }
455 }
456 }
457 )
458}
459
460#[cfg(test)]
461mod test {
462 use freya::prelude::*;
463 use freya_testing::prelude::*;
464
465 #[tokio::test]
466 pub async fn scroll_view_wheel() {
467 fn scroll_view_wheel_app() -> Element {
468 rsx!(
469 ScrollView {
470 rect {
471 height: "200",
472 width: "200",
473 }
474 rect {
475 height: "200",
476 width: "200",
477 }
478 rect {
479 height: "200",
480 width: "200",
481 }
482 rect {
483 height: "200",
484 width: "200",
485 }
486 }
487 )
488 }
489
490 let mut utils = launch_test(scroll_view_wheel_app);
491 let root = utils.root();
492 let content = root.get(0).get(0).get(0);
493 utils.wait_for_update().await;
494
495 assert!(content.get(0).is_visible()); assert!(content.get(1).is_visible()); assert!(content.get(2).is_visible()); assert!(!content.get(3).is_visible()); utils.push_event(TestEvent::Wheel {
503 name: EventName::Wheel,
504 scroll: (0., -300.).into(),
505 cursor: (5., 5.).into(),
506 });
507
508 utils.wait_for_update().await;
509
510 assert!(!content.get(0).is_visible()); assert!(content.get(1).is_visible()); assert!(content.get(2).is_visible()); assert!(content.get(3).is_visible()); }
517
518 #[tokio::test]
519 pub async fn scroll_view_scrollbar() {
520 fn scroll_view_scrollbar_app() -> Element {
521 rsx!(
522 ScrollView {
523 rect {
524 height: "200",
525 width: "200",
526 }
527 rect {
528 height: "200",
529 width: "200",
530 }
531 rect {
532 height: "200",
533 width: "200",
534 }
535 rect {
536 height: "200",
537 width: "200",
538 }
539 }
540 )
541 }
542
543 let mut utils = launch_test(scroll_view_scrollbar_app);
544 let root = utils.root();
545 let content = root.get(0).get(0).get(0);
546 utils.wait_for_update().await;
547
548 assert!(content.get(0).is_visible()); assert!(content.get(1).is_visible()); assert!(content.get(2).is_visible()); assert!(!content.get(3).is_visible()); utils.push_event(TestEvent::Mouse {
557 name: EventName::MouseMove,
558 cursor: (495., 20.).into(),
559 button: Some(MouseButton::Left),
560 });
561 utils.push_event(TestEvent::Mouse {
562 name: EventName::MouseDown,
563 cursor: (495., 20.).into(),
564 button: Some(MouseButton::Left),
565 });
566 utils.push_event(TestEvent::Mouse {
567 name: EventName::MouseMove,
568 cursor: (495., 320.).into(),
569 button: Some(MouseButton::Left),
570 });
571 utils.push_event(TestEvent::Mouse {
572 name: EventName::MouseUp,
573 cursor: (495., 320.).into(),
574 button: Some(MouseButton::Left),
575 });
576
577 utils.wait_for_update().await;
578
579 assert!(!content.get(0).is_visible()); assert!(content.get(1).is_visible()); assert!(content.get(2).is_visible()); assert!(content.get(3).is_visible()); for _ in 0..5 {
588 utils.push_event(TestEvent::Keyboard {
589 name: EventName::KeyDown,
590 key: Key::ArrowUp,
591 code: Code::ArrowUp,
592 modifiers: Modifiers::default(),
593 });
594 utils.wait_for_update().await;
595 }
596
597 assert!(content.get(0).is_visible());
598 assert!(content.get(1).is_visible());
599 assert!(content.get(2).is_visible());
600 assert!(!content.get(3).is_visible());
601
602 utils.push_event(TestEvent::Keyboard {
604 name: EventName::KeyDown,
605 key: Key::End,
606 code: Code::End,
607 modifiers: Modifiers::default(),
608 });
609 utils.wait_for_update().await;
610
611 assert!(!content.get(0).is_visible());
612 assert!(content.get(1).is_visible());
613 assert!(content.get(2).is_visible());
614 assert!(content.get(3).is_visible());
615 }
616}