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 = 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, 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, @@ -338,6 +343,11 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon self.mouse } + #[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), +} + +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 { + 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> { 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> { | 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 { 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> Processor { } } + /// 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,