493 lines
18 KiB
Rust
493 lines
18 KiB
Rust
use std::{
|
|
sync::{
|
|
Arc, Mutex,
|
|
mpsc::{self, RecvTimeoutError, TrySendError},
|
|
},
|
|
thread,
|
|
time::{Duration, SystemTime},
|
|
};
|
|
|
|
use evdev::{AbsoluteAxisCode, Device};
|
|
use eyre::eyre;
|
|
use tracing::{debug, error, info, warn};
|
|
|
|
use crate::{
|
|
constants::*,
|
|
state::{TouchState, TouchStateTracker},
|
|
};
|
|
|
|
pub(crate) trait TouchGestureInhibitor: Send {
|
|
fn next_should_inhibit(&mut self) -> eyre::Result<bool>;
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) enum Gesture {
|
|
/// X, Y coords
|
|
PointerMove(i32, i32),
|
|
/// Just a click
|
|
Click,
|
|
/// A long click
|
|
LongClick,
|
|
/// Start of a drag
|
|
DragStart,
|
|
/// End of a drag
|
|
DragEnd,
|
|
/// Vertical scrolling
|
|
VerticalScroll(i32),
|
|
/// Swiping
|
|
Swipe(SwipeGesture),
|
|
/// Double-tap on the top row of keys
|
|
TopRowDoubleTap(u32),
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) enum SwipeGesture {
|
|
Left,
|
|
Right,
|
|
Up,
|
|
Down,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
enum GestureInhibitionStatus {
|
|
/// No inhibition whatsoever
|
|
#[default]
|
|
Normal,
|
|
/// Actively inhibited
|
|
Inhibited,
|
|
/// Not inhibited anymore, but we still need to see a finger up event to fully cancel inhibition
|
|
WaitingForUp,
|
|
}
|
|
|
|
#[derive(Clone, Default)]
|
|
struct GestureInhibition(Arc<Mutex<GestureInhibitionStatus>>);
|
|
|
|
impl GestureInhibition {
|
|
fn should_inhibit(&self, ev: Option<&TouchState>) -> bool {
|
|
let mut lock = self.0.lock().unwrap();
|
|
match *lock {
|
|
GestureInhibitionStatus::Normal => false,
|
|
// Even if inhibited, if we see a finger up event, we can cancel touch inhibition
|
|
// This is because sometimes the finger up event can happen before the corresponding keyboard
|
|
// key up event.
|
|
GestureInhibitionStatus::WaitingForUp | GestureInhibitionStatus::Inhibited => {
|
|
if let Some(ev) = ev
|
|
&& !ev.down
|
|
{
|
|
*lock = GestureInhibitionStatus::Normal;
|
|
}
|
|
|
|
// Always block the last up event even if we ended up transitioning back to normal
|
|
// This helps prevent spurious single-clicks in case keyboard / touch is somewhat de-synced
|
|
// (e.g. we see the full key down -> up event sequence before we see any touch event)
|
|
// since returning true here also causes GestureDetector to reset the state
|
|
true
|
|
}
|
|
}
|
|
}
|
|
|
|
fn inhibit(&self) {
|
|
*self.0.lock().unwrap() = GestureInhibitionStatus::Inhibited;
|
|
}
|
|
|
|
fn uninhibit(&self) {
|
|
*self.0.lock().unwrap() = GestureInhibitionStatus::WaitingForUp
|
|
}
|
|
}
|
|
|
|
pub(crate) struct GestureDetector {
|
|
keyboard_features_enabled: bool,
|
|
inhibition: GestureInhibition,
|
|
event_rx: mpsc::Receiver<eyre::Result<TouchState>>,
|
|
gesture_tx: mpsc::SyncSender<eyre::Result<Gesture>>,
|
|
last_touch: Option<TouchState>,
|
|
first_down: Option<TouchState>,
|
|
max_x: i32,
|
|
max_y: i32,
|
|
/// Absolute values of deltaX / Y, accumulated since the last down event
|
|
/// This is used to detect long-tap-to-right-click
|
|
delta_x_abs_acc: u32,
|
|
delta_y_abs_acc: u32,
|
|
/// Same but not absolute values
|
|
delta_x_acc: i32,
|
|
delta_y_acc: i32,
|
|
/// If true, we have seen a single click and are waiting for further
|
|
/// events to decide whether this is "just" a single click or the start
|
|
/// of a double-click-and-drag gesture
|
|
single_click_pending: bool,
|
|
/// If true, we have seen the second down event for a double tap on the top row, and are waiting
|
|
/// for the next up.
|
|
top_row_double_tap_pending: bool,
|
|
dragging: bool,
|
|
/// Has a long-click been emitted for the current streak of touch down events?
|
|
long_click_emitted: bool,
|
|
}
|
|
|
|
impl GestureDetector {
|
|
pub(crate) fn start<I: 'static + TouchGestureInhibitor>(
|
|
keyboard_features_enabled: bool,
|
|
touchpad_dev: Device,
|
|
mut inhibitor: I,
|
|
) -> eyre::Result<impl Iterator<Item = eyre::Result<Gesture>>> {
|
|
// First acquire some basic properties of the device
|
|
let mut max_x = -1;
|
|
let mut max_y = -1;
|
|
for (axis, info) in touchpad_dev.get_absinfo()? {
|
|
if axis == AbsoluteAxisCode::ABS_MT_POSITION_X {
|
|
max_x = info.maximum();
|
|
} else if axis == AbsoluteAxisCode::ABS_MT_POSITION_Y {
|
|
max_y = info.maximum()
|
|
}
|
|
}
|
|
|
|
if max_x == -1 || max_y == -1 {
|
|
return Err(eyre!("No max X / Y coordinates available"));
|
|
}
|
|
|
|
info!("Touch device has max_x = {max_x}, max_y = {max_y}, assuming minimums are 0");
|
|
|
|
let (event_tx, event_rx) = mpsc::sync_channel(16);
|
|
thread::spawn(move || {
|
|
let tracker = TouchStateTracker::new(touchpad_dev);
|
|
|
|
for event in tracker {
|
|
if let Err(TrySendError::Disconnected(_)) = event_tx.try_send(event) {
|
|
// Terminate if the receiving end is dropped
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
let inhibition = GestureInhibition::default();
|
|
let _inhibition = inhibition.clone();
|
|
|
|
thread::spawn(move || {
|
|
while let Ok(inhibit) = inhibitor.next_should_inhibit() {
|
|
if inhibit {
|
|
debug!("Touch inhibition started!");
|
|
_inhibition.inhibit();
|
|
} else {
|
|
debug!("Touch inhibition ended! But might still need a touch up event!");
|
|
_inhibition.uninhibit();
|
|
}
|
|
}
|
|
});
|
|
|
|
let (gesture_tx, gesture_rx) = mpsc::sync_channel(16);
|
|
|
|
let state = GestureDetector {
|
|
keyboard_features_enabled,
|
|
inhibition,
|
|
event_rx,
|
|
gesture_tx,
|
|
last_touch: None,
|
|
first_down: None,
|
|
delta_x_abs_acc: 0,
|
|
delta_y_abs_acc: 0,
|
|
delta_x_acc: 0,
|
|
delta_y_acc: 0,
|
|
single_click_pending: false,
|
|
top_row_double_tap_pending: false,
|
|
dragging: false,
|
|
long_click_emitted: false,
|
|
max_x,
|
|
max_y,
|
|
};
|
|
|
|
thread::spawn(|| {
|
|
if let Err(e) = state.run() {
|
|
error!("Gesture detection loop exitted abnormally: {e:?}");
|
|
}
|
|
});
|
|
|
|
Ok(gesture_rx.into_iter())
|
|
}
|
|
|
|
fn run(mut self) -> eyre::Result<()> {
|
|
loop {
|
|
// If we just had a single click, then either it's the start of
|
|
// a double-click-to-drag gesture, or it's "just" a single click
|
|
// We need a timeout here because if it is a single click, we need to
|
|
// be able to emit the single click event without too much delay.
|
|
let timeout = if self.single_click_pending {
|
|
DOUBLE_CLICK_DRAG_TIMEOUT
|
|
} else if self.first_down.is_some() {
|
|
// In this case, this may be a long click, so we also need to be able to "wake up" in case nothing happens
|
|
LONG_CLICK_DURATION
|
|
} else {
|
|
Duration::from_secs(86400)
|
|
};
|
|
|
|
let touch = self.event_rx.recv_timeout(timeout);
|
|
|
|
if self
|
|
.inhibition
|
|
.should_inhibit(touch.as_ref().ok().and_then(|inner| inner.as_ref().ok()))
|
|
{
|
|
debug!("Input still inhibited!");
|
|
// We also need to drop any pending clicks here
|
|
self.reset_state(true)?;
|
|
continue;
|
|
}
|
|
|
|
let touch = match touch {
|
|
Ok(Ok(touch)) => {
|
|
if SystemTime::now()
|
|
.duration_since(touch.timestamp)
|
|
.unwrap_or(INVALID_DURATION)
|
|
> Duration::from_secs(1)
|
|
{
|
|
warn!("Received event that's way too old, ignoring");
|
|
continue;
|
|
} else {
|
|
touch
|
|
}
|
|
}
|
|
Ok(Err(e)) => {
|
|
self.emit(Err(e))?;
|
|
continue;
|
|
}
|
|
// Timed out after a single click, meaning that it truly was just a single click (not a double-click-to-drag)
|
|
Err(RecvTimeoutError::Timeout) if self.single_click_pending => {
|
|
self.single_click_pending = false;
|
|
// Now emit a single click
|
|
self.emit(Ok(Gesture::Click))?;
|
|
continue;
|
|
}
|
|
// Timed out after the first down event, we may have a long click
|
|
Err(RecvTimeoutError::Timeout) if self.first_down.is_some() => {
|
|
// We do still need to verify the delta x and y accumulators to ensure we haven't moved much
|
|
if self.no_significant_movement_since_down() && !self.long_click_emitted {
|
|
self.long_click_emitted = true;
|
|
self.emit(Ok(Gesture::LongClick))?;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
// Unknown error, break the loop
|
|
Err(e) => return Err(e.into()),
|
|
};
|
|
|
|
if self.single_click_pending {
|
|
if touch
|
|
.timestamp
|
|
.duration_since(self.last_touch.as_ref().unwrap().timestamp)
|
|
.unwrap_or(INVALID_DURATION)
|
|
>= DOUBLE_CLICK_DRAG_TIMEOUT
|
|
{
|
|
// Just a single click
|
|
self.single_click_pending = false;
|
|
self.emit(Ok(Gesture::Click))?;
|
|
} else if touch.down {
|
|
if self.keyboard_features_enabled
|
|
&& touch.y < ((self.max_y as f64) * KEYBOARD_TOP_ROW_HEIGHT) as i32
|
|
{
|
|
// Double-tap on the top row of keys
|
|
self.top_row_double_tap_pending = true;
|
|
} else {
|
|
// Drag started
|
|
self.dragging = true;
|
|
self.emit(Ok(Gesture::DragStart))?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reset the flag -- we don't need it if we got here at all
|
|
self.single_click_pending = false;
|
|
|
|
// The finger has been down for a while, could be a long click or just regular pointer movement
|
|
if touch.down
|
|
&& let Some(ref last_touch) = self.last_touch
|
|
&& last_touch.down
|
|
&& let Some(ref first_down) = self.first_down
|
|
{
|
|
if touch
|
|
.timestamp
|
|
.duration_since(first_down.timestamp)
|
|
.unwrap_or(INVALID_DURATION)
|
|
>= LONG_CLICK_DURATION
|
|
&& self.no_significant_movement_since_down()
|
|
&& !self.long_click_emitted
|
|
{
|
|
// This is a long click
|
|
self.long_click_emitted = true;
|
|
self.emit(Ok(Gesture::LongClick))?;
|
|
} else {
|
|
let delta_x = touch.x - last_touch.x;
|
|
let delta_y = touch.y - last_touch.y;
|
|
|
|
self.delta_x_abs_acc += delta_x.abs() as u32;
|
|
self.delta_y_abs_acc += delta_y.abs() as u32;
|
|
self.delta_x_acc += delta_x;
|
|
self.delta_y_acc += delta_y;
|
|
|
|
if (first_down.x as f64) < self.max_x as f64 * SCROLL_EDGE_VERTICAL_THRESHOLD
|
|
|| ((self.max_x - first_down.x) as f64)
|
|
< self.max_x as f64 * SCROLL_EDGE_VERTICAL_THRESHOLD
|
|
{
|
|
// This is vertical scroll (left or right edge)
|
|
self.emit(Ok(Gesture::VerticalScroll(delta_y)))?;
|
|
} else if self.try_detect_swipe(touch.timestamp).is_none() {
|
|
self.emit(Ok(Gesture::PointerMove(delta_x, delta_y)))?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// The finger just tapped once (down then up before SINGLE_CLICK_TIMEOUT)
|
|
// but we can't emit a single click just yet because this may be the start of a
|
|
// double-click-to-drag.
|
|
if let Some(ref first_down) = self.first_down
|
|
&& !touch.down
|
|
&& !self.dragging
|
|
&& !self.top_row_double_tap_pending
|
|
&& self.no_significant_movement_since_down()
|
|
&& self.try_detect_swipe(touch.timestamp).is_none()
|
|
&& touch
|
|
.timestamp
|
|
.duration_since(first_down.timestamp)
|
|
.unwrap_or(INVALID_DURATION)
|
|
< SINGLE_CLICK_TIMEOUT
|
|
{
|
|
self.single_click_pending = true;
|
|
}
|
|
|
|
if self.dragging && !touch.down {
|
|
self.dragging = false;
|
|
self.emit(Ok(Gesture::DragEnd))?;
|
|
}
|
|
|
|
self.last_touch = Some(touch.clone());
|
|
|
|
if touch.down {
|
|
if self.first_down.is_none() {
|
|
self.first_down = Some(touch.clone());
|
|
}
|
|
} else {
|
|
if let Some(swipe) = self.try_detect_swipe(touch.timestamp) {
|
|
self.emit(Ok(Gesture::Swipe(swipe)))?;
|
|
} else if self.top_row_double_tap_pending
|
|
&& touch.y < ((self.max_y as f64) * KEYBOARD_TOP_ROW_HEIGHT) as i32
|
|
{
|
|
// Now that we see an up, this is when we need to emit the top-row double tap event
|
|
// (NOT when the down was seen)
|
|
let mut key_idx = None;
|
|
for i in 0..KEYBOARD_TOP_ROW_KEY_BOUNDS.len() {
|
|
let lower_bound = if i == 0 {
|
|
0
|
|
} else {
|
|
KEYBOARD_TOP_ROW_KEY_BOUNDS[i - 1]
|
|
};
|
|
let upper_bound = KEYBOARD_TOP_ROW_KEY_BOUNDS[i];
|
|
// The equal case is required on both ends because we might have an x coordinate of 0 or 1440
|
|
if touch.x >= lower_bound && touch.x <= upper_bound {
|
|
key_idx = Some(i as u32);
|
|
}
|
|
}
|
|
self.emit(Ok(Gesture::TopRowDoubleTap(key_idx.unwrap())))?
|
|
}
|
|
|
|
self.reset_state(false)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Emits a gesture event or error, but skips if the gesture event channel is already full.
|
|
/// Returns an error if trhe gesture event channel is closed
|
|
fn emit(&self, gesture_or_err: eyre::Result<Gesture>) -> eyre::Result<()> {
|
|
if gesture_or_err.is_ok() {
|
|
if let Err(TrySendError::Disconnected(_)) = self.gesture_tx.try_send(gesture_or_err) {
|
|
return Err(eyre!("channel closed, shutting down"));
|
|
}
|
|
} else {
|
|
self.gesture_tx.send(gesture_or_err)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn reset_state(&mut self, clear_single_click: bool) -> eyre::Result<()> {
|
|
if self.dragging {
|
|
// if we're resetting state, we also need to tell everyone that we have stopped dragging
|
|
// because dragging is a state that needs to be synchronized with consumers
|
|
self.dragging = false;
|
|
self.emit(Ok(Gesture::DragEnd))?;
|
|
}
|
|
|
|
self.first_down = None;
|
|
self.delta_x_abs_acc = 0;
|
|
self.delta_y_abs_acc = 0;
|
|
self.delta_x_acc = 0;
|
|
self.delta_y_acc = 0;
|
|
self.long_click_emitted = false;
|
|
self.top_row_double_tap_pending = false;
|
|
|
|
// This flag is set if we truly want to clear _all_ state, for example, when input inhibition
|
|
// is triggered. It is _not_ set when we just want to reset most of the state when the finger lifts.
|
|
if clear_single_click {
|
|
self.single_click_pending = false;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn no_significant_movement_since_down(&self) -> bool {
|
|
(self.delta_x_abs_acc as f64) < self.max_x as f64 * NO_MOVEMENT_THRESHOLD
|
|
&& (self.delta_y_abs_acc as f64) < self.max_y as f64 * NO_MOVEMENT_THRESHOLD
|
|
}
|
|
|
|
fn try_detect_swipe(&self, now: SystemTime) -> Option<SwipeGesture> {
|
|
if self.dragging || !self.keyboard_features_enabled {
|
|
return None;
|
|
}
|
|
|
|
let Some(ref first_down) = self.first_down else {
|
|
return None;
|
|
};
|
|
|
|
let Ok(dur) = now.duration_since(first_down.timestamp) else {
|
|
return None;
|
|
};
|
|
|
|
if dur >= SWIPE_DURATION {
|
|
return None;
|
|
}
|
|
|
|
// We really want the same speed to apply to both X and Y directions,
|
|
// so choose the wider direction as baseline
|
|
let speed_ref = std::cmp::max(self.max_x, self.max_y) as f64;
|
|
|
|
let speed_x = (self.delta_x_acc as f64 / dur.as_secs_f64()) / speed_ref;
|
|
|
|
let res_x = if speed_x > SWIPE_SPEED_THRESHOLD {
|
|
Some(SwipeGesture::Right)
|
|
} else if speed_x < -SWIPE_SPEED_THRESHOLD {
|
|
Some(SwipeGesture::Left)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let speed_y = (self.delta_y_acc as f64 / dur.as_secs_f64()) / speed_ref;
|
|
|
|
let res_y = if speed_y > SWIPE_SPEED_THRESHOLD {
|
|
Some(SwipeGesture::Down)
|
|
} else if speed_y < -SWIPE_SPEED_THRESHOLD {
|
|
Some(SwipeGesture::Up)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
match (res_x, res_y) {
|
|
(None, None) => None,
|
|
(Some(res_x), None) => Some(res_x),
|
|
(None, Some(res_y)) => Some(res_y),
|
|
(Some(res_x), Some(res_y)) => {
|
|
// Choose the direction with bigger absolute speed
|
|
if speed_x.abs() > speed_y.abs() {
|
|
Some(res_x)
|
|
} else {
|
|
Some(res_y)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|