use skia_safe::Point;
use crate::PointerKind;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum GestureKind {
Down,
Panning,
Tapped,
Up,
Wheel,
LongPressing,
Pointer,
PointerEnter,
PointerExit,
ContextMenu,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum MouseButton {
#[default]
Left,
Middle,
Right,
Back,
Forward,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum ContextMenuSource {
#[default]
Mouse,
Touch,
Keyboard,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct Gesture {
pub kind: GestureKind,
pub location: Point,
pub start: Point,
pub delta: Point,
pub total: Point,
pub time_ms: f64,
pub velocity: Point,
pub wheel: f32,
pub wheel_horizontal: bool,
pub cancelled: bool,
pub button: MouseButton,
pub source: ContextMenuSource,
pub touch: bool,
}
pub const TAPPED_CANCEL_MOVE_THRESHOLD_POINTS: f32 = 16.0;
pub const LONG_PRESS_MS: f64 = 1500.0;
const VELOCITY_WINDOW_MS: f64 = 16.0;
const VELOCITY_MIN_MS: f64 = 4.0;
const BURST_MS: f64 = 1.0;
#[derive(Clone, Copy, Default)]
struct Trail {
points: [(Point, f64); 16],
len: usize,
}
impl Trail {
fn push(&mut self, location: Point, time_ms: f64) {
if let Some(last) = self.points[..self.len].last_mut().filter(|(_, ms)| time_ms - ms < BURST_MS) {
last.0 = location;
return;
}
if self.len == self.points.len() {
self.points.copy_within(1.., 0);
self.len -= 1;
}
self.points[self.len] = (location, time_ms);
self.len += 1;
}
fn velocity(&self, location: Point, time_ms: f64, fallback: Point) -> Point {
let mut points = &self.points[..self.len];
if points.last().is_some_and(|(_, ms)| time_ms - ms < BURST_MS) {
points = &points[..points.len() - 1];
}
let Some(&first) = points.first() else { return fallback };
let since = time_ms - first.1;
if since < VELOCITY_MIN_MS {
return fallback;
}
let window = VELOCITY_WINDOW_MS.min(since);
let at = time_ms - window;
let &(last, last_ms) = points.last().expect("checked above");
if last_ms <= at {
let secs = ((time_ms - last_ms) / 1000.0) as f32;
return Point::new((location.x - last.x) / secs, (location.y - last.y) / secs);
}
let (mut from, mut previous) = (first.0, first);
for &(point, ms) in points[1..].iter().chain(std::iter::once(&(location, time_ms))) {
if ms >= at {
let (p0, t0) = previous;
let share = if ms - t0 < 1e-6 { 1.0 } else { ((at - t0) / (ms - t0)) as f32 };
from = Point::new(p0.x + (point.x - p0.x) * share, p0.y + (point.y - p0.y) * share);
break;
}
previous = (point, ms);
}
let secs = (window / 1000.0) as f32;
Point::new((location.x - from.x) / secs, (location.y - from.y) / secs)
}
}
#[derive(Clone, Copy)]
struct Pressed {
start: Point,
previous: Point,
velocity: Point,
trail: Trail,
button: MouseButton,
down_ms: f64,
long_press_done: bool,
long_pressed: bool,
touch: bool,
}
#[derive(Default)]
pub(crate) struct Recognizer {
down: Option<Pressed>,
}
impl Recognizer {
pub fn is_pressed(&self) -> bool {
self.down.is_some()
}
pub fn long_press_due(&self) -> Option<f64> {
self.down.filter(|p| !p.long_press_done).map(|p| p.down_ms + LONG_PRESS_MS)
}
pub fn long_press(&mut self, time_ms: f64) -> Option<Gesture> {
let pressed = self.down.as_mut().filter(|p| !p.long_press_done && time_ms >= p.down_ms + LONG_PRESS_MS)?;
(pressed.long_press_done, pressed.long_pressed) = (true, true);
let (start, button, touch) = (pressed.start, pressed.button, pressed.touch);
let zero = Point::default();
Some(Gesture {
kind: GestureKind::LongPressing,
location: start,
start,
delta: zero,
total: zero,
time_ms,
velocity: zero,
wheel: 0.0,
wheel_horizontal: false,
cancelled: false,
button,
source: ContextMenuSource::Touch,
touch,
})
}
pub fn feed(&mut self, pointer: PointerKind, button: MouseButton, touch: bool, location: Point, time_ms: f64, scale: f32) -> [Option<Gesture>; 2] {
let gesture = |kind, start: Point, previous: Point, velocity, button| Gesture {
kind,
location,
start,
delta: location - previous,
total: location - start,
time_ms,
velocity,
wheel: 0.0,
wheel_horizontal: false,
cancelled: kind == GestureKind::Up && pointer == PointerKind::Cancel,
button,
source: ContextMenuSource::Mouse,
touch,
};
let threshold = TAPPED_CANCEL_MOVE_THRESHOLD_POINTS * scale.max(0.1);
match (pointer, self.down) {
(PointerKind::Down, pressed) if pressed.is_none_or(|p| p.button == button) => {
let zero = Point::default();
let (start, previous, velocity, down_ms, long_press_done, long_pressed) = (location, location, zero, time_ms, false, false);
let mut trail = Trail::default();
trail.push(location, time_ms);
self.down = Some(Pressed { start, previous, velocity, trail, button, down_ms, long_press_done, long_pressed, touch });
[Some(gesture(GestureKind::Down, location, location, zero, button)), None]
}
(PointerKind::Move, Some(Pressed { start, previous, velocity, mut trail, button, down_ms, long_press_done, long_pressed, .. })) => {
let velocity = trail.velocity(location, time_ms, velocity);
trail.push(location, time_ms);
let total = location - start;
let long_press_done = long_press_done || total.x.abs() >= threshold || total.y.abs() >= threshold;
self.down = Some(Pressed { start, previous: location, velocity, trail, button, down_ms, long_press_done, long_pressed, touch });
if location == previous {
return [None, None];
}
[Some(gesture(GestureKind::Panning, start, previous, velocity, button)), None]
}
(PointerKind::Up, Some(Pressed { start, previous, velocity, button: pressed, long_pressed, .. })) if pressed == button => {
self.down = None;
let total = location - start;
let up = Some(gesture(GestureKind::Up, start, previous, velocity, button));
if !long_pressed && total.x.abs() < threshold && total.y.abs() < threshold {
[Some(gesture(GestureKind::Tapped, start, previous, velocity, button)), up]
} else {
[up, None]
}
}
(PointerKind::Cancel, Some(Pressed { start, previous, button, .. })) => {
self.down = None;
[Some(gesture(GestureKind::Up, start, previous, Point::default(), button)), None]
}
_ => [None, None],
}
}
}
#[derive(Clone, Copy, Default, Debug)]
pub struct VelocityAccumulator {
samples: [(Point, f64); Self::MAX_SAMPLES],
len: usize,
}
impl VelocityAccumulator {
const MAX_SAMPLES: usize = 5;
const CONSIDERATION_TIMEFRAME_MS: f64 = 150.0;
pub fn clear(&mut self) {
self.len = 0;
}
pub fn capture(&mut self, velocity: Point, time_ms: f64) {
if let Some(last) = self.samples[..self.len].last_mut().filter(|(_, ms)| time_ms - ms < BURST_MS) {
last.0 = velocity;
return;
}
if self.len == Self::MAX_SAMPLES {
self.samples.copy_within(1.., 0);
self.len -= 1;
}
self.samples[self.len] = (velocity, time_ms);
self.len += 1;
}
pub fn final_velocity(&self, now_ms: f64, clamp_absolute: f32) -> Point {
let (mut sum, mut weights) = (Point::default(), 0.0);
let relevant = self.samples[..self.len].iter().filter(|s| now_ms - s.1 <= Self::CONSIDERATION_TIMEFRAME_MS);
for (i, (velocity, _)) in relevant.enumerate() {
let weight = (i + 1) as f32;
sum += *velocity * weight;
weights += weight;
}
if weights == 0.0 {
return Point::default();
}
let limit = if clamp_absolute == 0.0 { f32::INFINITY } else { clamp_absolute };
Point::new((sum.x / weights).clamp(-limit, limit), (sum.y / weights).clamp(-limit, limit))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn moves_in_bursts_release_at_the_hands_speed() {
let (mut recognizer, mut accumulator) = (Recognizer::default(), VelocityAccumulator::default());
recognizer.feed(PointerKind::Down, MouseButton::Left, false, Point::default(), 0.0, 1.0);
let (mut y, mut time) = (0.0, 0.0);
for burst in 1..=8 {
for (later, dy) in [(0.0, 6.0), (0.01, 9.0)] {
(y, time) = (y + dy, burst as f64 * 15.0 + later);
if let [Some(gesture), _] = recognizer.feed(PointerKind::Move, MouseButton::Left, false, Point::new(0.0, y), time, 1.0) {
accumulator.capture(gesture.velocity, time);
}
}
}
let release = accumulator.final_velocity(time + 5.0, 0.0);
assert!((release.y - 1000.0).abs() < 2.0, "{release:?}");
}
}