Touch interface for phones and tablets; category toolbar for mouse and touch
- touch.rs: gesture recognition (tap, double tap, long press, drag, two-finger pan/pinch/rotate) - Touch mode: Auto (touch device or first touch) / On / Off in Settings -> Game - Larger interface factor on touch screens, top bar wraps, smaller minimap, fewer messages - Toolbar grouped into categories with pop-up rows and a menu button, also on the desktop - Long press shows information or drops held units; double tap with the hand slaps - Savegame name via the browser's text dialog when the name field is tapped - Touch help texts (EN/DE) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6f037294c0
commit
af6be62805
10 files changed
+744
-152
No files matched your search
+176
@@ -0,0 +1,176 @@
|
||||
// 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
|
||||
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<Vec2> {
|
||||
match self.st {
|
||||
St::One { pos, .. } => Some(pos),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn tap(&mut self, p: Vec2, now: f64, want_double: bool, out: &mut Vec<Gesture>) {
|
||||
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<Gesture> {
|
||||
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(t.position));
|
||||
self.st = St::One { id: t.id, start: t.position, 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(t.position));
|
||||
self.tap(t.position, now, want_double, &mut out);
|
||||
out.push(Gesture::Release(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 = 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| 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(ta.position, 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(a.position, b.position);
|
||||
self.pending_tap = None;
|
||||
self.st = St::Two { a: a.id, b: b.id, center, dist, angle };
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user