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 snap_to(&self, off: f32) {
126 let clamped = self.clamp_offset(off);
127 self.anim.borrow_mut().snap_to(clamped);
128 request_frame();
129 }
130
131 pub fn animate_to(&self, value: &T) {
133 if let Some((offset, _)) = self.anchors.iter().find(|(_, v)| v == value) {
134 self.anim.borrow_mut().set_target(*offset);
135 request_frame();
136 }
137 }
138
139 pub fn is_animating(&self) -> bool {
141 self.anim.borrow().is_animating()
142 }
143
144 pub fn on_pointer_down(&self, axis_pos: f32) {
146 self.drag_start.set(Some(axis_pos));
147 self.drag_base.set(*self.anim.borrow().get());
148 self.last_move_time.set(Some(Instant::now()));
149 self.last_move_pos.set(Some(axis_pos));
150 self.release_velocity.set(0.0);
151 }
152
153 pub fn on_pointer_move(&self, axis_pos: f32) {
155 if let Some(start) = self.drag_start.get() {
156 let now = Instant::now();
157 let delta = axis_pos - start;
158 let new_offset = self.drag_base.get() + delta;
159
160 if let (Some(last_pos), Some(last_time)) =
162 (self.last_move_pos.get(), self.last_move_time.get())
163 {
164 let dt = (now - last_time).as_secs_f32().max(1.0 / 240.0);
165 let vel = (axis_pos - last_pos) / dt;
166 self.release_velocity.set(vel);
167 }
168
169 self.snap_to(new_offset);
170 self.last_move_pos.set(Some(axis_pos));
171 self.last_move_time.set(Some(now));
172 }
173 }
174
175 pub fn on_pointer_up(&self) {
178 self.drag_start.set(None);
179 let off = *self.anim.borrow().get();
180 let vel = self.release_velocity.get();
181 let config = &*self.config;
182
183 if vel.abs() > config.velocity_threshold {
185 let target_off = if vel < 0.0 {
186 self.next_anchor_down(off)
187 } else {
188 self.next_anchor_up(off)
189 };
190 self.anim.borrow_mut().set_target(target_off);
191 request_frame();
192 return;
193 }
194
195 let target_off = self.snap_target(off, config.positional_threshold);
198 self.anim.borrow_mut().set_target(target_off);
199 request_frame();
200 }
201
202 fn clamp_offset(&self, off: f32) -> f32 {
204 let min = self.anchors.first().map(|(o, _)| *o).unwrap_or(0.0);
205 let max = self.anchors.last().map(|(o, _)| *o).unwrap_or(0.0);
206 let lo = if min <= max {
207 min - self.config.max_overshoot
208 } else {
209 max - self.config.max_overshoot
210 };
211 let hi = if min <= max {
212 max + self.config.max_overshoot
213 } else {
214 min + self.config.max_overshoot
215 };
216 off.clamp(lo.min(hi), lo.max(hi))
217 }
218
219 fn nearest_anchor(&self, off: f32) -> Option<(f32, T)> {
221 self.anchors
222 .iter()
223 .min_by(|(a, _), (b, _)| (a - off).abs().partial_cmp(&(b - off).abs()).unwrap())
224 .map(|(o, v)| (*o, v.clone()))
225 }
226
227 fn next_anchor_down(&self, off: f32) -> f32 {
229 self.anchors
230 .iter()
231 .rev()
232 .find(|(a, _)| *a < off)
233 .map(|(o, _)| *o)
234 .unwrap_or_else(|| self.anchors.last().map(|(o, _)| *o).unwrap_or(off))
235 }
236
237 fn next_anchor_up(&self, off: f32) -> f32 {
239 self.anchors
240 .iter()
241 .find(|(a, _)| *a > off)
242 .map(|(o, _)| *o)
243 .unwrap_or_else(|| self.anchors.first().map(|(o, _)| *o).unwrap_or(off))
244 }
245
246 fn snap_target(&self, off: f32, threshold: f32) -> f32 {
248 let upper = self.anchors.iter().find(|(a, _)| *a >= off);
250 let lower = self.anchors.iter().rev().find(|(a, _)| *a <= off);
251
252 match (lower, upper) {
253 (Some((lo, _)), Some((hi, _))) if lo != hi => {
254 let range = hi - lo;
256 if range == 0.0 {
257 *lo
258 } else {
259 let fraction = (off - lo) / range;
260 if fraction >= threshold { *hi } else { *lo }
261 }
262 }
263 (Some((lo, _)), _) => {
264 *lo
266 }
267 (_, Some((hi, _))) => {
268 *hi
270 }
271 _ => off,
272 }
273 }
274}
275
276impl<T: Clone + PartialEq + 'static> Clone for SwipeableState<T> {
277 fn clone(&self) -> Self {
278 Self {
279 anim: self.anim.clone(),
280 anchors: self.anchors.clone(),
281 config: self.config.clone(),
282 drag_start: self.drag_start.clone(),
283 drag_base: self.drag_base.clone(),
284 last_move_time: self.last_move_time.clone(),
285 last_move_pos: self.last_move_pos.clone(),
286 release_velocity: self.release_velocity.clone(),
287 }
288 }
289}