// Touch input: turns the raw touches of macroquad into gestures. // one finger: press, tap, double tap, long press, drag (start / move / end), release // two fingers: camera gesture (pan by the centre, pinch = zoom, twist = rotate) // Positions are device pixels (the unit of the interface, see ui::begin_ui). use macroquad::prelude::*; const LONG_PRESS: f64 = 0.5; // seconds without moving const DOUBLE_TAP: f64 = 0.3; // max. time between two taps // touches come in device pixels; everything here works in interface units (see ui::dpr) fn ui_pos(p: Vec2) -> Vec2 { p * crate::ui::dpr() / crate::ui::device_dpr() } const DRAG_CSS: f32 = 10.0; // movement (CSS pixels) that turns a press into a drag #[derive(Clone, Copy, Debug)] pub enum Gesture { Press(Vec2), Tap(Vec2), DoubleTap(Vec2), LongPress(Vec2), DragStart(Vec2), // start position of the drag DragMove(Vec2), DragEnd, Release(Vec2), Cancel, // a second finger arrived: abort the one-finger action Camera { center: Vec2, pan: Vec2, zoom: f32, rotate: f32 }, } #[derive(Clone, Copy)] enum St { Idle, One { id: u64, start: Vec2, pos: Vec2, t0: f64, drag: bool, long: bool }, Two { a: u64, b: u64, center: Vec2, dist: f32, angle: f32 }, Wait, // after a two-finger gesture: until all fingers are lifted } pub struct TouchInput { st: St, last_tap: Option<(Vec2, f64)>, pending_tap: Option<(Vec2, f64)>, // single tap held back while a double tap is possible pub seen: bool, // a touch happened since the start } fn ended(p: TouchPhase) -> bool { matches!(p, TouchPhase::Ended | TouchPhase::Cancelled) } impl TouchInput { pub fn new() -> TouchInput { TouchInput { st: St::Idle, last_tap: None, pending_tap: None, seen: false } } // a finger is on the screen (one-finger state) pub fn finger(&self) -> Option { match self.st { St::One { pos, .. } => Some(pos), _ => None, } } fn tap(&mut self, p: Vec2, now: f64, want_double: bool, out: &mut Vec) { let near = |a: Vec2, b: Vec2| a.distance(b) < DRAG_CSS * 3.0 * crate::ui::dpr(); if want_double { if let Some((q, t)) = self.pending_tap { if now - t < DOUBLE_TAP && near(p, q) { self.pending_tap = None; out.push(Gesture::DoubleTap(p)); return; } out.push(Gesture::Tap(q)); } self.pending_tap = Some((p, now)); } else { if let Some((q, _)) = self.pending_tap.take() { out.push(Gesture::Tap(q)); } out.push(Gesture::Tap(p)); } self.last_tap = Some((p, now)); } // want_double: taps are held back briefly so that a double tap can be recognised pub fn update(&mut self, now: f64, want_double: bool) -> Vec { let mut out = Vec::new(); let ts = touches(); if !ts.is_empty() { self.seen = true; } let find = |id: u64| ts.iter().find(|t| t.id == id); let active: Vec<&Touch> = ts.iter().filter(|t| !ended(t.phase)).collect(); let drag_px = DRAG_CSS * crate::ui::dpr(); match self.st { St::Idle => { if active.len() >= 2 { self.start_two(active[0], active[1]); } else if let Some(t) = active.first() { out.push(Gesture::Press(ui_pos(t.position))); self.st = St::One { id: t.id, start: ui_pos(t.position), pos: ui_pos(t.position), t0: now, drag: false, long: false }; } else if let Some(t) = ts.iter().find(|t| ended(t.phase)) { // started and ended between two frames out.push(Gesture::Press(ui_pos(t.position))); self.tap(ui_pos(t.position), now, want_double, &mut out); out.push(Gesture::Release(ui_pos(t.position))); } } St::One { id, start, t0, mut drag, mut long, .. } => { if active.len() >= 2 { out.push(Gesture::Cancel); self.start_two(active[0], active[1]); } else { match find(id) { Some(t) if !ended(t.phase) => { let pos = ui_pos(t.position); if !drag && pos.distance(start) > drag_px { drag = true; out.push(Gesture::DragStart(start)); } if drag { out.push(Gesture::DragMove(pos)); } else if !long && now - t0 > LONG_PRESS { long = true; out.push(Gesture::LongPress(pos)); } self.st = St::One { id, start, pos, t0, drag, long }; } other => { let pos = other.map(|t| ui_pos(t.position)).unwrap_or(start); if drag { out.push(Gesture::DragEnd); } else if !long { self.tap(pos, now, want_double, &mut out); } out.push(Gesture::Release(pos)); self.st = St::Idle; } } } } St::Two { a, b, center, dist, angle } => match (find(a).filter(|t| !ended(t.phase)), find(b).filter(|t| !ended(t.phase))) { (Some(ta), Some(tb)) => { let (c, d, an) = Self::geometry(ui_pos(ta.position), ui_pos(tb.position)); let mut rot = an - angle; if rot > std::f32::consts::PI { rot -= std::f32::consts::TAU; } else if rot < -std::f32::consts::PI { rot += std::f32::consts::TAU; } out.push(Gesture::Camera { center: c, pan: c - center, zoom: d / dist.max(1.0), rotate: rot }); self.st = St::Two { a, b, center: c, dist: d, angle: an }; } _ => self.st = if active.is_empty() { St::Idle } else { St::Wait }, }, St::Wait => { if active.is_empty() { self.st = St::Idle; } } } if let Some((p, t)) = self.pending_tap { if !want_double || now - t >= DOUBLE_TAP { self.pending_tap = None; out.push(Gesture::Tap(p)); } } out } fn geometry(p: Vec2, q: Vec2) -> (Vec2, f32, f32) { let d = q - p; ((p + q) * 0.5, d.length(), d.y.atan2(d.x)) } fn start_two(&mut self, a: &Touch, b: &Touch) { let (center, dist, angle) = Self::geometry(ui_pos(a.position), ui_pos(b.position)); self.pending_tap = None; self.st = St::Two { a: a.id, b: b.id, center, dist, angle }; } }