1use animato_core::Update;
4use animato_physics::{DragState, InertiaConfig, InertiaN, PointerData};
5use dioxus::prelude::{Signal, use_signal};
6use std::fmt;
7use std::sync::{Arc, Mutex};
8
9pub use animato_physics::{DragAxis, DragConstraints, Gesture, GestureConfig, SwipeDirection};
10
11#[derive(Clone, Debug, PartialEq)]
13pub struct DragConfig {
14 pub axis: DragAxis,
16 pub constraints: Option<DragConstraints>,
18 pub inertia: bool,
20 pub inertia_config: InertiaConfig<[f32; 2]>,
22 pub snap_points: Vec<[f32; 2]>,
24 pub elastic_edges: bool,
26}
27
28impl Default for DragConfig {
29 fn default() -> Self {
30 Self {
31 axis: DragAxis::Both,
32 constraints: None,
33 inertia: true,
34 inertia_config: InertiaConfig::new(1400.0, 2.0),
35 snap_points: Vec::new(),
36 elastic_edges: false,
37 }
38 }
39}
40
41#[derive(Clone)]
43pub struct DragHandle {
44 state: Arc<Mutex<DragState>>,
45 inertia: Arc<Mutex<Option<InertiaN<[f32; 2]>>>>,
46 position: Signal<[f32; 2]>,
47 config: DragConfig,
48}
49
50impl fmt::Debug for DragHandle {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 f.debug_struct("DragHandle")
53 .field("config", &self.config)
54 .finish_non_exhaustive()
55 }
56}
57
58impl DragHandle {
59 pub fn pointer_down(&self, x: f32, y: f32, pointer_id: u64) {
61 crate::with_lock(&self.inertia, |inertia| *inertia = None);
62 crate::with_lock(&self.state, |state| {
63 state.on_pointer_down(PointerData::new(x, y, pointer_id));
64 crate::set_signal(self.position, state.position());
65 });
66 }
67
68 pub fn pointer_move(&self, x: f32, y: f32, pointer_id: u64, dt: f32) {
70 crate::with_lock(&self.state, |state| {
71 state.on_pointer_move(PointerData::new(x, y, pointer_id), dt.max(0.0));
72 crate::set_signal(self.position, state.position());
73 });
74 }
75
76 pub fn pointer_up(&self, x: f32, y: f32, pointer_id: u64) {
78 let inertia = crate::with_lock(&self.state, |state| {
79 let inertia = if self.config.inertia {
80 state.on_pointer_up(PointerData::new(x, y, pointer_id))
81 } else {
82 let _ = state.on_pointer_up(PointerData::new(x, y, pointer_id));
83 None
84 };
85 if inertia.is_none()
86 && let Some(snapped) = nearest_snap(state.position(), &self.config.snap_points)
87 {
88 state.snap_to(snapped);
89 }
90 crate::set_signal(self.position, state.position());
91 inertia
92 });
93 crate::with_lock(&self.inertia, |slot| *slot = inertia);
94 }
95
96 pub fn set_constraints(&self, constraints: Option<DragConstraints>) {
98 crate::with_lock(&self.state, |state| {
99 state.set_constraints(constraints.unwrap_or_else(DragConstraints::unbounded));
100 crate::set_signal(self.position, state.position());
101 });
102 }
103
104 pub fn snap_to(&self, position: [f32; 2]) {
106 crate::with_lock(&self.inertia, |inertia| *inertia = None);
107 crate::with_lock(&self.state, |state| {
108 state.snap_to(position);
109 crate::set_signal(self.position, state.position());
110 });
111 }
112
113 pub fn position(&self) -> Signal<[f32; 2]> {
115 self.position
116 }
117
118 pub fn tick(&self, dt: f32) -> bool {
120 crate::with_lock(&self.inertia, |inertia| {
121 if let Some(active) = inertia.as_mut() {
122 let running = active.update(dt.max(0.0));
123 crate::set_signal(self.position, active.position());
124 if !running {
125 *inertia = None;
126 }
127 running
128 } else {
129 false
130 }
131 })
132 }
133}
134
135pub fn use_drag<T: 'static>(target: T, config: DragConfig) -> (Signal<[f32; 2]>, DragHandle) {
137 let _ = target;
138 let initial = [0.0, 0.0];
139 let mut state = DragState::new(initial).axis(config.axis);
140 if let Some(constraints) = config.constraints {
141 state = state.constraints(constraints);
142 }
143 state = state.inertia_config(config.inertia_config.clone());
144
145 let position = use_signal(|| initial);
146 let handle = DragHandle {
147 state: Arc::new(Mutex::new(state)),
148 inertia: Arc::new(Mutex::new(None)),
149 position,
150 config,
151 };
152
153 let loop_handle = handle.clone();
154 crate::spawn_animation_loop(move |dt| {
155 loop_handle.tick(dt);
156 true
157 });
158
159 (position, handle)
160}
161
162pub fn use_gesture<T: 'static>(target: T, config: GestureConfig) -> Signal<Option<Gesture>> {
164 let _ = (target, config);
165 use_signal(|| None)
166}
167
168#[derive(Clone)]
170pub struct PinchHandle {
171 scale: Signal<f32>,
172}
173
174impl fmt::Debug for PinchHandle {
175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176 f.debug_struct("PinchHandle").finish_non_exhaustive()
177 }
178}
179
180impl PinchHandle {
181 pub fn set_scale(&self, scale: f32) {
183 crate::set_signal(self.scale, crate::finite_or(scale, 1.0).max(0.0));
184 }
185
186 pub fn reset(&self) {
188 crate::set_signal(self.scale, 1.0);
189 }
190
191 pub fn scale(&self) -> Signal<f32> {
193 self.scale
194 }
195}
196
197pub fn use_pinch<T: 'static>(target: T) -> (Signal<f32>, PinchHandle) {
199 let _ = target;
200 let scale = use_signal(|| 1.0);
201 (scale, PinchHandle { scale })
202}
203
204#[derive(Clone, Copy, Debug, PartialEq)]
206pub struct SwipeConfig {
207 pub min_distance: f32,
209 pub min_velocity: f32,
211}
212
213impl Default for SwipeConfig {
214 fn default() -> Self {
215 Self {
216 min_distance: 40.0,
217 min_velocity: 100.0,
218 }
219 }
220}
221
222#[derive(Clone, Copy, Debug, PartialEq)]
224pub struct SwipeEvent {
225 pub direction: SwipeDirection,
227 pub velocity: f32,
229 pub distance: f32,
231}
232
233pub fn use_swipe<T: 'static>(target: T, config: SwipeConfig) -> Signal<Option<SwipeEvent>> {
235 let _ = (target, config);
236 use_signal(|| None)
237}
238
239fn nearest_snap(position: [f32; 2], snap_points: &[[f32; 2]]) -> Option<[f32; 2]> {
240 snap_points.iter().copied().min_by(|a, b| {
241 distance_sq(position, *a)
242 .partial_cmp(&distance_sq(position, *b))
243 .unwrap_or(std::cmp::Ordering::Equal)
244 })
245}
246
247fn distance_sq(a: [f32; 2], b: [f32; 2]) -> f32 {
248 let dx = a[0] - b[0];
249 let dy = a[1] - b[1];
250 dx * dx + dy * dy
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 use animato_physics::{GestureRecognizer, PointerData as PhysicsPointerData};
257 use dioxus::prelude::*;
258 use std::cell::RefCell;
259
260 thread_local! {
261 static DRAG_CAPTURE: RefCell<Option<(Signal<[f32; 2]>, DragHandle)>> = const { RefCell::new(None) };
262 static INERTIA_DRAG_CAPTURE: RefCell<Option<(Signal<[f32; 2]>, DragHandle)>> = const { RefCell::new(None) };
263 static PINCH_CAPTURE: RefCell<Option<(Signal<f32>, PinchHandle)>> = const { RefCell::new(None) };
264 static GESTURE_CAPTURE: RefCell<Option<Signal<Option<Gesture>>>> = const { RefCell::new(None) };
265 static SWIPE_CAPTURE: RefCell<Option<Signal<Option<SwipeEvent>>>> = const { RefCell::new(None) };
266 }
267
268 #[allow(non_snake_case)]
269 fn DragHookApp() -> Element {
270 let pair = use_drag(
271 "node",
272 DragConfig {
273 axis: DragAxis::X,
274 constraints: Some(DragConstraints::bounded(0.0, 100.0, 0.0, 100.0)),
275 inertia: false,
276 snap_points: vec![[0.0, 0.0], [100.0, 0.0]],
277 ..DragConfig::default()
278 },
279 );
280 DRAG_CAPTURE.with(|slot| *slot.borrow_mut() = Some(pair));
281
282 rsx! { div {} }
283 }
284
285 #[allow(non_snake_case)]
286 fn InertiaDragHookApp() -> Element {
287 let pair = use_drag(
288 "node",
289 DragConfig {
290 constraints: Some(DragConstraints::bounded(-500.0, 500.0, -500.0, 500.0)),
291 inertia: true,
292 inertia_config: InertiaConfig::new(500.0, 1.0),
293 ..DragConfig::default()
294 },
295 );
296 INERTIA_DRAG_CAPTURE.with(|slot| *slot.borrow_mut() = Some(pair));
297
298 rsx! { div {} }
299 }
300
301 #[allow(non_snake_case)]
302 fn GestureHookApp() -> Element {
303 let gesture = use_gesture("node", GestureConfig::default());
304 let pinch = use_pinch("node");
305 let swipe = use_swipe("node", SwipeConfig::default());
306 GESTURE_CAPTURE.with(|slot| *slot.borrow_mut() = Some(gesture));
307 PINCH_CAPTURE.with(|slot| *slot.borrow_mut() = Some(pinch));
308 SWIPE_CAPTURE.with(|slot| *slot.borrow_mut() = Some(swipe));
309
310 rsx! { div {} }
311 }
312
313 #[test]
314 fn nearest_snap_selects_closest_point() {
315 let points = [[0.0, 0.0], [10.0, 0.0], [20.0, 0.0]];
316 assert_eq!(nearest_snap([12.0, 0.0], &points), Some([10.0, 0.0]));
317 }
318
319 #[test]
320 fn swipe_config_has_useful_defaults() {
321 let config = SwipeConfig::default();
322 assert!(config.min_distance > 0.0);
323 assert!(config.min_velocity > 0.0);
324 }
325
326 #[test]
327 fn gesture_recognizer_detects_swipe() {
328 let mut recognizer = GestureRecognizer::new(GestureConfig::default());
329 recognizer.on_pointer_down(PhysicsPointerData::new(0.0, 0.0, 1), 0.0);
330 recognizer.on_pointer_move(PhysicsPointerData::new(100.0, 0.0, 1), 0.1);
331 let gesture = recognizer.on_pointer_up(PhysicsPointerData::new(100.0, 0.0, 1), 0.1);
332
333 assert!(matches!(
334 gesture,
335 Some(Gesture::Swipe {
336 direction: SwipeDirection::Right,
337 ..
338 })
339 ));
340 }
341
342 #[test]
343 fn drag_hook_updates_snaps_clamps_and_stops_without_inertia() {
344 DRAG_CAPTURE.with(|slot| *slot.borrow_mut() = None);
345 let mut dom = VirtualDom::new(DragHookApp);
346 dom.rebuild_in_place();
347 let (position, handle) =
348 DRAG_CAPTURE.with(|slot| slot.borrow().as_ref().cloned().expect("drag hook captured"));
349
350 assert_eq!(crate::read_signal(position), [0.0, 0.0]);
351 handle.pointer_down(0.0, 0.0, 1);
352 handle.pointer_move(80.0, 40.0, 99, 0.1);
353 assert_eq!(crate::read_signal(position), [0.0, 0.0]);
354
355 handle.pointer_move(80.0, 40.0, 1, 0.1);
356 assert_eq!(crate::read_signal(handle.position()), [80.0, 0.0]);
357 handle.pointer_up(80.0, 40.0, 1);
358 assert_eq!(crate::read_signal(position), [100.0, 0.0]);
359 assert!(!handle.tick(0.016));
360
361 handle.set_constraints(Some(DragConstraints::bounded(-10.0, 40.0, -10.0, 40.0)));
362 assert_eq!(crate::read_signal(position), [40.0, 0.0]);
363 handle.snap_to([5.0, 20.0]);
364 assert_eq!(crate::read_signal(position), [5.0, 0.0]);
365 handle.set_constraints(None);
366 handle.snap_to([f32::INFINITY, f32::NAN]);
367 assert_eq!(crate::read_signal(position), [0.0, 0.0]);
368 }
369
370 #[test]
371 fn drag_hook_runs_release_inertia_until_settled_or_cancelled() {
372 INERTIA_DRAG_CAPTURE.with(|slot| *slot.borrow_mut() = None);
373 let mut dom = VirtualDom::new(InertiaDragHookApp);
374 dom.rebuild_in_place();
375 let (position, handle) = INERTIA_DRAG_CAPTURE.with(|slot| {
376 slot.borrow()
377 .as_ref()
378 .cloned()
379 .expect("inertia drag hook captured")
380 });
381
382 handle.pointer_down(0.0, 0.0, 1);
383 handle.pointer_move(100.0, 0.0, 1, 0.01);
384 let release_position = crate::read_signal(position);
385 handle.pointer_up(100.0, 0.0, 1);
386 assert!(handle.tick(0.016));
387 assert!(crate::read_signal(position)[0] >= release_position[0]);
388
389 handle.snap_to([12.0, 24.0]);
390 assert_eq!(crate::read_signal(position), [12.0, 24.0]);
391 assert!(!handle.tick(0.016));
392 }
393
394 #[test]
395 fn gesture_pinch_and_swipe_hooks_return_stable_signals() {
396 GESTURE_CAPTURE.with(|slot| *slot.borrow_mut() = None);
397 PINCH_CAPTURE.with(|slot| *slot.borrow_mut() = None);
398 SWIPE_CAPTURE.with(|slot| *slot.borrow_mut() = None);
399 let mut dom = VirtualDom::new(GestureHookApp);
400 dom.rebuild_in_place();
401
402 let gesture = GESTURE_CAPTURE.with(|slot| {
403 slot.borrow()
404 .as_ref()
405 .copied()
406 .expect("gesture signal captured")
407 });
408 let (scale, pinch) = PINCH_CAPTURE.with(|slot| {
409 slot.borrow()
410 .as_ref()
411 .cloned()
412 .expect("pinch hook captured")
413 });
414 let swipe = SWIPE_CAPTURE.with(|slot| {
415 slot.borrow()
416 .as_ref()
417 .copied()
418 .expect("swipe signal captured")
419 });
420
421 assert_eq!(crate::read_signal(gesture), None);
422 assert_eq!(crate::read_signal(swipe), None);
423 assert_eq!(crate::read_signal(scale), 1.0);
424
425 pinch.set_scale(f32::NAN);
426 assert_eq!(crate::read_signal(pinch.scale()), 1.0);
427 pinch.set_scale(-2.0);
428 assert_eq!(crate::read_signal(scale), 0.0);
429 pinch.reset();
430 assert_eq!(crate::read_signal(scale), 1.0);
431 }
432}