mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 10:17:02 -06:00
Preferences › Devices › Enable Touch Gestures (on by default) was stored but nothing read it: no touch gesture was implemented. - `touch::Taps` follows the fingers egui reports (winit reads them from the system: WM_POINTER on Windows): fingers down together and lifted within half a second, none moved more than 16 points, is a tap. Two fingers undo, three redo, with the preference on. - egui makes the first finger the pointer too, so it has pressed on the canvas by the time a second lands: that press is dropped then (its tool interaction rolled back), and the rest of the gesture presses nothing. The tap so undoes what came before it, not the first finger's mark. - A pen is one contact, so pen strokes and taps are unaffected; pinch zoom is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
132 lines
5.3 KiB
Rust
132 lines
5.3 KiB
Rust
//! Touch gestures on the canvas (Preferences › Devices › Enable Touch Gestures, #585): a tap with
|
||
//! two fingers undoes, one with three redoes.
|
||
//!
|
||
//! egui hands every finger's contact over as [`egui::Event::Touch`] (winit reads them from the
|
||
//! system: `WM_POINTER` on Windows), and makes the first finger the mouse pointer too. So the first
|
||
//! finger has pressed on the canvas by the time a second lands: that press is dropped then
|
||
//! ([`Taps::feed`] says [`Touch::Fingers`]), and the tap undoes what came before it.
|
||
|
||
use egui::{Pos2, TouchId, TouchPhase};
|
||
|
||
/// How long the fingers of a tap may stay down, in seconds.
|
||
const TAP_TIME: f64 = 0.5;
|
||
/// How far (points) a finger of a tap may move: further, it is a pinch or a pan.
|
||
const TAP_SLOP: f32 = 16.0;
|
||
|
||
/// What a touch event made of the gesture.
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub(crate) enum Touch {
|
||
/// A second finger landed: the gesture isn't the first finger's press.
|
||
Fingers,
|
||
/// All fingers lifted from a tap of this many fingers (two or more).
|
||
Tap(usize),
|
||
}
|
||
|
||
/// The fingers down and how the gesture they make has gone so far.
|
||
#[derive(Clone, Debug, Default)]
|
||
pub(crate) struct Taps {
|
||
/// Each finger down and where it landed.
|
||
down: Vec<(TouchId, Pos2)>,
|
||
/// The most fingers down at once in this gesture.
|
||
most: usize,
|
||
/// When its first finger landed.
|
||
start: f64,
|
||
/// A finger moved too far or stayed too long: not a tap.
|
||
spoilt: bool,
|
||
}
|
||
|
||
impl Taps {
|
||
/// Follow a finger's `phase` at `pos` (touch `id`) at time `now` (seconds).
|
||
pub(crate) fn feed(&mut self, id: TouchId, phase: TouchPhase, pos: Pos2, now: f64) -> Option<Touch> {
|
||
match phase {
|
||
TouchPhase::Start => {
|
||
if self.down.is_empty() {
|
||
*self = Self { start: now, ..Self::default() };
|
||
}
|
||
if !self.down.iter().any(|(d, _)| *d == id) {
|
||
self.down.push((id, pos));
|
||
}
|
||
let before = self.most;
|
||
self.most = self.most.max(self.down.len());
|
||
(before < 2 && self.most >= 2).then_some(Touch::Fingers)
|
||
}
|
||
TouchPhase::Move => {
|
||
if self.down.iter().any(|(d, at)| *d == id && at.distance(pos) > TAP_SLOP) {
|
||
self.spoilt = true;
|
||
}
|
||
None
|
||
}
|
||
TouchPhase::End | TouchPhase::Cancel => {
|
||
let known = self.down.len();
|
||
self.down.retain(|(d, _)| *d != id);
|
||
if self.down.len() == known {
|
||
return None;
|
||
}
|
||
self.spoilt |= phase == TouchPhase::Cancel || now - self.start > TAP_TIME;
|
||
(self.down.is_empty() && !self.spoilt && self.most >= 2).then_some(Touch::Tap(self.most))
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A finger is down.
|
||
pub(crate) fn is_down(&self) -> bool {
|
||
!self.down.is_empty()
|
||
}
|
||
}
|
||
|
||
/// The command a tap of `fingers` runs: two undo, three redo.
|
||
pub(crate) fn tap_command(fingers: usize) -> Option<&'static str> {
|
||
match fingers {
|
||
2 => Some("edit.undo"),
|
||
3 => Some("edit.redo"),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn id(n: u64) -> TouchId {
|
||
TouchId(n)
|
||
}
|
||
|
||
#[test]
|
||
fn two_and_three_finger_taps() {
|
||
let mut t = Taps::default();
|
||
let p = Pos2::new(100.0, 100.0);
|
||
assert_eq!(t.feed(id(1), TouchPhase::Start, p, 0.0), None);
|
||
assert_eq!(t.feed(id(2), TouchPhase::Start, p + egui::vec2(60.0, 0.0), 0.05), Some(Touch::Fingers));
|
||
assert!(t.is_down());
|
||
assert_eq!(t.feed(id(1), TouchPhase::Move, p + egui::vec2(3.0, 2.0), 0.1), None);
|
||
assert_eq!(t.feed(id(1), TouchPhase::End, p, 0.15), None, "one finger still down");
|
||
assert_eq!(t.feed(id(2), TouchPhase::End, p, 0.2), Some(Touch::Tap(2)));
|
||
assert!(!t.is_down());
|
||
// Three fingers.
|
||
for (n, at) in [(4, 0.0), (5, 0.02), (6, 0.04)] {
|
||
t.feed(id(n), TouchPhase::Start, p, 1.0 + at);
|
||
}
|
||
let ends: Vec<_> = [4, 5, 6].into_iter().map(|n| t.feed(id(n), TouchPhase::End, p, 1.2)).collect();
|
||
assert_eq!(ends, [None, None, Some(Touch::Tap(3))]);
|
||
assert_eq!((tap_command(2), tap_command(3), tap_command(1)), (Some("edit.undo"), Some("edit.redo"), None));
|
||
}
|
||
|
||
#[test]
|
||
fn a_pinch_a_long_press_a_cancel_or_one_finger_is_no_tap() {
|
||
let p = Pos2::new(100.0, 100.0);
|
||
let gesture = |moved: f32, held: f64, last: TouchPhase, fingers: u64| {
|
||
let mut t = Taps::default();
|
||
for n in 0..fingers {
|
||
t.feed(id(n), TouchPhase::Start, p, 0.0);
|
||
}
|
||
t.feed(id(0), TouchPhase::Move, p + egui::vec2(moved, 0.0), held / 2.0);
|
||
(0..fingers).filter_map(|n| t.feed(id(n), if n + 1 == fingers { last } else { TouchPhase::End }, p, held)).last()
|
||
};
|
||
assert_eq!(gesture(0.0, 0.2, TouchPhase::End, 2), Some(Touch::Tap(2)));
|
||
assert_eq!(gesture(40.0, 0.2, TouchPhase::End, 2), None, "a pinch");
|
||
assert_eq!(gesture(0.0, 0.9, TouchPhase::End, 2), None, "held too long");
|
||
assert_eq!(gesture(0.0, 0.2, TouchPhase::Cancel, 2), None, "cancelled");
|
||
assert_eq!(gesture(0.0, 0.2, TouchPhase::End, 1), None, "a pen or one finger");
|
||
}
|
||
}
|