Projects STRLCPY alacritty Commits 40be4cac
🤬
  • 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.
  • Loading...
  • Christian Duerr committed with GitHub 1 year ago
    40be4cac
    1 parent c82de4cc
  • ■ ■ ■ ■ ■
    CHANGELOG.md
    skipped 12 lines
    13 13  - Support for horizontal scrolling in mouse mode and alternative scrolling modes
    14 14  - Support for fractional scaling on Wayland with wp-fractional-scale protocol
    15 15  - Support for running on GLES context
     16 +- Touchscreen input for click/scroll/select/zoom
    16 17   
    17 18  ### Changed
    18 19   
    skipped 1101 lines
  • ■ ■ ■ ■ ■
    alacritty/src/event.rs
    skipped 1 lines
    2 2   
    3 3  use std::borrow::Cow;
    4 4  use std::cmp::{max, min};
    5  -use std::collections::{HashMap, VecDeque};
     5 +use std::collections::{HashMap, HashSet, VecDeque};
    6 6  use std::error::Error;
    7 7  use std::ffi::OsStr;
    8 8  use std::fmt::Debug;
    skipped 10 lines
    19 19  use wayland_client::{Display as WaylandDisplay, EventQueue};
    20 20  use winit::dpi::PhysicalSize;
    21 21  use winit::event::{
    22  - ElementState, Event as WinitEvent, Ime, ModifiersState, MouseButton, StartCause, WindowEvent,
     22 + ElementState, Event as WinitEvent, Ime, ModifiersState, MouseButton, StartCause,
     23 + Touch as TouchEvent, WindowEvent,
    23 24  };
    24 25  use winit::event_loop::{
    25 26   ControlFlow, DeviceEventFilter, EventLoop, EventLoopProxy, EventLoopWindowTarget,
    skipped 40 lines
    66 67  /// Maximum number of search terms stored in the history.
    67 68  const MAX_SEARCH_HISTORY_SIZE: usize = 255;
    68 69   
     70 +/// Touch zoom speed.
     71 +const TOUCH_ZOOM_FACTOR: f32 = 0.01;
     72 + 
    69 73  /// Alacritty events.
    70 74  #[derive(Debug, Clone)]
    71 75  pub struct Event {
    skipped 114 lines
    186 190   pub terminal: &'a mut Term<T>,
    187 191   pub clipboard: &'a mut Clipboard,
    188 192   pub mouse: &'a mut Mouse,
     193 + pub touch: &'a mut TouchPurpose,
    189 194   pub received_count: &'a mut usize,
    190 195   pub suppress_chars: &'a mut bool,
    191 196   pub modifiers: &'a mut ModifiersState,
    skipped 144 lines
    336 341   #[inline]
    337 342   fn mouse(&self) -> &Mouse {
    338 343   self.mouse
     344 + }
     345 + 
     346 + #[inline]
     347 + fn touch_purpose(&mut self) -> &mut TouchPurpose {
     348 + self.touch
    339 349   }
    340 350   
    341 351   #[inline]
    skipped 674 lines
    1016 1026   }
    1017 1027  }
    1018 1028   
    1019  -#[derive(Debug, Eq, PartialEq)]
    1020  -pub enum ClickState {
     1029 +/// Identified purpose of the touch input.
     1030 +#[derive(Debug)]
     1031 +pub enum TouchPurpose {
    1021 1032   None,
    1022  - Click,
    1023  - DoubleClick,
    1024  - TripleClick,
     1033 + Select(TouchEvent),
     1034 + Scroll(TouchEvent),
     1035 + Zoom(TouchZoom),
     1036 + Tap(TouchEvent),
     1037 + Invalid(HashSet<u64>),
     1038 +}
     1039 + 
     1040 +impl Default for TouchPurpose {
     1041 + fn default() -> Self {
     1042 + Self::None
     1043 + }
     1044 +}
     1045 + 
     1046 +/// Touch zooming state.
     1047 +#[derive(Debug)]
     1048 +pub struct TouchZoom {
     1049 + slots: (TouchEvent, TouchEvent),
     1050 + fractions: f32,
     1051 +}
     1052 + 
     1053 +impl TouchZoom {
     1054 + pub fn new(slots: (TouchEvent, TouchEvent)) -> Self {
     1055 + Self { slots, fractions: Default::default() }
     1056 + }
     1057 + 
     1058 + /// Get slot distance change since last update.
     1059 + pub fn font_delta(&mut self, slot: TouchEvent) -> f32 {
     1060 + let old_distance = self.distance();
     1061 + 
     1062 + // Update touch slots.
     1063 + if slot.id == self.slots.0.id {
     1064 + self.slots.0 = slot;
     1065 + } else {
     1066 + self.slots.1 = slot;
     1067 + }
     1068 + 
     1069 + // Calculate font change in `FONT_SIZE_STEP` increments.
     1070 + let delta = (self.distance() - old_distance) * TOUCH_ZOOM_FACTOR + self.fractions;
     1071 + let font_delta = (delta.abs() / FONT_SIZE_STEP).floor() * FONT_SIZE_STEP * delta.signum();
     1072 + self.fractions = delta - font_delta;
     1073 + 
     1074 + font_delta
     1075 + }
     1076 + 
     1077 + /// Get active touch slots.
     1078 + pub fn slots(&self) -> HashSet<u64> {
     1079 + let mut set = HashSet::new();
     1080 + set.insert(self.slots.0.id);
     1081 + set.insert(self.slots.1.id);
     1082 + set
     1083 + }
     1084 + 
     1085 + /// Calculate distance between slots.
     1086 + fn distance(&self) -> f32 {
     1087 + let delta_x = self.slots.0.location.x - self.slots.1.location.x;
     1088 + let delta_y = self.slots.0.location.y - self.slots.1.location.y;
     1089 + delta_x.hypot(delta_y) as f32
     1090 + }
    1025 1091  }
    1026 1092   
    1027 1093  /// State of the mouse.
    skipped 53 lines
    1081 1147   }
    1082 1148  }
    1083 1149   
     1150 +#[derive(Debug, Eq, PartialEq)]
     1151 +pub enum ClickState {
     1152 + None,
     1153 + Click,
     1154 + DoubleClick,
     1155 + TripleClick,
     1156 +}
     1157 + 
    1084 1158  /// The amount of scroll accumulated from the pointer events.
    1085 1159  #[derive(Default, Debug)]
    1086 1160  pub struct AccumulatedScroll {
    skipped 130 lines
    1217 1291   self.ctx.window().set_mouse_visible(true);
    1218 1292   self.mouse_wheel_input(delta, phase);
    1219 1293   },
     1294 + WindowEvent::Touch(touch) => self.touch(touch),
    1220 1295   WindowEvent::Focused(is_focused) => {
    1221 1296   self.ctx.terminal.is_focused = is_focused;
    1222 1297   
    skipped 67 lines
    1290 1365   | WindowEvent::Destroyed
    1291 1366   | WindowEvent::ThemeChanged(_)
    1292 1367   | WindowEvent::HoveredFile(_)
    1293  - | WindowEvent::Touch(_)
    1294 1368   | WindowEvent::Moved(_) => (),
    1295 1369   }
    1296 1370   },
    skipped 295 lines
    1592 1666   | WindowEvent::HoveredFileCancelled
    1593 1667   | WindowEvent::Destroyed
    1594 1668   | WindowEvent::HoveredFile(_)
    1595  - | WindowEvent::Touch(_)
    1596 1669   | WindowEvent::Moved(_)
    1597 1670   ),
    1598 1671   WinitEvent::Suspended { .. }
    skipped 31 lines
  • ■ ■ ■ ■ ■ ■
    alacritty/src/input.rs
    skipped 6 lines
    7 7   
    8 8  use std::borrow::Cow;
    9 9  use std::cmp::{max, min, Ordering};
     10 +use std::collections::HashSet;
    10 11  use std::ffi::OsStr;
    11 12  use std::fmt::Debug;
    12 13  use std::marker::PhantomData;
     14 +use std::mem;
    13 15  use std::time::{Duration, Instant};
    14 16   
    15 17  use winit::dpi::PhysicalPosition;
    16 18  use winit::event::{
    17  - ElementState, KeyboardInput, ModifiersState, MouseButton, MouseScrollDelta, TouchPhase,
     19 + ElementState, KeyboardInput, ModifiersState, MouseButton, MouseScrollDelta,
     20 + Touch as TouchEvent, TouchPhase,
    18 21  };
    19 22  use winit::event_loop::EventLoopWindowTarget;
    20 23  #[cfg(target_os = "macos")]
    skipped 14 lines
    35 38  use crate::display::hint::HintMatch;
    36 39  use crate::display::window::Window;
    37 40  use crate::display::{Display, SizeInfo};
    38  -use crate::event::{ClickState, Event, EventType, Mouse, TYPING_SEARCH_DELAY};
     41 +use crate::event::{
     42 + ClickState, Event, EventType, Mouse, TouchPurpose, TouchZoom, TYPING_SEARCH_DELAY,
     43 +};
    39 44  use crate::message_bar::{self, Message};
    40 45  use crate::scheduler::{Scheduler, TimerId, Topic};
    41 46   
    skipped 8 lines
    50 55   
    51 56  /// Number of pixels for increasing the selection scrolling speed factor by one.
    52 57  const SELECTION_SCROLLING_STEP: f64 = 20.;
     58 + 
     59 +/// Touch scroll speed.
     60 +const TOUCH_SCROLL_FACTOR: f64 = 0.35;
     61 + 
     62 +/// Distance before a touch input is considered a drag.
     63 +const MAX_TAP_DISTANCE: f64 = 20.;
    53 64   
    54 65  /// Processes input from winit.
    55 66  ///
    skipped 16 lines
    72 83   fn selection_is_empty(&self) -> bool;
    73 84   fn mouse_mut(&mut self) -> &mut Mouse;
    74 85   fn mouse(&self) -> &Mouse;
     86 + fn touch_purpose(&mut self) -> &mut TouchPurpose;
    75 87   fn received_count(&mut self) -> &mut usize;
    76 88   fn suppress_chars(&mut self) -> &mut bool;
    77 89   fn modifiers(&mut self) -> &mut ModifiersState;
    skipped 657 lines
    735 747   }
    736 748   }
    737 749   
     750 + /// Handle touch input.
     751 + pub fn touch(&mut self, touch: TouchEvent) {
     752 + match touch.phase {
     753 + TouchPhase::Started => self.on_touch_start(touch),
     754 + TouchPhase::Moved => self.on_touch_motion(touch),
     755 + TouchPhase::Ended | TouchPhase::Cancelled => self.on_touch_end(touch),
     756 + }
     757 + }
     758 + 
     759 + /// Handle beginning of touch input.
     760 + pub fn on_touch_start(&mut self, touch: TouchEvent) {
     761 + let touch_purpose = self.ctx.touch_purpose();
     762 + *touch_purpose = match mem::take(touch_purpose) {
     763 + TouchPurpose::None => TouchPurpose::Tap(touch),
     764 + TouchPurpose::Tap(start) => TouchPurpose::Zoom(TouchZoom::new((start, touch))),
     765 + TouchPurpose::Zoom(zoom) => TouchPurpose::Invalid(zoom.slots()),
     766 + TouchPurpose::Scroll(event) | TouchPurpose::Select(event) => {
     767 + let mut set = HashSet::new();
     768 + set.insert(event.id);
     769 + TouchPurpose::Invalid(set)
     770 + },
     771 + TouchPurpose::Invalid(mut slots) => {
     772 + slots.insert(touch.id);
     773 + TouchPurpose::Invalid(slots)
     774 + },
     775 + };
     776 + }
     777 + 
     778 + /// Handle touch input movement.
     779 + pub fn on_touch_motion(&mut self, touch: TouchEvent) {
     780 + let touch_purpose = self.ctx.touch_purpose();
     781 + match touch_purpose {
     782 + TouchPurpose::None => (),
     783 + // Handle transition from tap to scroll/select.
     784 + TouchPurpose::Tap(start) => {
     785 + let delta_x = touch.location.x - start.location.x;
     786 + let delta_y = touch.location.y - start.location.y;
     787 + if delta_x.abs() > MAX_TAP_DISTANCE {
     788 + // Update gesture state.
     789 + let start_location = start.location;
     790 + *touch_purpose = TouchPurpose::Select(*start);
     791 + 
     792 + // Start simulated mouse input.
     793 + self.mouse_moved(start_location);
     794 + self.mouse_input(ElementState::Pressed, MouseButton::Left);
     795 + 
     796 + // Apply motion since touch start.
     797 + self.on_touch_motion(touch);
     798 + } else if delta_y.abs() > MAX_TAP_DISTANCE {
     799 + // Update gesture state.
     800 + *touch_purpose = TouchPurpose::Scroll(*start);
     801 + 
     802 + // Apply motion since touch start.
     803 + self.on_touch_motion(touch);
     804 + }
     805 + },
     806 + TouchPurpose::Zoom(zoom) => {
     807 + let font_delta = zoom.font_delta(touch);
     808 + self.ctx.change_font_size(font_delta);
     809 + },
     810 + TouchPurpose::Scroll(last_touch) => {
     811 + // Calculate delta and update last touch position.
     812 + let delta_y = touch.location.y - last_touch.location.y;
     813 + *touch_purpose = TouchPurpose::Scroll(touch);
     814 + 
     815 + self.scroll_terminal(0., delta_y * TOUCH_SCROLL_FACTOR);
     816 + },
     817 + TouchPurpose::Select(_) => self.mouse_moved(touch.location),
     818 + TouchPurpose::Invalid(_) => (),
     819 + }
     820 + }
     821 + 
     822 + /// Handle end of touch input.
     823 + pub fn on_touch_end(&mut self, touch: TouchEvent) {
     824 + // Finalize the touch motion up to the release point.
     825 + self.on_touch_motion(touch);
     826 + 
     827 + let touch_purpose = self.ctx.touch_purpose();
     828 + match touch_purpose {
     829 + // Simulate LMB clicks.
     830 + TouchPurpose::Tap(start) => {
     831 + let start_location = start.location;
     832 + *touch_purpose = Default::default();
     833 + 
     834 + self.mouse_moved(start_location);
     835 + self.mouse_input(ElementState::Pressed, MouseButton::Left);
     836 + self.mouse_input(ElementState::Released, MouseButton::Left);
     837 + },
     838 + // Invalidate zoom once a finger was released.
     839 + TouchPurpose::Zoom(zoom) => {
     840 + let mut slots = zoom.slots();
     841 + slots.remove(&touch.id);
     842 + *touch_purpose = TouchPurpose::Invalid(slots);
     843 + },
     844 + // Reset touch state once all slots were released.
     845 + TouchPurpose::Invalid(slots) => {
     846 + slots.remove(&touch.id);
     847 + if slots.is_empty() {
     848 + *touch_purpose = Default::default();
     849 + }
     850 + },
     851 + // Release simulated LMB.
     852 + TouchPurpose::Select(_) => {
     853 + *touch_purpose = Default::default();
     854 + self.mouse_input(ElementState::Released, MouseButton::Left);
     855 + },
     856 + // Reset touch state on scroll finish.
     857 + TouchPurpose::Scroll(_) => *touch_purpose = Default::default(),
     858 + TouchPurpose::None => (),
     859 + }
     860 + }
     861 + 
    738 862   pub fn mouse_input(&mut self, state: ElementState, button: MouseButton) {
    739 863   match button {
    740 864   MouseButton::Left => self.ctx.mouse_mut().left_button_state = state,
    skipped 362 lines
    1103 1227   #[inline]
    1104 1228   fn mouse(&self) -> &Mouse {
    1105 1229   self.mouse
     1230 + }
     1231 + 
     1232 + #[inline]
     1233 + fn touch_purpose(&mut self) -> &mut TouchPurpose {
     1234 + unimplemented!();
    1106 1235   }
    1107 1236   
    1108 1237   fn received_count(&mut self) -> &mut usize {
    skipped 288 lines
  • ■ ■ ■ ■
    alacritty/src/window_context.rs
    skipped 41 lines
    42 42  use crate::config::UiConfig;
    43 43  use crate::display::window::Window;
    44 44  use crate::display::Display;
    45  -use crate::event::{ActionContext, Event, EventProxy, EventType, Mouse, SearchState};
     45 +use crate::event::{ActionContext, Event, EventProxy, EventType, Mouse, SearchState, TouchPurpose};
    46 46  use crate::logging::LOG_TARGET_IPC_CONFIG;
    47 47  use crate::message_bar::MessageBuffer;
    48 48  use crate::scheduler::Scheduler;
    skipped 13 lines
    62 62   notifier: Notifier,
    63 63   font_size: Size,
    64 64   mouse: Mouse,
     65 + touch: TouchPurpose,
    65 66   dirty: bool,
    66 67   occluded: bool,
    67 68   preserve_title: bool,
    skipped 187 lines
    255 256   ipc_config: Default::default(),
    256 257   modifiers: Default::default(),
    257 258   mouse: Default::default(),
     259 + touch: Default::default(),
    258 260   dirty: Default::default(),
    259 261   occluded: Default::default(),
    260 262   })
    skipped 180 lines
    441 443   notifier: &mut self.notifier,
    442 444   display: &mut self.display,
    443 445   mouse: &mut self.mouse,
     446 + touch: &mut self.touch,
    444 447   dirty: &mut self.dirty,
    445 448   occluded: &mut self.occluded,
    446 449   terminal: &mut terminal,
    skipped 148 lines
Please wait...
Page is in error, reload to recover