1use crate::Vec2;
2use crate::animation::{AnimatedValue, AnimationSpec};
3use crate::frame_clock::request_frame;
4use std::cell::{Cell, RefCell};
5use std::rc::Rc;
6use web_time::Instant;
7
8#[derive(Clone, Copy, Debug, PartialEq)]
10pub enum DragOrientation {
11 Horizontal,
12 Vertical,
13}
14
15#[derive(Clone)]
17pub struct SwipeableConfig {
18 pub animation_spec: AnimationSpec,
20 pub positional_threshold: f32,
23 pub velocity_threshold: f32,
27 pub max_overshoot: f32,
29}
30
31impl Default for SwipeableConfig {
32 fn default() -> Self {
33 Self {
34 animation_spec: AnimationSpec::spring_gentle(),
35 positional_threshold: 0.3,
36 velocity_threshold: f32::MAX,
37 max_overshoot: 0.0,
38 }
39 }
40}
41
42pub struct SwipeableState<T: Clone + PartialEq + 'static> {
68 anim: Rc<RefCell<AnimatedValue<f32>>>,
69 anchors: Rc<Vec<(f32, T)>>,
70 config: Rc<SwipeableConfig>,
71 drag_start: Rc<Cell<Option<f32>>>,
72 drag_base: Rc<Cell<f32>>,
73 last_move_time: Rc<Cell<Option<Instant>>>,
74 last_move_pos: Rc<Cell<Option<f32>>>,
75 release_velocity: Rc<Cell<f32>>,
76}
77
78impl<T: Clone + PartialEq + 'static> SwipeableState<T> {
79 pub fn new(anchors: Vec<(f32, T)>, config: SwipeableConfig) -> Self {
84 assert!(
85 !anchors.is_empty(),
86 "SwipeableState requires at least one anchor"
87 );
88 let initial_offset = anchors[0].0;
89 Self {
90 anim: Rc::new(RefCell::new(AnimatedValue::new(
91 initial_offset,
92 config.animation_spec.clone(),
93 ))),
94 anchors: Rc::new(anchors),
95 config: Rc::new(config),
96 drag_start: Rc::new(Cell::new(None)),
97 drag_base: Rc::new(Cell::new(initial_offset)),
98 last_move_time: Rc::new(Cell::new(None)),
99 last_move_pos: Rc::new(Cell::new(None)),
100 release_velocity: Rc::new(Cell::new(0.0)),
101 }
102 }
103
104 pub fn offset(&self) -> f32 {
107 let mut anim = self.anim.borrow_mut();
108 if anim.update() {
109 request_frame();
110 }
111 *anim.get()
112 }
113
114 pub fn current_value(&self) -> T {
116 let off = *self.anim.borrow().get();
117 self.nearest_anchor(off).map(|(_, v)| v).unwrap_or_else(|| {
118 self.anchors
119 .first()
120 .map(|(_, v)| v.clone())
121 .unwrap_or_else(|| panic!("SwipeableState has no anchors"))
122 })
123 }
124
125 pub fn snap_to(&self, off: f32) {
127 let clamped = self.clamp_offset(off);
128 self.anim.borrow_mut().snap_to(clamped);
129 request_frame();
130 }
131
132 pub fn animate_to(&self, value: &T) {
134 if let Some((offset, _)) = self.anchors.iter().find(|(_, v)| v == value) {
135 self.anim.borrow_mut().set_target(*offset);
136 request_frame();
137 }
138 }
139
140 pub fn is_animating(&self) -> bool {
142 self.anim.borrow().is_animating()
143 }
144
145 pub fn on_pointer_down(&self, axis_pos: f32) {
147 self.drag_start.set(Some(axis_pos));
148 self.drag_base.set(*self.anim.borrow().get());
149 self.last_move_time.set(Some(Instant::now()));
150 self.last_move_pos.set(Some(axis_pos));
151 self.release_velocity.set(0.0);
152 }
153
154 pub fn on_pointer_move(&self, axis_pos: f32) {
156 if let Some(start) = self.drag_start.get() {
157 let now = Instant::now();
158 let delta = axis_pos - start;
159 let new_offset = self.drag_base.get() + delta;
160
161 if let (Some(last_pos), Some(last_time)) =
163 (self.last_move_pos.get(), self.last_move_time.get())
164 {
165 let dt = (now - last_time).as_secs_f32().max(1.0 / 240.0);
166 let vel = (axis_pos - last_pos) / dt;
167 self.release_velocity.set(vel);
168 }
169
170 self.snap_to(new_offset);
171 self.last_move_pos.set(Some(axis_pos));
172 self.last_move_time.set(Some(now));
173 }
174 }
175
176 pub fn on_pointer_up(&self) {
179 self.drag_start.set(None);
180 let off = *self.anim.borrow().get();
181 let vel = self.release_velocity.get();
182 let config = &*self.config;
183
184 if vel.abs() > config.velocity_threshold {
186 let target_off = if vel < 0.0 {
187 self.next_anchor_down(off)
188 } else {
189 self.next_anchor_up(off)
190 };
191 self.anim.borrow_mut().set_target(target_off);
192 request_frame();
193 return;
194 }
195
196 let target_off = self.snap_target(off, config.positional_threshold);
199 self.anim.borrow_mut().set_target(target_off);
200 request_frame();
201 }
202
203 fn clamp_offset(&self, off: f32) -> f32 {
205 let min = self.anchors.first().map(|(o, _)| *o).unwrap_or(0.0);
206 let max = self.anchors.last().map(|(o, _)| *o).unwrap_or(0.0);
207 let lo = if min <= max {
208 min - self.config.max_overshoot
209 } else {
210 max - self.config.max_overshoot
211 };
212 let hi = if min <= max {
213 max + self.config.max_overshoot
214 } else {
215 min + self.config.max_overshoot
216 };
217 off.clamp(lo.min(hi), lo.max(hi))
218 }
219
220 fn nearest_anchor(&self, off: f32) -> Option<(f32, T)> {
222 self.anchors
223 .iter()
224 .min_by(|(a, _), (b, _)| (a - off).abs().partial_cmp(&(b - off).abs()).unwrap())
225 .map(|(o, v)| (*o, v.clone()))
226 }
227
228 fn next_anchor_down(&self, off: f32) -> f32 {
230 self.anchors
231 .iter()
232 .rev()
233 .find(|(a, _)| *a < off)
234 .map(|(o, _)| *o)
235 .unwrap_or_else(|| self.anchors.last().map(|(o, _)| *o).unwrap_or(off))
236 }
237
238 fn next_anchor_up(&self, off: f32) -> f32 {
240 self.anchors
241 .iter()
242 .find(|(a, _)| *a > off)
243 .map(|(o, _)| *o)
244 .unwrap_or_else(|| self.anchors.first().map(|(o, _)| *o).unwrap_or(off))
245 }
246
247 fn snap_target(&self, off: f32, threshold: f32) -> f32 {
249 let upper = self.anchors.iter().find(|(a, _)| *a >= off);
251 let lower = self.anchors.iter().rev().find(|(a, _)| *a <= off);
252
253 match (lower, upper) {
254 (Some((lo, _)), Some((hi, _))) if lo != hi => {
255 let range = hi - lo;
257 if range == 0.0 {
258 *lo
259 } else {
260 let fraction = (off - lo) / range;
261 if fraction >= threshold { *hi } else { *lo }
262 }
263 }
264 (Some((lo, _)), _) => {
265 *lo
267 }
268 (_, Some((hi, _))) => {
269 *hi
271 }
272 _ => off,
273 }
274 }
275}
276
277impl<T: Clone + PartialEq + 'static> Clone for SwipeableState<T> {
278 fn clone(&self) -> Self {
279 Self {
280 anim: self.anim.clone(),
281 anchors: self.anchors.clone(),
282 config: self.config.clone(),
283 drag_start: self.drag_start.clone(),
284 drag_base: self.drag_base.clone(),
285 last_move_time: self.last_move_time.clone(),
286 last_move_pos: self.last_move_pos.clone(),
287 release_velocity: self.release_velocity.clone(),
288 }
289 }
290}