use std::cell::RefCell;
use std::collections::BTreeMap;
use crate::geometry::Point;
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TouchDeviceId(pub u64);
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TouchId(pub u64);
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum TouchPhase {
Start,
Move,
End,
Cancel,
}
#[derive(Copy, Clone, Debug)]
pub struct MultiTouchInfo {
pub device_id: TouchDeviceId,
pub num_touches: usize,
pub zoom_delta: f32,
pub rotation_delta: f32,
pub translation_delta: Point,
pub force: f32,
pub center_pos: Point,
}
#[derive(Copy, Clone, Debug)]
struct ActiveTouch {
pos: Point,
prev_pos: Point,
force: f32,
}
#[derive(Default)]
pub struct TouchState {
active: BTreeMap<(TouchDeviceId, TouchId), ActiveTouch>,
last: Option<MultiTouchInfo>,
topology_changed: bool,
}
impl TouchState {
pub fn new() -> Self {
Self::default()
}
pub fn on_start(&mut self, device: TouchDeviceId, id: TouchId, pos: Point, force: Option<f32>) {
self.active.insert(
(device, id),
ActiveTouch {
pos,
prev_pos: pos,
force: force.unwrap_or(0.0),
},
);
self.topology_changed = true;
}
pub fn on_move(&mut self, device: TouchDeviceId, id: TouchId, pos: Point, force: Option<f32>) {
if let Some(t) = self.active.get_mut(&(device, id)) {
t.pos = pos;
if let Some(f) = force {
t.force = f;
}
}
}
pub fn on_end_or_cancel(&mut self, device: TouchDeviceId, id: TouchId) {
if self.active.remove(&(device, id)).is_some() {
self.topology_changed = true;
}
if self.active.len() < 2 {
self.last = None;
}
}
pub fn update_gesture(&mut self) {
let device = self.active.keys().next().map(|(d, _)| *d);
let Some(device) = device else {
self.last = None;
return;
};
let touches: Vec<ActiveTouch> = self
.active
.iter()
.filter(|((d, _), _)| *d == device)
.map(|(_, t)| *t)
.collect();
if touches.len() < 2 {
self.last = None;
return;
}
let n = touches.len() as f64;
let (mut cx, mut cy) = (0.0, 0.0);
let (mut pcx, mut pcy) = (0.0, 0.0);
for t in &touches {
cx += t.pos.x;
cy += t.pos.y;
pcx += t.prev_pos.x;
pcy += t.prev_pos.y;
}
cx /= n;
cy /= n;
pcx /= n;
pcy /= n;
let mut zoom_sum = 0.0_f32;
let mut rotation_sum = 0.0_f32;
let mut force_sum = 0.0_f32;
let mut zoom_count = 0;
for t in &touches {
force_sum += t.force;
let dx = (t.pos.x - cx) as f32;
let dy = (t.pos.y - cy) as f32;
let pdx = (t.prev_pos.x - pcx) as f32;
let pdy = (t.prev_pos.y - pcy) as f32;
let r = (dx * dx + dy * dy).sqrt();
let pr = (pdx * pdx + pdy * pdy).sqrt();
if pr > 1.0 && r > 1.0 {
zoom_sum += r / pr;
rotation_sum += dy.atan2(dx) - pdy.atan2(pdx);
zoom_count += 1;
}
}
let (zoom_delta, rotation_delta) = if self.topology_changed || zoom_count == 0 {
(1.0, 0.0)
} else {
let mut rot = rotation_sum / zoom_count as f32;
use std::f32::consts::PI;
while rot > PI {
rot -= 2.0 * PI;
}
while rot < -PI {
rot += 2.0 * PI;
}
(zoom_sum / zoom_count as f32, rot)
};
let translation_delta = if self.topology_changed {
Point::new(0.0, 0.0)
} else {
Point::new(cx - pcx, cy - pcy)
};
self.last = Some(MultiTouchInfo {
device_id: device,
num_touches: touches.len(),
zoom_delta,
rotation_delta,
translation_delta,
force: force_sum / n as f32,
center_pos: Point::new(cx, cy),
});
for t in self.active.values_mut() {
t.prev_pos = t.pos;
}
self.topology_changed = false;
}
pub fn current(&self) -> Option<MultiTouchInfo> {
self.last
}
pub fn active_count(&self) -> usize {
self.active.len()
}
}
thread_local! {
static CURRENT: RefCell<Option<MultiTouchInfo>> = RefCell::new(None);
static LAST_TOUCH_EVENT_AT: std::cell::Cell<Option<web_time::Instant>> =
const { std::cell::Cell::new(None) };
}
pub fn set_current(info: Option<MultiTouchInfo>) {
CURRENT.with(|c| *c.borrow_mut() = info);
}
pub fn current_multi_touch() -> Option<MultiTouchInfo> {
CURRENT.with(|c| *c.borrow())
}
pub(crate) fn note_touch_event() {
LAST_TOUCH_EVENT_AT.with(|c| c.set(Some(web_time::Instant::now())));
}
pub fn last_touch_event_age() -> Option<std::time::Duration> {
LAST_TOUCH_EVENT_AT.with(|c| c.get()).map(|t| t.elapsed())
}
#[doc(hidden)]
pub fn clear_last_touch_event_for_testing() {
LAST_TOUCH_EVENT_AT.with(|c| c.set(None));
}