1#[cfg(any(feature = "std", feature = "alloc"))]
4use crate::inertia::{InertiaBounds, InertiaConfig, InertiaN};
5
6#[derive(Clone, Copy, Debug, PartialEq)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9pub struct PointerData {
10 pub x: f32,
12 pub y: f32,
14 pub pressure: f32,
16 pub pointer_id: u64,
18}
19
20impl PointerData {
21 pub fn new(x: f32, y: f32, pointer_id: u64) -> Self {
23 Self {
24 x,
25 y,
26 pressure: 1.0,
27 pointer_id,
28 }
29 }
30
31 pub fn position(&self) -> [f32; 2] {
33 [self.x, self.y]
34 }
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub enum DragAxis {
41 Both,
43 X,
45 Y,
47}
48
49#[derive(Clone, Copy, Debug, PartialEq)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52pub struct DragConstraints {
53 pub min_x: Option<f32>,
55 pub max_x: Option<f32>,
57 pub min_y: Option<f32>,
59 pub max_y: Option<f32>,
61 pub grid_snap: Option<f32>,
63}
64
65impl Default for DragConstraints {
66 fn default() -> Self {
67 Self::unbounded()
68 }
69}
70
71impl DragConstraints {
72 pub fn unbounded() -> Self {
74 Self {
75 min_x: None,
76 max_x: None,
77 min_y: None,
78 max_y: None,
79 grid_snap: None,
80 }
81 }
82
83 pub fn bounded(min_x: f32, max_x: f32, min_y: f32, max_y: f32) -> Self {
85 Self {
86 min_x: Some(min_x),
87 max_x: Some(max_x),
88 min_y: Some(min_y),
89 max_y: Some(max_y),
90 grid_snap: None,
91 }
92 }
93
94 pub fn with_grid_snap(mut self, grid: f32) -> Self {
96 self.grid_snap = if grid.is_finite() && grid > 0.0 {
97 Some(grid)
98 } else {
99 None
100 };
101 self
102 }
103
104 pub fn constrain(
106 &self,
107 position: [f32; 2],
108 axis: DragAxis,
109 locked_origin: [f32; 2],
110 ) -> [f32; 2] {
111 let mut x = finite_or_zero(position[0]);
112 let mut y = finite_or_zero(position[1]);
113
114 match axis {
115 DragAxis::Both => {}
116 DragAxis::X => y = locked_origin[1],
117 DragAxis::Y => x = locked_origin[0],
118 }
119
120 x = clamp_optional(x, self.min_x, self.max_x);
121 y = clamp_optional(y, self.min_y, self.max_y);
122
123 if let Some(grid) = self.grid_snap {
124 x = snap(x, grid);
125 y = snap(y, grid);
126 x = clamp_optional(x, self.min_x, self.max_x);
127 y = clamp_optional(y, self.min_y, self.max_y);
128 }
129
130 [x, y]
131 }
132}
133
134#[cfg(any(feature = "std", feature = "alloc"))]
139#[derive(Clone, Debug)]
140#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
141pub struct DragState {
142 position: [f32; 2],
143 start_position: [f32; 2],
144 start_pointer: [f32; 2],
145 last_position: [f32; 2],
146 velocity: [f32; 2],
147 active_pointer_id: Option<u64>,
148 axis: DragAxis,
149 constraints: DragConstraints,
150 inertia_config: InertiaConfig<[f32; 2]>,
151 velocity_smoothing: f32,
152}
153
154#[cfg(any(feature = "std", feature = "alloc"))]
155impl DragState {
156 pub fn new(position: [f32; 2]) -> Self {
158 Self {
159 position,
160 start_position: position,
161 start_pointer: [0.0, 0.0],
162 last_position: position,
163 velocity: [0.0, 0.0],
164 active_pointer_id: None,
165 axis: DragAxis::Both,
166 constraints: DragConstraints::unbounded(),
167 inertia_config: InertiaConfig::new(1400.0, 2.0),
168 velocity_smoothing: 0.35,
169 }
170 }
171
172 pub fn axis(mut self, axis: DragAxis) -> Self {
174 self.axis = axis;
175 self
176 }
177
178 pub fn constraints(mut self, constraints: DragConstraints) -> Self {
180 self.constraints = constraints;
181 self
182 }
183
184 pub fn inertia_config(mut self, config: InertiaConfig<[f32; 2]>) -> Self {
186 self.inertia_config = config;
187 self
188 }
189
190 pub fn velocity_smoothing(mut self, smoothing: f32) -> Self {
192 self.velocity_smoothing = smoothing.clamp(0.0, 1.0);
193 self
194 }
195
196 pub fn position(&self) -> [f32; 2] {
198 self.position
199 }
200
201 pub fn velocity(&self) -> [f32; 2] {
203 self.velocity
204 }
205
206 pub fn is_dragging(&self) -> bool {
208 self.active_pointer_id.is_some()
209 }
210
211 pub fn active_pointer_id(&self) -> Option<u64> {
213 self.active_pointer_id
214 }
215
216 pub fn on_pointer_down(&mut self, data: PointerData) {
218 self.active_pointer_id = Some(data.pointer_id);
219 self.start_pointer = data.position();
220 self.start_position = self.position;
221 self.last_position = self.position;
222 self.velocity = [0.0, 0.0];
223 }
224
225 pub fn on_pointer_move(&mut self, data: PointerData, dt: f32) {
227 if self.active_pointer_id != Some(data.pointer_id) {
228 return;
229 }
230
231 let delta = [
232 data.x - self.start_pointer[0],
233 data.y - self.start_pointer[1],
234 ];
235 let raw_position = [
236 self.start_position[0] + delta[0],
237 self.start_position[1] + delta[1],
238 ];
239 let constrained = self
240 .constraints
241 .constrain(raw_position, self.axis, self.start_position);
242
243 let dt = dt.max(0.0);
244 if dt > 0.0 {
245 let instant = [
246 (constrained[0] - self.last_position[0]) / dt,
247 (constrained[1] - self.last_position[1]) / dt,
248 ];
249 let alpha = self.velocity_smoothing;
250 self.velocity = [
251 self.velocity[0] * (1.0 - alpha) + instant[0] * alpha,
252 self.velocity[1] * (1.0 - alpha) + instant[1] * alpha,
253 ];
254 }
255
256 self.position = constrained;
257 self.last_position = constrained;
258 }
259
260 pub fn on_pointer_up(&mut self, data: PointerData) -> Option<InertiaN<[f32; 2]>> {
262 if self.active_pointer_id != Some(data.pointer_id) {
263 return None;
264 }
265
266 self.active_pointer_id = None;
267 let velocity = match self.axis {
268 DragAxis::Both => self.velocity,
269 DragAxis::X => [self.velocity[0], 0.0],
270 DragAxis::Y => [0.0, self.velocity[1]],
271 };
272
273 if velocity[0].abs() <= self.inertia_config.min_velocity
274 && velocity[1].abs() <= self.inertia_config.min_velocity
275 {
276 self.velocity = [0.0, 0.0];
277 return None;
278 }
279
280 let mut config = self.inertia_config.clone();
281 config.bounds = self.bounds_for_inertia();
282 let mut inertia = InertiaN::new(config, self.position);
283 inertia.kick(velocity);
284 Some(inertia)
285 }
286
287 fn bounds_for_inertia(&self) -> Option<InertiaBounds<[f32; 2]>> {
288 match (
289 self.constraints.min_x,
290 self.constraints.max_x,
291 self.constraints.min_y,
292 self.constraints.max_y,
293 ) {
294 (Some(min_x), Some(max_x), Some(min_y), Some(max_y)) => {
295 Some(InertiaBounds::new([min_x, min_y], [max_x, max_y]))
296 }
297 _ => self.inertia_config.bounds.clone(),
298 }
299 }
300}
301
302#[inline]
303fn finite_or_zero(value: f32) -> f32 {
304 if value.is_finite() { value } else { 0.0 }
305}
306
307#[inline]
308fn clamp_optional(value: f32, min: Option<f32>, max: Option<f32>) -> f32 {
309 match (min, max) {
310 (Some(a), Some(b)) => value.clamp(a.min(b), a.max(b)),
311 (Some(min), None) => value.max(min),
312 (None, Some(max)) => value.min(max),
313 (None, None) => value,
314 }
315}
316
317#[inline]
318fn snap(value: f32, grid: f32) -> f32 {
319 if grid > 0.0 {
320 libm::roundf(value / grid) * grid
321 } else {
322 value
323 }
324}
325
326#[cfg(all(test, any(feature = "std", feature = "alloc")))]
327mod tests {
328 use super::*;
329
330 #[cfg(any(feature = "std", feature = "alloc"))]
331 #[test]
332 fn drag_respects_axis_and_constraints() {
333 let mut drag = DragState::new([0.0, 5.0])
334 .axis(DragAxis::X)
335 .constraints(DragConstraints::bounded(-10.0, 10.0, -10.0, 10.0));
336
337 drag.on_pointer_down(PointerData::new(0.0, 0.0, 1));
338 drag.on_pointer_move(PointerData::new(30.0, 40.0, 1), 0.016);
339
340 assert_eq!(drag.position(), [10.0, 5.0]);
341 }
342
343 #[cfg(any(feature = "std", feature = "alloc"))]
344 #[test]
345 fn drag_ignores_wrong_pointer_id() {
346 let mut drag = DragState::new([0.0, 0.0]);
347 drag.on_pointer_down(PointerData::new(0.0, 0.0, 7));
348 drag.on_pointer_move(PointerData::new(20.0, 0.0, 8), 0.016);
349 assert_eq!(drag.position(), [0.0, 0.0]);
350 }
351
352 #[cfg(any(feature = "std", feature = "alloc"))]
353 #[test]
354 fn drag_estimates_velocity_with_ema() {
355 let mut drag = DragState::new([0.0, 0.0]).velocity_smoothing(1.0);
356 drag.on_pointer_down(PointerData::new(0.0, 0.0, 1));
357 drag.on_pointer_move(PointerData::new(16.0, 0.0, 1), 0.016);
358 assert!((drag.velocity()[0] - 1000.0).abs() < 0.01);
359 assert_eq!(drag.velocity()[1], 0.0);
360 }
361
362 #[cfg(any(feature = "std", feature = "alloc"))]
363 #[test]
364 fn grid_snap_applies_to_position() {
365 let mut drag = DragState::new([0.0, 0.0])
366 .constraints(DragConstraints::unbounded().with_grid_snap(10.0));
367 drag.on_pointer_down(PointerData::new(0.0, 0.0, 1));
368 drag.on_pointer_move(PointerData::new(16.0, 24.0, 1), 0.016);
369 assert_eq!(drag.position(), [20.0, 20.0]);
370 }
371}