1use std::rc::Rc;
18
19use dioxus::html::geometry::PixelsVector2D;
20use dioxus::html::{MountedData, ScrollBehavior};
21use dioxus::prelude::*;
22
23use crate::core::{Point, Rect};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum ScrollAxis {
28 #[default]
30 Y,
31 X,
33 Both,
35}
36
37pub fn edge_delta(
41 pos: Point,
42 rect: Rect,
43 threshold: f64,
44 speed: f64,
45 axis: ScrollAxis,
46) -> (f64, f64) {
47 let ramp = |dist_into_band: f64| (dist_into_band / threshold.max(1.0)).clamp(0.0, 1.0) * speed;
48 let mut dx = 0.0;
49 let mut dy = 0.0;
50 if matches!(axis, ScrollAxis::X | ScrollAxis::Both) {
51 let from_left = pos.x - rect.x;
52 let from_right = rect.x + rect.width - pos.x;
53 if from_left < threshold {
54 dx = -ramp(threshold - from_left);
55 } else if from_right < threshold {
56 dx = ramp(threshold - from_right);
57 }
58 }
59 if matches!(axis, ScrollAxis::Y | ScrollAxis::Both) {
60 let from_top = pos.y - rect.y;
61 let from_bottom = rect.y + rect.height - pos.y;
62 if from_top < threshold {
63 dy = -ramp(threshold - from_top);
64 } else if from_bottom < threshold {
65 dy = ramp(threshold - from_bottom);
66 }
67 }
68 (dx, dy)
69}
70
71#[component]
74pub fn AutoScroll(
75 #[props(default = 48.0)]
77 threshold: f64,
78 #[props(default = 24.0)]
80 speed: f64,
81 #[props(default)]
83 axis: ScrollAxis,
84 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
85 children: Element,
86) -> Element {
87 let mut mounted = use_signal(|| None::<Rc<MountedData>>);
88 let busy = use_signal(|| false);
91
92 let scroll_for = move |point: Point| {
93 let Some(m) = mounted.peek().clone() else {
94 return;
95 };
96 if *busy.peek() {
97 return;
98 }
99 let mut busy = busy;
100 busy.set(true);
101 spawn(async move {
102 if let Ok(r) = m.get_client_rect().await {
103 let rect = Rect::new(r.origin.x, r.origin.y, r.size.width, r.size.height);
104 let (dx, dy) = edge_delta(point, rect, threshold, speed, axis);
105 if dx != 0.0 || dy != 0.0 {
106 if let Ok(offset) = m.get_scroll_offset().await {
107 let _ = m
108 .scroll(
109 PixelsVector2D::new(offset.x + dx, offset.y + dy),
110 ScrollBehavior::Instant,
111 )
112 .await;
113 }
114 }
115 }
116 busy.set(false);
117 });
118 };
119
120 rsx! {
121 div {
122 onmounted: move |evt: Event<MountedData>| {
123 mounted.set(Some(evt.data()));
124 },
125 ondragover: move |evt: DragEvent| {
129 let c = evt.client_coordinates();
130 scroll_for(Point::new(c.x, c.y));
131 },
132 onpointermove: move |evt: PointerEvent| {
135 if evt.pointer_type() != "mouse" && evt.pressure() > 0.0 {
136 let c = evt.client_coordinates();
137 scroll_for(Point::new(c.x, c.y));
138 }
139 },
140 ..attributes,
141 {children}
142 }
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn deltas_ramp_toward_edges() {
152 let rect = Rect::new(0.0, 0.0, 200.0, 400.0);
153 assert_eq!(
155 edge_delta(Point::new(100.0, 200.0), rect, 48.0, 24.0, ScrollAxis::Both),
156 (0.0, 0.0)
157 );
158 let (_, dy) = edge_delta(Point::new(100.0, 10.0), rect, 48.0, 24.0, ScrollAxis::Y);
160 assert!(dy < 0.0 && dy >= -24.0);
161 let (_, dy) = edge_delta(Point::new(100.0, 400.0), rect, 48.0, 24.0, ScrollAxis::Y);
163 assert_eq!(dy, 24.0);
164 let (dx, _) = edge_delta(Point::new(1.0, 200.0), rect, 48.0, 24.0, ScrollAxis::Y);
166 assert_eq!(dx, 0.0);
167 }
168}