Skip to content

Commit

Permalink
Add touch input support
Browse files Browse the repository at this point in the history
This patch builds upon the prior work by @4z3 and @bytbox to add
touchscreen support to Alacritty. While some inspiration was taken from
@4z3's patch, it was rewritten from scratch.

This patch supports 4 basic touch interactions:
 - Tap
 - Scroll
 - Select
 - Zoom

Tap allows emulating the mouse to enter a single LMB click. While it
would be possible to add more complicated mouse emulation including
support for RMB and others, it's likely more confusing than anything
else and could conflict with other more useful touch actions.

Scroll and Select are started by horizontal or vertical dragging. While
selection isn't particularly accurate with a fat finger, it works
reasonably well and the separation from selection through horizontal and
vertical start feels pretty natural.

Since horizontal drag is reserved for selection we do not support
horizontal scrolling inside the terminal. While it would be possible to
somewhat support it by starting a selection with vertical movement and
then scrolling horizontally afterwards, it would likely just confuse
people so it was left out.

Zoom is pretty simple in just changing the font size when a two-finger
pinch gesture is used. Performance of this is pretty terrible especially
on low-end hardware since this obviously isn't a cheap operation, but it
seems like a worthwhile addition since small touchscreen devices are
most likely to need frequent font size adjustment to make output
readable.

Closes #3671.
  • Loading branch information
