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: cfg!(target_os = "android"),
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 focus = use_focus();
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 return;
294 } else if let Some(origin) = drag_origin() {
295 let coords = e.global_location();
296 let distance = (origin - coords).abs();
297
298 const DRAG_THRESHOLD: f64 = 2.0;
301
302 if distance.x > DRAG_THRESHOLD || distance.y > DRAG_THRESHOLD {
303 let delta = origin - coords;
304
305 scroll_controller
306 .scroll_to_y((corrected_scrolled_y - delta.y as f32) as i32);
307 scroll_controller
308 .scroll_to_x((corrected_scrolled_x - delta.x as f32) as i32);
309
310 dragging_content.set(Some(coords));
311 e.prevent_default();
312 timeout.reset();
313 }
314 return;
315 }
316 }
317
318 let clicking_scrollbar = clicking_scrollbar.peek();
319
320 if let Some((Axis::Y, y)) = *clicking_scrollbar {
321 let coordinates = e.element_location();
322 let cursor_y = coordinates.y - y - size.read().area.min_y() as f64;
323
324 let scroll_position = get_scroll_position_from_cursor(
325 cursor_y as f32,
326 size.read().inner_sizes.height,
327 size.read().area.height(),
328 );
329
330 scroll_controller.scroll_to_y(scroll_position);
331 } else if let Some((Axis::X, x)) = *clicking_scrollbar {
332 let coordinates = e.element_location();
333 let cursor_x = coordinates.x - x - size.read().area.min_x() as f64;
334
335 let scroll_position = get_scroll_position_from_cursor(
336 cursor_x as f32,
337 size.read().inner_sizes.width,
338 size.read().area.width(),
339 );
340
341 scroll_controller.scroll_to_x(scroll_position);
342 }
343
344 if clicking_scrollbar.is_some() {
345 e.prevent_default();
346 timeout.reset();
347 if !focus.is_focused() {
348 focus.request_focus();
349 }
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 {
400 drag_origin.set(Some(e.global_location()));
401 focus.request_focus();
402 timeout.reset();
403 }
404 };
405
406 rect()
407 .width(layout.width.clone())
408 .height(layout.height.clone())
409 .max_width(layout.maximum_width.clone())
410 .max_height(layout.maximum_height.clone())
411 .a11y_id(focus.a11y_id())
412 .a11y_focusable(false)
413 .a11y_role(AccessibilityRole::ScrollView)
414 .a11y_builder(move |node| {
415 node.set_scroll_x(corrected_scrolled_x as f64);
416 node.set_scroll_y(corrected_scrolled_y as f64)
417 })
418 .scrollable(true)
419 .on_wheel(on_wheel)
420 .on_capture_global_pointer_press(on_capture_global_pointer_press)
421 .on_mouse_move(on_mouse_move)
422 .on_capture_global_pointer_move(on_capture_global_pointer_move)
423 .on_key_down(on_key_down)
424 .on_global_key_up(on_global_key_up)
425 .on_global_key_down(on_global_key_down)
426 .on_pointer_down(on_pointer_down)
427 .child(
428 rect()
429 .width(container_width)
430 .height(container_height)
431 .horizontal()
432 .child(
433 rect()
434 .direction(direction)
435 .width(content_width)
436 .height(content_height)
437 .max_width(layout.maximum_width.clone())
438 .max_height(layout.maximum_height.clone())
439 .offset_x(corrected_scrolled_x)
440 .offset_y(corrected_scrolled_y)
441 .spacing(layout.spacing.get())
442 .overflow(Overflow::Clip)
443 .on_sized(move |e: Event<SizedEventData>| {
444 size.set_if_modified(e.clone())
445 })
446 .children(self.children.clone()),
447 )
448 .maybe_child(vertical_scrollbar_is_visible.then_some({
449 rect().child(ScrollBar {
450 theme: None,
451 clicking_scrollbar,
452 axis: Axis::Y,
453 offset: scrollbar_y,
454 size: Size::px(size.read().area.height()),
455 thumb: ScrollThumb {
456 theme: None,
457 clicking_scrollbar,
458 axis: Axis::Y,
459 size: scrollbar_height,
460 },
461 })
462 })),
463 )
464 .maybe_child(horizontal_scrollbar_is_visible.then_some({
465 rect().child(ScrollBar {
466 theme: None,
467 clicking_scrollbar,
468 axis: Axis::X,
469 offset: scrollbar_x,
470 size: Size::px(size.read().area.width()),
471 thumb: ScrollThumb {
472 theme: None,
473 clicking_scrollbar,
474 axis: Axis::X,
475 size: scrollbar_width,
476 },
477 })
478 }))
479 }
480
481 fn render_key(&self) -> DiffKey {
482 self.key.clone().or(self.default_key())
483 }
484}