use std::{
collections::{HashMap, VecDeque},
time::{Duration, Instant},
};
use crate::PxPosition;
const KEEP_EVENTS_COUNT: usize = 10;
const INERTIA_DECAY_CONSTANT: f32 = 5.0;
const MIN_INERTIA_VELOCITY: f32 = 10.0;
const INERTIA_MIN_VELOCITY_THRESHOLD_FOR_START: f32 = 50.0;
const INERTIA_MOMENTUM_FACTOR: f32 = 1.0;
const MAX_INERTIA_VELOCITY: f32 = 6000.0;
#[derive(Debug, Clone)]
struct TouchPointState {
last_position: PxPosition,
last_update_time: Instant,
velocity_tracker: VelocityTracker,
generated_scroll_event: bool,
}
#[derive(Debug, Clone)]
struct VelocityTracker {
samples: VecDeque<(Instant, f32, f32)>,
last_sample_time: Instant,
}
const VELOCITY_SAMPLE_WINDOW: Duration = Duration::from_millis(90);
const VELOCITY_IDLE_CUTOFF: Duration = Duration::from_millis(65);
#[derive(Debug, Clone)]
struct ActiveInertia {
velocity_x: f32,
velocity_y: f32,
last_tick_time: Instant,
}
fn clamp_inertia_velocity(vx: f32, vy: f32) -> (f32, f32) {
if !vx.is_finite() || !vy.is_finite() {
return (0.0, 0.0);
}
let magnitude_sq = vx * vx + vy * vy;
if !magnitude_sq.is_finite() {
return (0.0, 0.0);
}
let magnitude = magnitude_sq.sqrt();
if magnitude > MAX_INERTIA_VELOCITY && MAX_INERTIA_VELOCITY > 0.0 {
let scale = MAX_INERTIA_VELOCITY / magnitude;
return (vx * scale, vy * scale);
}
(vx, vy)
}
#[derive(Debug, Clone)]
struct TouchScrollConfig {
min_move_threshold: f32,
enabled: bool,
}
impl Default for TouchScrollConfig {
fn default() -> Self {
Self {
min_move_threshold: 5.0,
enabled: true,
}
}
}
#[derive(Default)]
pub struct CursorState {
position: Option<PxPosition>,
events: VecDeque<CursorEvent>,
touch_points: HashMap<u64, TouchPointState>,
touch_scroll_config: TouchScrollConfig,
active_inertia: Option<ActiveInertia>,
clear_position_on_next_frame: bool,
}
impl CursorState {
pub(crate) fn frame_cleanup(&mut self) {
if self.clear_position_on_next_frame {
self.update_position(None);
self.clear_position_on_next_frame = false;
}
}
pub fn push_event(&mut self, event: CursorEvent) {
self.events.push_back(event);
if self.events.len() > KEEP_EVENTS_COUNT {
self.events.pop_front();
}
}
pub fn update_position(&mut self, position: impl Into<Option<PxPosition>>) {
self.position = position.into();
}
fn process_and_queue_inertial_scroll(&mut self) {
if let Some(mut inertia) = self.active_inertia.take() {
let now = Instant::now();
let delta_time = now.duration_since(inertia.last_tick_time).as_secs_f32();
if delta_time <= 0.0 {
self.active_inertia = Some(inertia);
return;
}
let scroll_delta_x = inertia.velocity_x * delta_time;
let scroll_delta_y = inertia.velocity_y * delta_time;
if scroll_delta_x.abs() > 0.01 || scroll_delta_y.abs() > 0.01 {
self.push_scroll_event(now, scroll_delta_x, scroll_delta_y);
}
let decay = (-INERTIA_DECAY_CONSTANT * delta_time).exp();
inertia.velocity_x *= decay;
inertia.velocity_y *= decay;
inertia.last_tick_time = now;
if inertia.velocity_x.abs() >= MIN_INERTIA_VELOCITY
|| inertia.velocity_y.abs() >= MIN_INERTIA_VELOCITY
{
self.active_inertia = Some(inertia);
}
}
}
fn push_scroll_event(&mut self, timestamp: Instant, dx: f32, dy: f32) {
self.push_event(CursorEvent {
timestamp,
content: CursorEventContent::Scroll(ScrollEventConent {
delta_x: dx,
delta_y: dy,
}),
gesture_state: GestureState::Dragged,
});
}
pub fn take_events(&mut self) -> Vec<CursorEvent> {
self.process_and_queue_inertial_scroll();
self.events.drain(..).collect()
}
pub fn clear(&mut self) {
self.events.clear();
self.update_position(None);
self.active_inertia = None;
self.touch_points.clear();
self.clear_position_on_next_frame = false;
}
pub fn position(&self) -> Option<PxPosition> {
self.position
}
pub fn handle_touch_start(&mut self, touch_id: u64, position: PxPosition) {
self.active_inertia = None; let now = Instant::now();
self.touch_points.insert(
touch_id,
TouchPointState {
last_position: position,
last_update_time: now,
velocity_tracker: VelocityTracker::new(now),
generated_scroll_event: false,
},
);
self.update_position(position);
let press_event = CursorEvent {
timestamp: now,
content: CursorEventContent::Pressed(PressKeyEventType::Left),
gesture_state: GestureState::TapCandidate,
};
self.push_event(press_event);
}
pub fn handle_touch_move(
&mut self,
touch_id: u64,
current_position: PxPosition,
) -> Option<CursorEvent> {
let now = Instant::now();
self.update_position(current_position);
if !self.touch_scroll_config.enabled {
return None;
}
if let Some(touch_state) = self.touch_points.get_mut(&touch_id) {
let delta_x = (current_position.x - touch_state.last_position.x).to_f32();
let delta_y = (current_position.y - touch_state.last_position.y).to_f32();
let move_distance = (delta_x * delta_x + delta_y * delta_y).sqrt();
let time_delta = now
.duration_since(touch_state.last_update_time)
.as_secs_f32();
touch_state.last_position = current_position;
touch_state.last_update_time = now;
if move_distance >= self.touch_scroll_config.min_move_threshold {
self.active_inertia = None;
if time_delta > 0.0 {
let velocity_x = delta_x / time_delta;
let velocity_y = delta_y / time_delta;
touch_state
.velocity_tracker
.push(now, velocity_x, velocity_y);
}
touch_state.generated_scroll_event = true;
return Some(CursorEvent {
timestamp: now,
content: CursorEventContent::Scroll(ScrollEventConent {
delta_x, delta_y,
}),
gesture_state: GestureState::Dragged,
});
}
}
None
}
pub fn handle_touch_end(&mut self, touch_id: u64) {
let now = Instant::now();
let mut was_drag = false;
if let Some(touch_state) = self.touch_points.get_mut(&touch_id) {
was_drag |= touch_state.generated_scroll_event;
if self.touch_scroll_config.enabled {
if let Some((avg_vx, avg_vy)) = touch_state.velocity_tracker.resolve(now) {
let velocity_magnitude = (avg_vx * avg_vx + avg_vy * avg_vy).sqrt();
if velocity_magnitude > INERTIA_MIN_VELOCITY_THRESHOLD_FOR_START {
let (inertia_vx, inertia_vy) = clamp_inertia_velocity(
avg_vx * INERTIA_MOMENTUM_FACTOR,
avg_vy * INERTIA_MOMENTUM_FACTOR,
);
self.active_inertia = Some(ActiveInertia {
velocity_x: inertia_vx,
velocity_y: inertia_vy,
last_tick_time: now,
});
} else {
self.active_inertia = None;
}
} else {
self.active_inertia = None;
}
} else {
self.active_inertia = None; }
} else {
self.active_inertia = None; }
if self.active_inertia.is_some() {
was_drag = true;
}
self.touch_points.remove(&touch_id);
let release_event = CursorEvent {
timestamp: now,
content: CursorEventContent::Released(PressKeyEventType::Left),
gesture_state: if was_drag {
GestureState::Dragged
} else {
GestureState::TapCandidate
},
};
self.push_event(release_event);
if self.touch_points.is_empty() && self.active_inertia.is_none() {
self.clear_position_on_next_frame = true;
}
}
}
impl VelocityTracker {
fn new(now: Instant) -> Self {
Self {
samples: VecDeque::new(),
last_sample_time: now,
}
}
fn push(&mut self, now: Instant, vx: f32, vy: f32) {
let (vx, vy) = clamp_inertia_velocity(vx, vy);
self.samples.push_back((now, vx, vy));
self.last_sample_time = now;
self.prune(now);
}
fn resolve(&mut self, now: Instant) -> Option<(f32, f32)> {
self.prune(now);
if self.samples.is_empty() {
return None;
}
let idle_time = now.duration_since(self.last_sample_time);
if idle_time >= VELOCITY_IDLE_CUTOFF {
self.samples.clear();
return None;
}
let mut weighted_sum_x = 0.0f32;
let mut weighted_sum_y = 0.0f32;
let mut total_weight = 0.0f32;
let window_secs = VELOCITY_SAMPLE_WINDOW.as_secs_f32().max(f32::EPSILON);
for &(timestamp, vx, vy) in &self.samples {
let age_secs = now
.duration_since(timestamp)
.as_secs_f32()
.clamp(0.0, window_secs);
let weight = (window_secs - age_secs).max(0.0);
if weight > 0.0 {
weighted_sum_x += vx * weight;
weighted_sum_y += vy * weight;
total_weight += weight;
}
}
if total_weight <= f32::EPSILON {
self.samples.clear();
return None;
}
let avg_x = weighted_sum_x / total_weight;
let avg_y = weighted_sum_y / total_weight;
let damping = 1.0 - idle_time.as_secs_f32() / VELOCITY_IDLE_CUTOFF.as_secs_f32();
let damping = damping.clamp(0.0, 1.0);
let (avg_x, avg_y) = clamp_inertia_velocity(avg_x * damping, avg_y * damping);
Some((avg_x, avg_y))
}
fn prune(&mut self, now: Instant) {
while let Some(&(timestamp, _, _)) = self.samples.front() {
if now.duration_since(timestamp) > VELOCITY_SAMPLE_WINDOW {
self.samples.pop_front();
} else {
break;
}
}
}
}
#[derive(Debug, Clone)]
pub struct CursorEvent {
pub timestamp: Instant,
pub content: CursorEventContent,
pub gesture_state: GestureState,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ScrollEventConent {
pub delta_x: f32,
pub delta_y: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CursorEventContent {
Pressed(PressKeyEventType),
Released(PressKeyEventType),
Scroll(ScrollEventConent),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GestureState {
#[default]
TapCandidate,
Dragged,
}
impl CursorEventContent {
pub fn from_press_event(
state: winit::event::ElementState,
button: winit::event::MouseButton,
) -> Option<Self> {
let event_type = match button {
winit::event::MouseButton::Left => PressKeyEventType::Left,
winit::event::MouseButton::Right => PressKeyEventType::Right,
winit::event::MouseButton::Middle => PressKeyEventType::Middle,
_ => return None, };
let state = match state {
winit::event::ElementState::Pressed => Self::Pressed(event_type),
winit::event::ElementState::Released => Self::Released(event_type),
};
Some(state)
}
pub fn from_scroll_event(delta: winit::event::MouseScrollDelta) -> Self {
let (delta_x, delta_y) = match delta {
winit::event::MouseScrollDelta::LineDelta(x, y) => (x, y),
winit::event::MouseScrollDelta::PixelDelta(delta) => (delta.x as f32, delta.y as f32),
};
const MOUSE_WHEEL_SPEED_MULTIPLIER: f32 = 50.0;
Self::Scroll(ScrollEventConent {
delta_x: delta_x * MOUSE_WHEEL_SPEED_MULTIPLIER,
delta_y: delta_y * MOUSE_WHEEL_SPEED_MULTIPLIER,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PressKeyEventType {
Left,
Right,
Middle,
}