chrisduerr committed Feb 12, 2023
1 parent b0195eb commit 8add77f
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 12 deletions.
86 changes: 77 additions & 9 deletions alacritty/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use std::borrow::Cow;
use std::cmp::{max, min};
use std::collections::{HashMap, VecDeque};
use std::collections::{HashMap, HashSet, VecDeque};
use std::error::Error;
use std::ffi::OsStr;
use std::fmt::Debug;
Expand All @@ -19,7 +19,8 @@ use log::{debug, error, info, warn};
use wayland_client::{Display as WaylandDisplay, EventQueue};
use winit::dpi::PhysicalSize;
use winit::event::{
ElementState, Event as WinitEvent, Ime, ModifiersState, MouseButton, StartCause, WindowEvent,
ElementState, Event as WinitEvent, Ime, ModifiersState, MouseButton, StartCause,
Touch as TouchEvent, WindowEvent,
};
use winit::event_loop::{
ControlFlow, DeviceEventFilter, EventLoop, EventLoopProxy, EventLoopWindowTarget,
Expand Down Expand Up @@ -66,6 +67,9 @@ const MAX_SEARCH_WHILE_TYPING: Option<usize> = Some(1000);
/// Maximum number of search terms stored in the history.
const MAX_SEARCH_HISTORY_SIZE: usize = 255;

/// Touch zoom speed.
const TOUCH_ZOOM_FACTOR: f32 = 0.01;

/// Alacritty events.
#[derive(Debug, Clone)]
pub struct Event {
Expand Down Expand Up @@ -186,6 +190,7 @@ pub struct ActionContext<'a, N, T> {
pub terminal: &'a mut Term<T>,
pub clipboard: &'a mut Clipboard,
pub mouse: &'a mut Mouse,
pub touch: &'a mut Touch,
pub received_count: &'a mut usize,
pub suppress_chars: &'a mut bool,
pub modifiers: &'a mut ModifiersState,
Expand Down Expand Up @@ -338,6 +343,11 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
self.mouse
}

#[inline]
fn touch(&mut self) -> &mut Touch {
self.touch
}

#[inline]
fn received_count(&mut self) -> &mut usize {
self.received_count
Expand Down Expand Up @@ -1016,12 +1026,63 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> {
}
}

#[derive(Debug, Eq, PartialEq)]
pub enum ClickState {
/// Identified purpose of the touch input.
#[derive(Default, Debug)]
pub enum Touch {
#[default]
None,
Click,
DoubleClick,
TripleClick,
Select(TouchEvent),
Scroll(TouchEvent),
Zoom(TouchZoom),
Tap(TouchEvent),
Invalid(HashSet<u64>),
}

/// Touch zooming state.
#[derive(Debug)]
pub struct TouchZoom {
slots: (TouchEvent, TouchEvent),
fractions: f32,
}

impl TouchZoom {
pub fn new(slots: (TouchEvent, TouchEvent)) -> Self {
Self { slots, fractions: Default::default() }
}

/// Get slot distance change since last update.
pub fn font_delta(&mut self, slot: TouchEvent) -> f32 {
let old_distance = self.distance();

// Update touch slots.
if slot.id == self.slots.0.id {
self.slots.0 = slot;
} else {
self.slots.1 = slot;
}

// Calculate font change in `FONT_SIZE_STEP` increments.
let delta = (self.distance() - old_distance) * TOUCH_ZOOM_FACTOR + self.fractions;
let font_delta = (delta.abs() / FONT_SIZE_STEP).floor() * FONT_SIZE_STEP * delta.signum();
self.fractions = delta - font_delta;

font_delta
}

/// Get active touch slots.
pub fn slots(&self) -> HashSet<u64> {
let mut set = HashSet::new();
set.insert(self.slots.0.id);
set.insert(self.slots.1.id);
set
}

/// Calculate distance between slots.
fn distance(&self) -> f32 {
let delta_x = self.slots.0.location.x - self.slots.1.location.x;
let delta_y = self.slots.0.location.y - self.slots.1.location.y;
delta_x.hypot(delta_y) as f32
}
}

/// State of the mouse.
Expand Down Expand Up @@ -1081,6 +1142,14 @@ impl Mouse {
}
}

#[derive(Debug, Eq, PartialEq)]
pub enum ClickState {
None,
Click,
DoubleClick,
TripleClick,
}

/// The amount of scroll accumulated from the pointer events.
#[derive(Default, Debug)]
pub struct AccumulatedScroll {
Expand Down Expand Up @@ -1217,6 +1286,7 @@ impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> {
self.ctx.window().set_mouse_visible(true);
self.mouse_wheel_input(delta, phase);
},
WindowEvent::Touch(touch) => self.touch(touch),
WindowEvent::Focused(is_focused) => {
self.ctx.terminal.is_focused = is_focused;

Expand Down Expand Up @@ -1290,7 +1360,6 @@ impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> {
| WindowEvent::Destroyed
| WindowEvent::ThemeChanged(_)
| WindowEvent::HoveredFile(_)
| WindowEvent::Touch(_)
| WindowEvent::Moved(_) => (),
}
},
Expand Down Expand Up @@ -1592,7 +1661,6 @@ impl Processor {
| WindowEvent::HoveredFileCancelled
| WindowEvent::Destroyed
| WindowEvent::HoveredFile(_)
| WindowEvent::Touch(_)
| WindowEvent::Moved(_)
),
WinitEvent::Suspended { .. }
Expand Down
131 changes: 129 additions & 2 deletions alacritty/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@

use std::borrow::Cow;
use std::cmp::{max, min, Ordering};
use std::collections::HashSet;
use std::ffi::OsStr;
use std::fmt::Debug;
use std::marker::PhantomData;
use std::mem;
use std::time::{Duration, Instant};

use winit::dpi::PhysicalPosition;
use winit::event::{
ElementState, KeyboardInput, ModifiersState, MouseButton, MouseScrollDelta, TouchPhase,
ElementState, KeyboardInput, ModifiersState, MouseButton, MouseScrollDelta,
Touch as TouchEvent, TouchPhase,
};
use winit::event_loop::EventLoopWindowTarget;
#[cfg(target_os = "macos")]
Expand All @@ -35,7 +38,7 @@ use crate::config::{Action, BindingMode, Key, MouseAction, SearchAction, UiConfi
use crate::display::hint::HintMatch;
use crate::display::window::Window;
use crate::display::{Display, SizeInfo};
use crate::event::{ClickState, Event, EventType, Mouse, TYPING_SEARCH_DELAY};
use crate::event::{ClickState, Event, EventType, Mouse, Touch, TouchZoom, TYPING_SEARCH_DELAY};
use crate::message_bar::{self, Message};
use crate::scheduler::{Scheduler, TimerId, Topic};

Expand All @@ -51,6 +54,12 @@ const MIN_SELECTION_SCROLLING_HEIGHT: f64 = 5.;
/// Number of pixels for increasing the selection scrolling speed factor by one.
const SELECTION_SCROLLING_STEP: f64 = 20.;

/// Touch scroll speed.
const TOUCH_SCROLL_FACTOR: f64 = 0.35;

/// Distance before a touch input is considered a drag.
const MAX_TAP_DISTANCE: f64 = 20.;

/// Processes input from winit.
///
/// An escape sequence may be emitted in case specific keys or key combinations
Expand All @@ -72,6 +81,7 @@ pub trait ActionContext<T: EventListener> {
fn selection_is_empty(&self) -> bool;
fn mouse_mut(&mut self) -> &mut Mouse;
fn mouse(&self) -> &Mouse;
fn touch(&mut self) -> &mut Touch;
fn received_count(&mut self) -> &mut usize;
fn suppress_chars(&mut self) -> &mut bool;
fn modifiers(&mut self) -> &mut ModifiersState;
Expand Down Expand Up @@ -735,6 +745,118 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
}
}

/// Handle touch input.
pub fn touch(&mut self, touch: TouchEvent) {
match touch.phase {
TouchPhase::Started => self.on_touch_start(touch),
TouchPhase::Moved => self.on_touch_motion(touch),
TouchPhase::Ended | TouchPhase::Cancelled => self.on_touch_end(touch),
}
}

/// Handle beginning of touch input.
pub fn on_touch_start(&mut self, touch: TouchEvent) {
let touch_purpose = self.ctx.touch();
*touch_purpose = match mem::take(touch_purpose) {
Touch::None => Touch::Tap(touch),
Touch::Tap(start) => Touch::Zoom(TouchZoom::new((start, touch))),
Touch::Zoom(zoom) => Touch::Invalid(zoom.slots()),
Touch::Scroll(event) | Touch::Select(event) => {
let mut set = HashSet::new();
set.insert(event.id);
Touch::Invalid(set)
},
Touch::Invalid(mut slots) => {
slots.insert(touch.id);
Touch::Invalid(slots)
},
};
}

/// Handle touch input movement.
pub fn on_touch_motion(&mut self, touch: TouchEvent) {
let touch_purpose = self.ctx.touch();
match touch_purpose {
Touch::None => (),
// Handle transition from tap to scroll/select.
Touch::Tap(start) => {
let delta_x = touch.location.x - start.location.x;
let delta_y = touch.location.y - start.location.y;
if delta_x.abs() > MAX_TAP_DISTANCE {
// Update gesture state.
let start_location = start.location;
*touch_purpose = Touch::Select(*start);

// Start simulated mouse input.
self.mouse_moved(start_location);
self.mouse_input(ElementState::Pressed, MouseButton::Left);

// Apply motion since touch start.
self.on_touch_motion(touch);
} else if delta_y.abs() > MAX_TAP_DISTANCE {
// Update gesture state.
*touch_purpose = Touch::Scroll(*start);

// Apply motion since touch start.
self.on_touch_motion(touch);
}
},
Touch::Zoom(zoom) => {
let font_delta = zoom.font_delta(touch);
self.ctx.change_font_size(font_delta);
},
Touch::Scroll(last_touch) => {
// Calculate delta and update last touch position.
let delta_y = touch.location.y - last_touch.location.y;
*touch_purpose = Touch::Scroll(touch);

self.scroll_terminal(0., delta_y * TOUCH_SCROLL_FACTOR);
},
Touch::Select(_) => self.mouse_moved(touch.location),
Touch::Invalid(_) => (),
}
}

/// Handle end of touch input.
pub fn on_touch_end(&mut self, touch: TouchEvent) {
// Finalize the touch motion up to the release point.
self.on_touch_motion(touch);

let touch_purpose = self.ctx.touch();
match touch_purpose {
// Simulate LMB clicks.
Touch::Tap(start) => {
let start_location = start.location;
*touch_purpose = Default::default();

self.mouse_moved(start_location);
self.mouse_input(ElementState::Pressed, MouseButton::Left);
self.mouse_input(ElementState::Released, MouseButton::Left);
},
// Invalidate zoom once a finger was released.
Touch::Zoom(zoom) => {
let mut slots = zoom.slots();
slots.remove(&touch.id);
*touch_purpose = Touch::Invalid(slots);
},
// Reset touch state once all slots were released.
Touch::Invalid(slots) => {
slots.remove(&touch.id);
if slots.is_empty() {
*touch_purpose = Default::default();
}
},
// Release simulated LMB.
Touch::Select(_) => {
*touch_purpose = Default::default();
self.mouse_input(ElementState::Released, MouseButton::Left);
},
// Reset touch state on scroll finish.
Touch::Scroll(_) => *touch_purpose = Default::default(),
Touch::None => (),
}
}

pub fn mouse_input(&mut self, state: ElementState, button: MouseButton) {
match button {
MouseButton::Left => self.ctx.mouse_mut().left_button_state = state,
Expand Down Expand Up @@ -1093,6 +1215,11 @@ mod tests {
self.mouse
}

#[inline]
fn touch(&mut self) -> &mut Touch {
unimplemented!();
}

fn received_count(&mut self) -> &mut usize {
&mut self.received_count
}
Expand Down
5 changes: 4 additions & 1 deletion alacritty/src/window_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ use crate::clipboard::Clipboard;
use crate::config::UiConfig;
use crate::display::window::Window;
use crate::display::Display;
use crate::event::{ActionContext, Event, EventProxy, EventType, Mouse, SearchState};
use crate::event::{ActionContext, Event, EventProxy, EventType, Mouse, SearchState, Touch};
use crate::logging::LOG_TARGET_IPC_CONFIG;
use crate::message_bar::MessageBuffer;
use crate::scheduler::Scheduler;
Expand All @@ -62,6 +62,7 @@ pub struct WindowContext {
notifier: Notifier,
font_size: Size,
mouse: Mouse,
touch: Touch,
dirty: bool,
occluded: bool,
preserve_title: bool,
Expand Down Expand Up @@ -255,6 +256,7 @@ impl WindowContext {
ipc_config: Default::default(),
modifiers: Default::default(),
mouse: Default::default(),
touch: Default::default(),
dirty: Default::default(),
occluded: Default::default(),
})
Expand Down Expand Up @@ -441,6 +443,7 @@ impl WindowContext {
notifier: &mut self.notifier,
display: &mut self.display,
mouse: &mut self.mouse,
touch: &mut self.touch,
dirty: &mut self.dirty,
occluded: &mut self.occluded,
terminal: &mut terminal,
Expand Down

0 comments on commit 8add77f

Please sign in to comment.