#[cfg(not(feature = "std"))]
#[allow(unused_imports)]
use num_traits::Float as _;
use crate::inertia::{InertiaConfig, InertiaN};
#[derive(Debug, Clone, Default)]
pub struct PointerData {
pub x: f32,
pub y: f32,
pub pressure: f32,
pub pointer_id: i32,
}
#[derive(Debug, Clone, Default)]
pub struct DragConstraints {
pub bounds: Option<[f32; 4]>,
pub axis_lock: Option<DragAxis>,
pub snap_to_grid: Option<[f32; 2]>,
pub snap_on_release: Option<[f32; 2]>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DragAxis {
X,
Y,
}
pub struct DragState {
position: [f32; 2],
velocity: [f32; 2],
dragging: bool,
start_pointer: [f32; 2],
start_position: [f32; 2],
last_pointer: [f32; 2],
constraints: DragConstraints,
#[cfg(feature = "std")]
on_drag_start_cb: Option<Box<dyn FnMut([f32; 2])>>,
#[cfg(feature = "std")]
#[allow(clippy::type_complexity)]
on_drag_end_cb: Option<Box<dyn FnMut([f32; 2], [f32; 2])>>,
#[cfg(feature = "std")]
on_click_cb: Option<Box<dyn FnMut([f32; 2])>>,
#[cfg(feature = "std")]
#[allow(clippy::type_complexity)]
on_throw_update_cb: Option<Box<dyn FnMut([f32; 2], [f32; 2])>>,
click_threshold: f32,
}
impl core::fmt::Debug for DragState {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("DragState")
.field("position", &self.position)
.field("velocity", &self.velocity)
.field("dragging", &self.dragging)
.field("start_pointer", &self.start_pointer)
.field("start_position", &self.start_position)
.field("last_pointer", &self.last_pointer)
.field("constraints", &self.constraints)
.field("click_threshold", &self.click_threshold)
.finish()
}
}
impl DragState {
pub fn new() -> Self {
Self {
position: [0.0, 0.0],
velocity: [0.0, 0.0],
dragging: false,
start_pointer: [0.0, 0.0],
start_position: [0.0, 0.0],
last_pointer: [0.0, 0.0],
constraints: DragConstraints::default(),
#[cfg(feature = "std")]
on_drag_start_cb: None,
#[cfg(feature = "std")]
on_drag_end_cb: None,
#[cfg(feature = "std")]
on_click_cb: None,
#[cfg(feature = "std")]
on_throw_update_cb: None,
click_threshold: 5.0,
}
}
pub fn with_constraints(mut self, constraints: DragConstraints) -> Self {
self.constraints = constraints;
self
}
pub fn with_position(mut self, position: [f32; 2]) -> Self {
self.position = position;
self
}
pub fn with_click_threshold(mut self, threshold: f32) -> Self {
self.click_threshold = threshold;
self
}
#[cfg(feature = "std")]
pub fn on_drag_start<F: FnMut([f32; 2]) + 'static>(&mut self, f: F) -> &mut Self {
self.on_drag_start_cb = Some(Box::new(f));
self
}
#[cfg(feature = "std")]
pub fn on_drag_end<F: FnMut([f32; 2], [f32; 2]) + 'static>(&mut self, f: F) -> &mut Self {
self.on_drag_end_cb = Some(Box::new(f));
self
}
#[cfg(feature = "std")]
pub fn on_click<F: FnMut([f32; 2]) + 'static>(&mut self, f: F) -> &mut Self {
self.on_click_cb = Some(Box::new(f));
self
}
#[cfg(feature = "std")]
pub fn on_throw_update<F: FnMut([f32; 2], [f32; 2]) + 'static>(&mut self, f: F) -> &mut Self {
self.on_throw_update_cb = Some(Box::new(f));
self
}
pub fn on_pointer_down(&mut self, x: f32, y: f32) {
self.dragging = true;
self.start_pointer = [x, y];
self.start_position = self.position;
self.last_pointer = [x, y];
self.velocity = [0.0, 0.0];
#[cfg(feature = "std")]
{
if let Some(ref mut cb) = self.on_drag_start_cb {
cb(self.position);
}
}
}
pub fn on_pointer_move(&mut self, x: f32, y: f32, dt: f32) {
if !self.dragging {
return;
}
let dx = x - self.start_pointer[0];
let dy = y - self.start_pointer[1];
let mut new_pos = [self.start_position[0] + dx, self.start_position[1] + dy];
new_pos = self.apply_constraints(new_pos);
if dt > 1e-6 {
let inst_vx = (x - self.last_pointer[0]) / dt;
let inst_vy = (y - self.last_pointer[1]) / dt;
self.velocity[0] = 0.8 * inst_vx + 0.2 * self.velocity[0];
self.velocity[1] = 0.8 * inst_vy + 0.2 * self.velocity[1];
}
self.position = new_pos;
self.last_pointer = [x, y];
}
pub fn on_pointer_up(&mut self) -> InertiaN<[f32; 2]> {
self.dragging = false;
let dx = self.last_pointer[0] - self.start_pointer[0];
let dy = self.last_pointer[1] - self.start_pointer[1];
#[allow(unused_variables)]
let distance = (dx * dx + dy * dy).sqrt();
if let Some(grid) = &self.constraints.snap_on_release {
if grid[0] > 0.0 {
self.position[0] = (self.position[0] / grid[0]).round() * grid[0];
}
if grid[1] > 0.0 {
self.position[1] = (self.position[1] / grid[1]).round() * grid[1];
}
}
#[cfg(feature = "std")]
{
if distance < self.click_threshold {
if let Some(ref mut cb) = self.on_click_cb {
cb(self.position);
}
}
if let Some(ref mut cb) = self.on_drag_end_cb {
cb(self.position, self.velocity);
}
}
InertiaN::new(InertiaConfig::default_flick(), self.position).with_velocity(self.velocity)
}
pub fn position(&self) -> [f32; 2] {
self.position
}
pub fn velocity(&self) -> [f32; 2] {
self.velocity
}
pub fn is_dragging(&self) -> bool {
self.dragging
}
fn apply_constraints(&self, mut pos: [f32; 2]) -> [f32; 2] {
if let Some(axis) = &self.constraints.axis_lock {
match axis {
DragAxis::X => pos[1] = self.start_position[1],
DragAxis::Y => pos[0] = self.start_position[0],
}
}
if let Some(bounds) = &self.constraints.bounds {
pos[0] = pos[0].clamp(bounds[0], bounds[2]);
pos[1] = pos[1].clamp(bounds[1], bounds[3]);
}
if let Some(grid) = &self.constraints.snap_to_grid {
if grid[0] > 0.0 {
pos[0] = (pos[0] / grid[0]).round() * grid[0];
}
if grid[1] > 0.0 {
pos[1] = (pos[1] / grid[1]).round() * grid[1];
}
}
pos
}
}
impl Default for DragState {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::Update;
#[test]
fn drag_basic_movement() {
let mut drag = DragState::new().with_position([100.0, 100.0]);
drag.on_pointer_down(50.0, 50.0);
drag.on_pointer_move(70.0, 60.0, 1.0 / 60.0);
assert_eq!(drag.position(), [120.0, 110.0]);
}
#[test]
fn drag_axis_lock_x() {
let mut drag = DragState::new().with_constraints(DragConstraints {
axis_lock: Some(DragAxis::X),
..Default::default()
});
drag.on_pointer_down(0.0, 0.0);
drag.on_pointer_move(50.0, 30.0, 1.0 / 60.0);
let pos = drag.position();
assert!((pos[0] - 50.0).abs() < 1e-6);
assert!((pos[1]).abs() < 1e-6, "Y should be locked: {}", pos[1]);
}
#[test]
fn drag_axis_lock_y() {
let mut drag = DragState::new().with_constraints(DragConstraints {
axis_lock: Some(DragAxis::Y),
..Default::default()
});
drag.on_pointer_down(0.0, 0.0);
drag.on_pointer_move(50.0, 30.0, 1.0 / 60.0);
let pos = drag.position();
assert!((pos[0]).abs() < 1e-6, "X should be locked: {}", pos[0]);
assert!((pos[1] - 30.0).abs() < 1e-6);
}
#[test]
fn drag_bounds_clamping() {
let mut drag = DragState::new().with_constraints(DragConstraints {
bounds: Some([0.0, 0.0, 100.0, 100.0]),
..Default::default()
});
drag.on_pointer_down(50.0, 50.0);
drag.on_pointer_move(200.0, 200.0, 1.0 / 60.0);
let pos = drag.position();
assert!(pos[0] <= 100.0, "X should be clamped: {}", pos[0]);
assert!(pos[1] <= 100.0, "Y should be clamped: {}", pos[1]);
}
#[test]
fn drag_grid_snapping() {
let mut drag = DragState::new().with_constraints(DragConstraints {
snap_to_grid: Some([10.0, 10.0]),
..Default::default()
});
drag.on_pointer_down(0.0, 0.0);
drag.on_pointer_move(17.0, 23.0, 1.0 / 60.0);
let pos = drag.position();
assert!(
(pos[0] - 20.0).abs() < 1e-6,
"X should snap to 20: {}",
pos[0]
);
assert!(
(pos[1] - 20.0).abs() < 1e-6,
"Y should snap to 20: {}",
pos[1]
);
}
#[test]
fn drag_velocity_tracking() {
let mut drag = DragState::new();
drag.on_pointer_down(0.0, 0.0);
drag.on_pointer_move(100.0, 0.0, 1.0 / 60.0);
let vel = drag.velocity();
assert!(vel[0] > 1000.0, "Expected large X velocity: {}", vel[0]);
}
#[test]
fn drag_pointer_up_returns_inertia() {
let mut drag = DragState::new();
drag.on_pointer_down(0.0, 0.0);
drag.on_pointer_move(50.0, 0.0, 1.0 / 60.0);
let mut inertia = drag.on_pointer_up();
assert!(!drag.is_dragging());
let pos_before = inertia.position();
inertia.update(1.0 / 60.0);
let pos_after = inertia.position();
assert!(
pos_after[0] > pos_before[0],
"Inertia should continue moving"
);
}
#[test]
fn drag_not_dragging_ignores_moves() {
let mut drag = DragState::new();
drag.on_pointer_move(100.0, 100.0, 1.0 / 60.0);
assert_eq!(drag.position(), [0.0, 0.0]);
}
}