summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristian Duerr <contact@christianduerr.com>2023-02-13 22:35:18 +0000
committerGitHub <noreply@github.com>2023-02-13 22:35:18 +0000
commit40be4caca1805ae3b77e65600e8f3daeae56d5c3 (patch)
treeab11017869ceac94cd03c30c35e097c341d2138c
parentc82de4ccadcaf2e74dc8e8ef2da1ebe13e2ca5a9 (diff)
Add touch input support
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.
-rw-r--r--CHANGELOG.md1
-rw-r--r--alacritty/src/event.rs91
-rw-r--r--alacritty/src/input.rs133
-rw-r--r--alacritty/src/window_context.rs5
4 files changed, 218 insertions, 12 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index abada71e..701d9631 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Support for horizontal scrolling in mouse mode and alternative scrolling modes
- Support for fractional scaling on Wayland with wp-fractional-scale protocol
- Support for running on GLES context
+- Touchscreen input for click/scroll/select/zoom
### Changed
diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs
index 768b7a47..5eafc09c 100644
--- a/alacritty/src/event.rs
+++ b/alacritty/src/event.rs
@@ -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;
@@ -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,
@@ -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 {
@@ -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 TouchPurpose,
pub received_count: &'a mut usize,
pub suppress_chars: &'a mut bool,
pub modifiers: &'a mut ModifiersState,
@@ -339,6 +344,11 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
}
#[inline]
+ fn touch_purpose(&mut self) -> &mut TouchPurpose {
+ self.touch
+ }
+
+ #[inline]
fn received_count(&mut self) -> &mut usize {
self.received_count
}
@@ -1016,12 +1026,68 @@ 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(Debug)]
+pub enum TouchPurpose {
None,
- Click,
- DoubleClick,
- TripleClick,
+ Select(TouchEvent),
+ Scroll(TouchEvent),
+ Zoom(TouchZoom),
+ Tap(TouchEvent),
+ Invalid(HashSet<u64>),
+}
+
+impl Default for TouchPurpose {
+ fn default() -> Self {
+ Self::None
+ }
+}
+
+/// 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.
@@ -1081,6 +1147,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 {
@@ -1217,6 +1291,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;
@@ -1290,7 +1365,6 @@ impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> {
| WindowEvent::Destroyed
| WindowEvent::ThemeChanged(_)
| WindowEvent::HoveredFile(_)
- | WindowEvent::Touch(_)
| WindowEvent::Moved(_) => (),
}
},
@@ -1592,7 +1666,6 @@ impl Processor {
| WindowEvent::HoveredFileCancelled
| WindowEvent::Destroyed
| WindowEvent::HoveredFile(_)
- | WindowEvent::Touch(_)
| WindowEvent::Moved(_)
),
WinitEvent::Suspended { .. }
diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs
index d454746a..f4c81006 100644
--- a/alacritty/src/input.rs
+++ b/alacritty/src/input.rs
@@ -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")]
@@ -35,7 +38,9 @@ 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, TouchPurpose, TouchZoom, TYPING_SEARCH_DELAY,
+};
use crate::message_bar::{self, Message};
use crate::scheduler::{Scheduler, TimerId, Topic};
@@ -51,6 +56,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
@@ -72,6 +83,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_purpose(&mut self) -> &mut TouchPurpose;
fn received_count(&mut self) -> &mut usize;
fn suppress_chars(&mut self) -> &mut bool;
fn modifiers(&mut self) -> &mut ModifiersState;
@@ -735,6 +747,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_purpose();
+ *touch_purpose = match mem::take(touch_purpose) {
+ TouchPurpose::None => TouchPurpose::Tap(touch),
+ TouchPurpose::Tap(start) => TouchPurpose::Zoom(TouchZoom::new((start, touch))),
+ TouchPurpose::Zoom(zoom) => TouchPurpose::Invalid(zoom.slots()),
+ TouchPurpose::Scroll(event) | TouchPurpose::Select(event) => {
+ let mut set = HashSet::new();
+ set.insert(event.id);
+ TouchPurpose::Invalid(set)
+ },
+ TouchPurpose::Invalid(mut slots) => {
+ slots.insert(touch.id);
+ TouchPurpose::Invalid(slots)
+ },
+ };
+ }
+
+ /// Handle touch input movement.
+ pub fn on_touch_motion(&mut self, touch: TouchEvent) {
+ let touch_purpose = self.ctx.touch_purpose();
+ match touch_purpose {
+ TouchPurpose::None => (),
+ // Handle transition from tap to scroll/select.
+ TouchPurpose::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 = TouchPurpose::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 = TouchPurpose::Scroll(*start);
+
+ // Apply motion since touch start.
+ self.on_touch_motion(touch);
+ }
+ },
+ TouchPurpose::Zoom(zoom) => {
+ let font_delta = zoom.font_delta(touch);
+ self.ctx.change_font_size(font_delta);
+ },
+ TouchPurpose::Scroll(last_touch) => {
+ // Calculate delta and update last touch position.
+ let delta_y = touch.location.y - last_touch.location.y;
+ *touch_purpose = TouchPurpose::Scroll(touch);
+
+ self.scroll_terminal(0., delta_y * TOUCH_SCROLL_FACTOR);
+ },
+ TouchPurpose::Select(_) => self.mouse_moved(touch.location),
+ TouchPurpose::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_purpose();
+ match touch_purpose {
+ // Simulate LMB clicks.
+ TouchPurpose::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.
+ TouchPurpose::Zoom(zoom) => {
+ let mut slots = zoom.slots();
+ slots.remove(&touch.id);
+ *touch_purpose = TouchPurpose::Invalid(slots);
+ },
+ // Reset touch state once all slots were released.
+ TouchPurpose::Invalid(slots) => {
+ slots.remove(&touch.id);
+ if slots.is_empty() {
+ *touch_purpose = Default::default();
+ }
+ },
+ // Release simulated LMB.
+ TouchPurpose::Select(_) => {
+ *touch_purpose = Default::default();
+ self.mouse_input(ElementState::Released, MouseButton::Left);
+ },
+ // Reset touch state on scroll finish.
+ TouchPurpose::Scroll(_) => *touch_purpose = Default::default(),
+ TouchPurpose::None => (),
+ }
+ }
+
pub fn mouse_input(&mut self, state: ElementState, button: MouseButton) {
match button {
MouseButton::Left => self.ctx.mouse_mut().left_button_state = state,
@@ -1105,6 +1229,11 @@ mod tests {
self.mouse
}
+ #[inline]
+ fn touch_purpose(&mut self) -> &mut TouchPurpose {
+ unimplemented!();
+ }
+
fn received_count(&mut self) -> &mut usize {
&mut self.received_count
}
diff --git a/alacritty/src/window_context.rs b/alacritty/src/window_context.rs
index 6680f583..7c2754e0 100644
--- a/alacritty/src/window_context.rs
+++ b/alacritty/src/window_context.rs
@@ -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, TouchPurpose};
use crate::logging::LOG_TARGET_IPC_CONFIG;
use crate::message_bar::MessageBuffer;
use crate::scheduler::Scheduler;
@@ -62,6 +62,7 @@ pub struct WindowContext {
notifier: Notifier,
font_size: Size,
mouse: Mouse,
+ touch: TouchPurpose,
dirty: bool,
occluded: bool,
preserve_title: bool,
@@ -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(),
})
@@ -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,