1use crate::animation::{AnimatedValue, AnimationSpec};
2use crate::frame_clock::request_frame;
3use std::cell::{Cell, RefCell};
4use std::rc::Rc;
5use web_time::Instant;
6
7#[derive(Clone, Copy, Debug, PartialEq)]
9pub enum DragOrientation {
10 Horizontal,
11 Vertical,
12}
13
14#[derive(Clone)]
16pub struct SwipeableConfig {
17 pub animation_spec: AnimationSpec,
19 pub positional_threshold: f32,
22 pub velocity_threshold: f32,
26 pub max_overshoot: f32,
28}
29
30impl Default for SwipeableConfig {
31 fn default() -> Self {
32 Self {
33 animation_spec: AnimationSpec::spring_gentle(),
34 positional_threshold: 0.3,
35 velocity_threshold: f32::MAX,
36 max_overshoot: 0.0,
37 }
38 }
39}
40
41pub struct SwipeableState<T: Clone + PartialEq + 'static> {
67 anim: Rc<RefCell<AnimatedValue<f32>>>,
68 anchors: Rc<Vec<(f32, T)>>,
69 config: Rc<SwipeableConfig>,
70 drag_start: Rc<Cell<Option<f32>>>,
71 drag_base: Rc<Cell<f32>>,
72 last_move_time: Rc<Cell<Option<Instant>>>,
73 last_move_pos: Rc<Cell<Option<f32>>>,
74 release_velocity: Rc<Cell<f32>>,
75}
76
77impl<T: Clone + PartialEq + 'static> SwipeableState<T> {
78 pub fn new(anchors: Vec<(f32, T)>, config: SwipeableConfig) -> Self {
83 assert!(
84 !anchors.is_empty(),
85 "SwipeableState requires at least one anchor"
86 );
87 let initial_offset = anchors[0].0;
88 Self {
89 anim: Rc::new(RefCell::new(AnimatedValue::new(
90 initial_offset,
91 config.animation_spec,
92 ))),
93 anchors: Rc::new(anchors),
94 config: Rc::new(config),
95 drag_start: Rc::new(Cell::new(None)),
96 drag_base: Rc::new(Cell::new(initial_offset)),
97 last_move_time: Rc::new(Cell::new(None)),
98 last_move_pos: Rc::new(Cell::new(None)),
99 release_velocity: Rc::new(Cell::new(0.0)),
100 }
101 }
102
103 pub fn offset(&self) -> f32 {
106 let mut anim = self.anim.borrow_mut();
107 if anim.update() {
108 request_frame();
109 }
110 *anim.get()
111 }
112
113 pub fn current_value(&self) -> T {
115 let off = *self.anim.borrow().get();
116 self.nearest_anchor(off).map(|(_, v)| v).unwrap_or_else(|| {
117 self.anchors
118 .first()
119 .map(|(_, v)| v.clone())
120 .unwrap_or_else(|| panic!("SwipeableState has no anchors"))
121 })
122 }
123
124 pub fn anchors(&self) -> Vec<(f32, T)> {
126 self.anchors.iter().cloned().collect()
127 }
128
129 pub fn anchor_offset(&self, value: &T) -> Option<f32> {
131 self.anchors
132 .iter()
133 .find(|(_, v)| v == value)
134 .map(|(o, _)| *o)
135 }
136
137 pub fn snap_to(&self, off: f32) {
139 let clamped = self.clamp_offset(off);
140 self.anim.borrow_mut().snap_to(clamped);
141 request_frame();
142 }
143
144 pub fn animate_to(&self, value: &T) {
146 if let Some((offset, _)) = self.anchors.iter().find(|(_, v)| v == value) {
147 self.anim.borrow_mut().set_target(*offset);
148 request_frame();
149 }
150 }
151
152 pub fn is_animating(&self) -> bool {
154 self.anim.borrow().is_animating()
155 }
156
157 pub fn on_pointer_down(&self, axis_pos: f32) {
159 self.drag_start.set(Some(axis_pos));
160 self.drag_base.set(*self.anim.borrow().get());
161 self.last_move_time.set(Some(Instant::now()));
162 self.last_move_pos.set(Some(axis_pos));
163 self.release_velocity.set(0.0);
164 }
165
166 pub fn on_pointer_move(&self, axis_pos: f32) {
168 if let Some(start) = self.drag_start.get() {
169 let now = Instant::now();
170 let delta = axis_pos - start;
171 let new_offset = self.drag_base.get() + delta;
172
173 if let (Some(last_pos), Some(last_time)) =
175 (self.last_move_pos.get(), self.last_move_time.get())
176 {
177 let dt = (now - last_time).as_secs_f32().max(1.0 / 240.0);
178 let vel = (axis_pos - last_pos) / dt;
179 self.release_velocity.set(vel);
180 }
181
182 self.snap_to(new_offset);
183 self.last_move_pos.set(Some(axis_pos));
184 self.last_move_time.set(Some(now));
185 }
186 }
187
188 pub fn on_pointer_up(&self) {
191 self.drag_start.set(None);
192 let off = *self.anim.borrow().get();
193 let vel = self.release_velocity.get();
194 let config = &*self.config;
195
196 if vel.abs() > config.velocity_threshold {
198 let target_off = if vel < 0.0 {
199 self.next_anchor_down(off)
200 } else {
201 self.next_anchor_up(off)
202 };
203 self.anim.borrow_mut().set_target(target_off);
204 request_frame();
205 return;
206 }
207
208 let target_off = self.snap_target(off, config.positional_threshold);
211 self.anim.borrow_mut().set_target(target_off);
212 request_frame();
213 }
214
215 fn clamp_offset(&self, off: f32) -> f32 {
217 let min = self.anchors.first().map(|(o, _)| *o).unwrap_or(0.0);
218 let max = self.anchors.last().map(|(o, _)| *o).unwrap_or(0.0);
219 let lo = if min <= max {
220 min - self.config.max_overshoot
221 } else {
222 max - self.config.max_overshoot
223 };
224 let hi = if min <= max {
225 max + self.config.max_overshoot
226 } else {
227 min + self.config.max_overshoot
228 };
229 off.clamp(lo.min(hi), lo.max(hi))
230 }
231
232 fn nearest_anchor(&self, off: f32) -> Option<(f32, T)> {
234 self.anchors
235 .iter()
236 .min_by(|(a, _), (b, _)| (a - off).abs().partial_cmp(&(b - off).abs()).unwrap())
237 .map(|(o, v)| (*o, v.clone()))
238 }
239
240 fn next_anchor_down(&self, off: f32) -> f32 {
242 self.anchors
243 .iter()
244 .rev()
245 .find(|(a, _)| *a < off)
246 .map(|(o, _)| *o)
247 .unwrap_or_else(|| self.anchors.last().map(|(o, _)| *o).unwrap_or(off))
248 }
249
250 fn next_anchor_up(&self, off: f32) -> f32 {
252 self.anchors
253 .iter()
254 .find(|(a, _)| *a > off)
255 .map(|(o, _)| *o)
256 .unwrap_or_else(|| self.anchors.first().map(|(o, _)| *o).unwrap_or(off))
257 }
258
259 fn snap_target(&self, off: f32, threshold: f32) -> f32 {
261 let upper = self.anchors.iter().find(|(a, _)| *a >= off);
263 let lower = self.anchors.iter().rev().find(|(a, _)| *a <= off);
264
265 match (lower, upper) {
266 (Some((lo, _)), Some((hi, _))) if lo != hi => {
267 let range = hi - lo;
269 if range == 0.0 {
270 *lo
271 } else {
272 let fraction = (off - lo) / range;
273 if fraction >= threshold { *hi } else { *lo }
274 }
275 }
276 (Some((lo, _)), _) => {
277 *lo
279 }
280 (_, Some((hi, _))) => {
281 *hi
283 }
284 _ => off,
285 }
286 }
287}
288
289pub struct DraggableConfig {
290 pub orientation: Option<DragOrientation>,
291 pub enabled: bool,
292 pub reverse_direction: bool,
293 pub start_drag_immediately: bool,
294 pub slop_px: f32,
295}
296
297impl Default for DraggableConfig {
298 fn default() -> Self {
299 Self {
300 orientation: None,
301 enabled: true,
302 reverse_direction: false,
303 start_drag_immediately: false,
304 slop_px: 18.0,
305 }
306 }
307}
308
309impl DragOrientation {
310 pub fn horizontal() -> DraggableConfig {
311 DraggableConfig {
312 orientation: Some(DragOrientation::Horizontal),
313 ..Default::default()
314 }
315 }
316
317 pub fn vertical() -> DraggableConfig {
318 DraggableConfig {
319 orientation: Some(DragOrientation::Vertical),
320 ..Default::default()
321 }
322 }
323}
324
325pub struct TransformableState {
326 on_transform: Rc<dyn Fn(f32, crate::Vec2, f32)>,
327 enabled: Cell<bool>,
328 lock_rotation_on_zoom_pan: Cell<bool>,
329 last_pinch: Cell<Option<Instant>>,
330}
331
332impl TransformableState {
333 pub fn new(on_transform: impl Fn(f32, crate::Vec2, f32) + 'static) -> Self {
334 Self {
335 on_transform: Rc::new(on_transform),
336 enabled: Cell::new(true),
337 lock_rotation_on_zoom_pan: Cell::new(false),
338 last_pinch: Cell::new(None),
339 }
340 }
341
342 pub fn set_enabled(&self, enabled: bool) {
343 self.enabled.set(enabled);
344 }
345
346 pub fn set_lock_rotation_on_zoom_pan(&self, lock: bool) {
347 self.lock_rotation_on_zoom_pan.set(lock);
348 }
349
350 pub fn handle_gesture(&self, gesture: &crate::shortcuts::Gesture) -> bool {
351 if !self.enabled.get() {
352 return false;
353 }
354 match gesture {
355 crate::shortcuts::Gesture::PinchWithCenter { delta_scale, .. } => {
356 self.last_pinch.set(Some(Instant::now()));
357 (self.on_transform)(*delta_scale, crate::Vec2::ZERO, 0.0);
358 request_frame();
359 true
360 }
361 crate::shortcuts::Gesture::Rotate { delta_rotation, .. } => {
362 if self.lock_rotation_on_zoom_pan.get()
363 && let Some(t) = self.last_pinch.get()
364 && t.elapsed().as_millis() < 120
365 {
366 return true;
367 }
368 (self.on_transform)(1.0, crate::Vec2::ZERO, *delta_rotation);
369 request_frame();
370 true
371 }
372 crate::shortcuts::Gesture::Pan { delta, .. } => {
373 (self.on_transform)(1.0, *delta, 0.0);
374 request_frame();
375 true
376 }
377 _ => false,
378 }
379 }
380}
381
382impl Clone for TransformableState {
383 fn clone(&self) -> Self {
384 Self {
385 on_transform: self.on_transform.clone(),
386 enabled: Cell::new(self.enabled.get()),
387 lock_rotation_on_zoom_pan: Cell::new(self.lock_rotation_on_zoom_pan.get()),
388 last_pinch: Cell::new(self.last_pinch.get()),
389 }
390 }
391}
392
393pub type ConfirmValueChange<T> = Rc<dyn Fn(T) -> bool>;
394
395pub struct AnchoredDraggableState<T: Clone + PartialEq + 'static> {
396 inner: SwipeableState<T>,
397 config: SwipeableConfig,
398 confirm_value_change: Rc<RefCell<Option<ConfirmValueChange<T>>>>,
399 target: Rc<RefCell<Option<T>>>,
400}
401
402impl<T: Clone + PartialEq + 'static> AnchoredDraggableState<T> {
403 pub fn new(
404 anchors: Vec<(f32, T)>,
405 config: SwipeableConfig,
406 confirm_value_change: Option<ConfirmValueChange<T>>,
407 ) -> Self {
408 let target = anchors.first().map(|(_, v)| v.clone());
409 Self {
410 inner: SwipeableState::new(anchors, config.clone()),
411 config,
412 confirm_value_change: Rc::new(RefCell::new(confirm_value_change)),
413 target: Rc::new(RefCell::new(target)),
414 }
415 }
416
417 pub fn update_anchors(&mut self, anchors: Vec<(f32, T)>) {
418 let current = self.inner.offset();
419 self.inner = SwipeableState::new(anchors, self.config.clone());
420 self.inner.snap_to(current);
421 }
422
423 pub fn anchors(&self) -> Vec<(f32, T)> {
424 self.inner.anchors()
425 }
426
427 pub fn anchor_offset_for(&self, value: &T) -> Option<f32> {
428 self.inner.anchor_offset(value)
429 }
430
431 pub fn offset(&self) -> f32 {
432 self.inner.offset()
433 }
434
435 pub fn current_value(&self) -> T {
436 self.inner.current_value()
437 }
438
439 pub fn target_value(&self) -> Option<T> {
440 self.target.borrow().clone()
441 }
442
443 pub fn is_animating(&self) -> bool {
444 self.inner.is_animating()
445 }
446
447 pub fn snap_to(&self, value: &T) {
448 if self
449 .confirm_value_change
450 .borrow()
451 .as_ref()
452 .is_some_and(|f| !f(value.clone()))
453 {
454 return;
455 }
456 if let Some(offset) = self.inner.anchor_offset(value) {
457 *self.target.borrow_mut() = Some(value.clone());
458 self.inner.snap_to(offset);
459 }
460 }
461
462 pub fn animate_to(&self, value: &T) {
463 if self
464 .confirm_value_change
465 .borrow()
466 .as_ref()
467 .is_some_and(|f| !f(value.clone()))
468 {
469 return;
470 }
471 *self.target.borrow_mut() = Some(value.clone());
472 self.inner.animate_to(value);
473 }
474
475 pub fn on_pointer_down(&self, axis_pos: f32) {
476 self.inner.on_pointer_down(axis_pos);
477 }
478
479 pub fn on_pointer_move(&self, axis_pos: f32) {
480 self.inner.on_pointer_move(axis_pos);
481 }
482
483 pub fn on_pointer_up(&self) {
484 let candidate = self.inner.current_value();
485 if self
486 .confirm_value_change
487 .borrow()
488 .as_ref()
489 .is_some_and(|f| !f(candidate.clone()))
490 {
491 let settled = self.target.borrow().clone();
492 if let Some(settled) = settled {
493 self.inner.animate_to(&settled);
494 }
495 return;
496 }
497 *self.target.borrow_mut() = Some(candidate);
498 self.inner.on_pointer_up();
499 }
500}
501
502impl<T: Clone + PartialEq + 'static> Clone for AnchoredDraggableState<T> {
503 fn clone(&self) -> Self {
504 Self {
505 inner: self.inner.clone(),
506 config: self.config.clone(),
507 confirm_value_change: self.confirm_value_change.clone(),
508 target: self.target.clone(),
509 }
510 }
511}
512
513impl<T: Clone + PartialEq + 'static> Clone for SwipeableState<T> {
514 fn clone(&self) -> Self {
515 Self {
516 anim: self.anim.clone(),
517 anchors: self.anchors.clone(),
518 config: self.config.clone(),
519 drag_start: self.drag_start.clone(),
520 drag_base: self.drag_base.clone(),
521 last_move_time: self.last_move_time.clone(),
522 last_move_pos: self.last_move_pos.clone(),
523 release_velocity: self.release_velocity.clone(),
524 }
525 }
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531 use crate::runtime::HitRegion;
532 use std::cell::RefCell;
533 use std::rc::Rc;
534
535 #[test]
536 fn test_transformable_forwards_deltas() {
537 let seen = Rc::new(RefCell::new(Vec::new()));
538 let seen_pan = seen.clone();
539 let state = TransformableState::new(move |zoom, pan, rotation| {
540 seen_pan.borrow_mut().push((zoom, pan.x, rotation));
541 });
542 assert!(
543 state.handle_gesture(&crate::shortcuts::Gesture::PinchWithCenter {
544 delta_scale: 2.0,
545 center: crate::Vec2 { x: 1.0, y: 1.0 },
546 })
547 );
548 assert!(state.handle_gesture(&crate::shortcuts::Gesture::Pan {
549 delta: crate::Vec2 { x: 3.0, y: 4.0 },
550 center: crate::Vec2 { x: 0.0, y: 0.0 },
551 }));
552 assert!(!state.handle_gesture(&crate::shortcuts::Gesture::SwipeLeft));
553 assert_eq!(seen.borrow().len(), 2);
554 assert_eq!(seen.borrow()[0].0, 2.0);
555 assert_eq!(seen.borrow()[1].1, 3.0);
556 }
557
558 #[test]
559 fn test_anchored_draggable_vetoes() {
560 let state = AnchoredDraggableState::new(
561 vec![(0.0, "a"), (100.0, "b")],
562 SwipeableConfig::default(),
563 Some(Rc::new(|v: &'static str| v != "b")),
564 );
565 state.animate_to(&"b");
566 assert_eq!(state.target_value(), Some("a"));
567 state.animate_to(&"a");
568 assert_eq!(state.target_value(), Some("a"));
569 assert_eq!(state.anchors().len(), 2);
570 assert_eq!(state.anchor_offset_for(&"b"), Some(100.0));
571 }
572
573 #[test]
574 fn test_route_action_to_point_walks_up() {
575 let fired = Rc::new(RefCell::new(Vec::new()));
576 let fired_child = fired.clone();
577 let child = HitRegion {
578 id: 2,
579 parent: Some(1),
580 rect: crate::Rect {
581 x: 0.0,
582 y: 0.0,
583 w: 10.0,
584 h: 10.0,
585 },
586 z_index: 2.0,
587 on_action: Some(Rc::new(move |_| {
588 fired_child.borrow_mut().push(2u64);
589 false
590 })),
591 ..Default::default()
592 };
593 let parent = HitRegion {
594 id: 1,
595 rect: crate::Rect {
596 x: 0.0,
597 y: 0.0,
598 w: 100.0,
599 h: 100.0,
600 },
601 z_index: 1.0,
602 on_action: Some(Rc::new(move |_| true)),
603 ..Default::default()
604 };
605 let hits = vec![parent, child];
606 let action =
607 crate::shortcuts::Action::Gesture(crate::shortcuts::Gesture::PinchWithCenter {
608 delta_scale: 1.0,
609 center: crate::Vec2 { x: 5.0, y: 5.0 },
610 });
611 assert!(crate::runtime::route_action_to_point(
612 &hits,
613 &action,
614 crate::Vec2 { x: 5.0, y: 5.0 }
615 ));
616 assert_eq!(*fired.borrow(), vec![2u64]);
617 assert!(!crate::runtime::route_action_to_point(
618 &hits,
619 &action,
620 crate::Vec2 { x: 500.0, y: 500.0 }
621 ));
622 }
623}