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