diff options
Diffstat (limited to 'alacritty/src/input/mod.rs')
-rw-r--r-- | alacritty/src/input/mod.rs | 1503 |
1 files changed, 1503 insertions, 0 deletions
diff --git a/alacritty/src/input/mod.rs b/alacritty/src/input/mod.rs new file mode 100644 index 00000000..584b8240 --- /dev/null +++ b/alacritty/src/input/mod.rs @@ -0,0 +1,1503 @@ +//! Handle input from winit. +//! +//! Certain key combinations should send some escape sequence back to the PTY. +//! In order to figure that out, state about which modifier keys are pressed +//! needs to be tracked. Additionally, we need a bit of a state machine to +//! determine what to do when a non-modifier key is pressed. + +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 log::debug; +use winit::dpi::PhysicalPosition; +use winit::event::{ + ElementState, Modifiers, MouseButton, MouseScrollDelta, Touch as TouchEvent, TouchPhase, +}; +use winit::event_loop::EventLoopWindowTarget; +use winit::keyboard::ModifiersState; +#[cfg(target_os = "macos")] +use winit::platform::macos::EventLoopWindowTargetExtMacOS; +use winit::window::CursorIcon; + +use alacritty_terminal::event::EventListener; +use alacritty_terminal::grid::{Dimensions, Scroll}; +use alacritty_terminal::index::{Boundary, Column, Direction, Point, Side}; +use alacritty_terminal::selection::SelectionType; +use alacritty_terminal::term::search::Match; +use alacritty_terminal::term::{ClipboardType, Term, TermMode}; +use alacritty_terminal::vi_mode::ViMotion; +use alacritty_terminal::vte::ansi::{ClearMode, Handler}; + +use crate::clipboard::Clipboard; +use crate::config::{Action, BindingMode, MouseAction, SearchAction, UiConfig, ViAction}; +use crate::display::hint::HintMatch; +use crate::display::window::Window; +use crate::display::{Display, SizeInfo}; +use crate::event::{ + ClickState, Event, EventType, InlineSearchState, Mouse, TouchPurpose, TouchZoom, +}; +use crate::message_bar::{self, Message}; +use crate::scheduler::{Scheduler, TimerId, Topic}; + +pub mod keyboard; + +/// Font size change interval. +pub const FONT_SIZE_STEP: f32 = 0.5; + +/// Interval for mouse scrolling during selection outside of the boundaries. +const SELECTION_SCROLLING_INTERVAL: Duration = Duration::from_millis(15); + +/// Minimum number of pixels at the bottom/top where selection scrolling is performed. +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.; + +/// Distance before a touch input is considered a drag. +const MAX_TAP_DISTANCE: f64 = 20.; + +/// Threshold used for double_click/triple_click. +const CLICK_THRESHOLD: Duration = Duration::from_millis(400); + +/// Processes input from winit. +/// +/// An escape sequence may be emitted in case specific keys or key combinations +/// are activated. +pub struct Processor<T: EventListener, A: ActionContext<T>> { + pub ctx: A, + _phantom: PhantomData<T>, +} + +pub trait ActionContext<T: EventListener> { + fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&self, _data: B) {} + fn mark_dirty(&mut self) {} + fn size_info(&self) -> SizeInfo; + fn copy_selection(&mut self, _ty: ClipboardType) {} + fn start_selection(&mut self, _ty: SelectionType, _point: Point, _side: Side) {} + fn toggle_selection(&mut self, _ty: SelectionType, _point: Point, _side: Side) {} + fn update_selection(&mut self, _point: Point, _side: Side) {} + fn clear_selection(&mut self) {} + 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 modifiers(&mut self) -> &mut Modifiers; + fn scroll(&mut self, _scroll: Scroll) {} + fn window(&mut self) -> &mut Window; + fn display(&mut self) -> &mut Display; + fn terminal(&self) -> &Term<T>; + fn terminal_mut(&mut self) -> &mut Term<T>; + fn spawn_new_instance(&mut self) {} + #[cfg(target_os = "macos")] + fn create_new_window(&mut self, _tabbing_id: Option<String>) {} + #[cfg(not(target_os = "macos"))] + fn create_new_window(&mut self) {} + fn change_font_size(&mut self, _delta: f32) {} + fn reset_font_size(&mut self) {} + fn pop_message(&mut self) {} + fn message(&self) -> Option<&Message>; + fn config(&self) -> &UiConfig; + fn event_loop(&self) -> &EventLoopWindowTarget<Event>; + fn mouse_mode(&self) -> bool; + fn clipboard_mut(&mut self) -> &mut Clipboard; + fn scheduler_mut(&mut self) -> &mut Scheduler; + fn start_search(&mut self, _direction: Direction) {} + fn confirm_search(&mut self) {} + fn cancel_search(&mut self) {} + fn search_input(&mut self, _c: char) {} + fn search_pop_word(&mut self) {} + fn search_history_previous(&mut self) {} + fn search_history_next(&mut self) {} + fn search_next(&mut self, origin: Point, direction: Direction, side: Side) -> Option<Match>; + fn advance_search_origin(&mut self, _direction: Direction) {} + fn search_direction(&self) -> Direction; + fn search_active(&self) -> bool; + fn on_typing_start(&mut self) {} + fn toggle_vi_mode(&mut self) {} + fn inline_search_state(&mut self) -> &mut InlineSearchState; + fn start_inline_search(&mut self, _direction: Direction, _stop_short: bool) {} + fn inline_search_next(&mut self) {} + fn inline_search_previous(&mut self) {} + fn hint_input(&mut self, _character: char) {} + fn trigger_hint(&mut self, _hint: &HintMatch) {} + fn expand_selection(&mut self) {} + fn on_terminal_input_start(&mut self) {} + fn paste(&mut self, _text: &str, _bracketed: bool) {} + fn spawn_daemon<I, S>(&self, _program: &str, _args: I) + where + I: IntoIterator<Item = S> + Debug + Copy, + S: AsRef<OsStr>, + { + } +} + +impl Action { + fn toggle_selection<T, A>(ctx: &mut A, ty: SelectionType) + where + A: ActionContext<T>, + T: EventListener, + { + ctx.toggle_selection(ty, ctx.terminal().vi_mode_cursor.point, Side::Left); + + // Make sure initial selection is not empty. + if let Some(selection) = &mut ctx.terminal_mut().selection { + selection.include_all(); + } + } +} + +trait Execute<T: EventListener> { + fn execute<A: ActionContext<T>>(&self, ctx: &mut A); +} + +impl<T: EventListener> Execute<T> for Action { + #[inline] + fn execute<A: ActionContext<T>>(&self, ctx: &mut A) { + match self { + Action::Esc(s) => ctx.paste(s, false), + Action::Command(program) => ctx.spawn_daemon(program.program(), program.args()), + Action::Hint(hint) => { + ctx.display().hint_state.start(hint.clone()); + ctx.mark_dirty(); + }, + Action::ToggleViMode => { + ctx.on_typing_start(); + ctx.toggle_vi_mode() + }, + action @ (Action::ViMotion(_) | Action::Vi(_)) + if !ctx.terminal().mode().contains(TermMode::VI) => + { + debug!("Ignoring {action:?}: Vi mode inactive"); + }, + Action::ViMotion(motion) => { + ctx.on_typing_start(); + ctx.terminal_mut().vi_motion(*motion); + ctx.mark_dirty(); + }, + Action::Vi(ViAction::ToggleNormalSelection) => { + Self::toggle_selection(ctx, SelectionType::Simple); + }, + Action::Vi(ViAction::ToggleLineSelection) => { + Self::toggle_selection(ctx, SelectionType::Lines); + }, + Action::Vi(ViAction::ToggleBlockSelection) => { + Self::toggle_selection(ctx, SelectionType::Block); + }, + Action::Vi(ViAction::ToggleSemanticSelection) => { + Self::toggle_selection(ctx, SelectionType::Semantic); + }, + Action::Vi(ViAction::Open) => { + let hint = ctx.display().vi_highlighted_hint.take(); + if let Some(hint) = &hint { + ctx.mouse_mut().block_hint_launcher = false; + ctx.trigger_hint(hint); + } + ctx.display().vi_highlighted_hint = hint; + }, + Action::Vi(ViAction::SearchNext) => { + ctx.on_typing_start(); + + let terminal = ctx.terminal(); + let direction = ctx.search_direction(); + let vi_point = terminal.vi_mode_cursor.point; + let origin = match direction { + Direction::Right => vi_point.add(terminal, Boundary::None, 1), + Direction::Left => vi_point.sub(terminal, Boundary::None, 1), + }; + + if let Some(regex_match) = ctx.search_next(origin, direction, Side::Left) { + ctx.terminal_mut().vi_goto_point(*regex_match.start()); + ctx.mark_dirty(); + } + }, + Action::Vi(ViAction::SearchPrevious) => { + ctx.on_typing_start(); + + let terminal = ctx.terminal(); + let direction = ctx.search_direction().opposite(); + let vi_point = terminal.vi_mode_cursor.point; + let origin = match direction { + Direction::Right => vi_point.add(terminal, Boundary::None, 1), + Direction::Left => vi_point.sub(terminal, Boundary::None, 1), + }; + + if let Some(regex_match) = ctx.search_next(origin, direction, Side::Left) { + ctx.terminal_mut().vi_goto_point(*regex_match.start()); + ctx.mark_dirty(); + } + }, + Action::Vi(ViAction::SearchStart) => { + let terminal = ctx.terminal(); + let origin = terminal.vi_mode_cursor.point.sub(terminal, Boundary::None, 1); + + if let Some(regex_match) = ctx.search_next(origin, Direction::Left, Side::Left) { + ctx.terminal_mut().vi_goto_point(*regex_match.start()); + ctx.mark_dirty(); + } + }, + Action::Vi(ViAction::SearchEnd) => { + let terminal = ctx.terminal(); + let origin = terminal.vi_mode_cursor.point.add(terminal, Boundary::None, 1); + + if let Some(regex_match) = ctx.search_next(origin, Direction::Right, Side::Right) { + ctx.terminal_mut().vi_goto_point(*regex_match.end()); + ctx.mark_dirty(); + } + }, + Action::Vi(ViAction::CenterAroundViCursor) => { + let term = ctx.terminal(); + let display_offset = term.grid().display_offset() as i32; + let target = -display_offset + term.screen_lines() as i32 / 2 - 1; + let line = term.vi_mode_cursor.point.line; + let scroll_lines = target - line.0; + + ctx.scroll(Scroll::Delta(scroll_lines)); + }, + Action::Vi(ViAction::InlineSearchForward) => { + ctx.start_inline_search(Direction::Right, false) + }, + Action::Vi(ViAction::InlineSearchBackward) => { + ctx.start_inline_search(Direction::Left, false) + }, + Action::Vi(ViAction::InlineSearchForwardShort) => { + ctx.start_inline_search(Direction::Right, true) + }, + Action::Vi(ViAction::InlineSearchBackwardShort) => { + ctx.start_inline_search(Direction::Left, true) + }, + Action::Vi(ViAction::InlineSearchNext) => ctx.inline_search_next(), + Action::Vi(ViAction::InlineSearchPrevious) => ctx.inline_search_previous(), + action @ Action::Search(_) if !ctx.search_active() => { + debug!("Ignoring {action:?}: Search mode inactive"); + }, + Action::Search(SearchAction::SearchFocusNext) => { + ctx.advance_search_origin(ctx.search_direction()); + }, + Action::Search(SearchAction::SearchFocusPrevious) => { + let direction = ctx.search_direction().opposite(); + ctx.advance_search_origin(direction); + }, + Action::Search(SearchAction::SearchConfirm) => ctx.confirm_search(), + Action::Search(SearchAction::SearchCancel) => ctx.cancel_search(), + Action::Search(SearchAction::SearchClear) => { + let direction = ctx.search_direction(); + ctx.cancel_search(); + ctx.start_search(direction); + }, + Action::Search(SearchAction::SearchDeleteWord) => ctx.search_pop_word(), + Action::Search(SearchAction::SearchHistoryPrevious) => ctx.search_history_previous(), + Action::Search(SearchAction::SearchHistoryNext) => ctx.search_history_next(), + Action::Mouse(MouseAction::ExpandSelection) => ctx.expand_selection(), + Action::SearchForward => ctx.start_search(Direction::Right), + Action::SearchBackward => ctx.start_search(Direction::Left), + Action::Copy => ctx.copy_selection(ClipboardType::Clipboard), + #[cfg(not(any(target_os = "macos", windows)))] + Action::CopySelection => ctx.copy_selection(ClipboardType::Selection), + Action::ClearSelection => ctx.clear_selection(), + Action::Paste => { + let text = ctx.clipboard_mut().load(ClipboardType::Clipboard); + ctx.paste(&text, true); + }, + Action::PasteSelection => { + let text = ctx.clipboard_mut().load(ClipboardType::Selection); + ctx.paste(&text, true); + }, + Action::ToggleFullscreen => ctx.window().toggle_fullscreen(), + Action::ToggleMaximized => ctx.window().toggle_maximized(), + #[cfg(target_os = "macos")] + Action::ToggleSimpleFullscreen => ctx.window().toggle_simple_fullscreen(), + #[cfg(target_os = "macos")] + Action::Hide => ctx.event_loop().hide_application(), + #[cfg(target_os = "macos")] + Action::HideOtherApplications => ctx.event_loop().hide_other_applications(), + #[cfg(not(target_os = "macos"))] + Action::Hide => ctx.window().set_visible(false), + Action::Minimize => ctx.window().set_minimized(true), + Action::Quit => ctx.terminal_mut().exit(), + Action::IncreaseFontSize => ctx.change_font_size(FONT_SIZE_STEP), + Action::DecreaseFontSize => ctx.change_font_size(FONT_SIZE_STEP * -1.), + Action::ResetFontSize => ctx.reset_font_size(), + Action::ScrollPageUp + | Action::ScrollPageDown + | Action::ScrollHalfPageUp + | Action::ScrollHalfPageDown => { + // Move vi mode cursor. + let term = ctx.terminal_mut(); + let (scroll, amount) = match self { + Action::ScrollPageUp => (Scroll::PageUp, term.screen_lines() as i32), + Action::ScrollPageDown => (Scroll::PageDown, -(term.screen_lines() as i32)), + Action::ScrollHalfPageUp => { + let amount = term.screen_lines() as i32 / 2; + (Scroll::Delta(amount), amount) + }, + Action::ScrollHalfPageDown => { + let amount = -(term.screen_lines() as i32 / 2); + (Scroll::Delta(amount), amount) + }, + _ => unreachable!(), + }; + + let old_vi_cursor = term.vi_mode_cursor; + term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, amount); + if old_vi_cursor != term.vi_mode_cursor { + ctx.mark_dirty(); + } + + ctx.scroll(scroll); + }, + Action::ScrollLineUp => ctx.scroll(Scroll::Delta(1)), + Action::ScrollLineDown => ctx.scroll(Scroll::Delta(-1)), + Action::ScrollToTop => { + ctx.scroll(Scroll::Top); + + // Move vi mode cursor. + let topmost_line = ctx.terminal().topmost_line(); + ctx.terminal_mut().vi_mode_cursor.point.line = topmost_line; + ctx.terminal_mut().vi_motion(ViMotion::FirstOccupied); + ctx.mark_dirty(); + }, + Action::ScrollToBottom => { + ctx.scroll(Scroll::Bottom); + + // Move vi mode cursor. + let term = ctx.terminal_mut(); + term.vi_mode_cursor.point.line = term.bottommost_line(); + + // Move to beginning twice, to always jump across linewraps. + term.vi_motion(ViMotion::FirstOccupied); + term.vi_motion(ViMotion::FirstOccupied); + ctx.mark_dirty(); + }, + Action::ClearHistory => ctx.terminal_mut().clear_screen(ClearMode::Saved), + Action::ClearLogNotice => ctx.pop_message(), + #[cfg(not(target_os = "macos"))] + Action::CreateNewWindow => ctx.create_new_window(), + Action::SpawnNewInstance => ctx.spawn_new_instance(), + #[cfg(target_os = "macos")] + Action::CreateNewWindow => ctx.create_new_window(None), + #[cfg(target_os = "macos")] + Action::CreateNewTab => { + let tabbing_id = Some(ctx.window().tabbing_id()); + ctx.create_new_window(tabbing_id); + }, + #[cfg(target_os = "macos")] + Action::SelectNextTab => ctx.window().select_next_tab(), + #[cfg(target_os = "macos")] + Action::SelectPreviousTab => ctx.window().select_previous_tab(), + #[cfg(target_os = "macos")] + Action::SelectTab1 => ctx.window().select_tab_at_index(0), + #[cfg(target_os = "macos")] + Action::SelectTab2 => ctx.window().select_tab_at_index(1), + #[cfg(target_os = "macos")] + Action::SelectTab3 => ctx.window().select_tab_at_index(2), + #[cfg(target_os = "macos")] + Action::SelectTab4 => ctx.window().select_tab_at_index(3), + #[cfg(target_os = "macos")] + Action::SelectTab5 => ctx.window().select_tab_at_index(4), + #[cfg(target_os = "macos")] + Action::SelectTab6 => ctx.window().select_tab_at_index(5), + #[cfg(target_os = "macos")] + Action::SelectTab7 => ctx.window().select_tab_at_index(6), + #[cfg(target_os = "macos")] + Action::SelectTab8 => ctx.window().select_tab_at_index(7), + #[cfg(target_os = "macos")] + Action::SelectTab9 => ctx.window().select_tab_at_index(8), + #[cfg(target_os = "macos")] + Action::SelectLastTab => ctx.window().select_last_tab(), + Action::ReceiveChar | Action::None => (), + } + } +} + +impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { + pub fn new(ctx: A) -> Self { + Self { ctx, _phantom: Default::default() } + } + + #[inline] + pub fn mouse_moved(&mut self, position: PhysicalPosition<f64>) { + let size_info = self.ctx.size_info(); + + let (x, y) = position.into(); + + let lmb_pressed = self.ctx.mouse().left_button_state == ElementState::Pressed; + let rmb_pressed = self.ctx.mouse().right_button_state == ElementState::Pressed; + if !self.ctx.selection_is_empty() && (lmb_pressed || rmb_pressed) { + self.update_selection_scrolling(y); + } + + let display_offset = self.ctx.terminal().grid().display_offset(); + let old_point = self.ctx.mouse().point(&size_info, display_offset); + + let x = x.clamp(0, size_info.width() as i32 - 1) as usize; + let y = y.clamp(0, size_info.height() as i32 - 1) as usize; + self.ctx.mouse_mut().x = x; + self.ctx.mouse_mut().y = y; + + let inside_text_area = size_info.contains_point(x, y); + let cell_side = self.cell_side(x); + + let point = self.ctx.mouse().point(&size_info, display_offset); + let cell_changed = old_point != point; + + // If the mouse hasn't changed cells, do nothing. + if !cell_changed + && self.ctx.mouse().cell_side == cell_side + && self.ctx.mouse().inside_text_area == inside_text_area + { + return; + } + + self.ctx.mouse_mut().inside_text_area = inside_text_area; + self.ctx.mouse_mut().cell_side = cell_side; + + // Update mouse state and check for URL change. + let mouse_state = self.cursor_state(); + self.ctx.window().set_mouse_cursor(mouse_state); + + // Prompt hint highlight update. + self.ctx.mouse_mut().hint_highlight_dirty = true; + + // Don't launch URLs if mouse has moved. + self.ctx.mouse_mut().block_hint_launcher = true; + + if (lmb_pressed || rmb_pressed) + && (self.ctx.modifiers().state().shift_key() || !self.ctx.mouse_mode()) + { + self.ctx.update_selection(point, cell_side); + } else if cell_changed + && self.ctx.terminal().mode().intersects(TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG) + { + if lmb_pressed { + self.mouse_report(32, ElementState::Pressed); + } else if self.ctx.mouse().middle_button_state == ElementState::Pressed { + self.mouse_report(33, ElementState::Pressed); + } else if self.ctx.mouse().right_button_state == ElementState::Pressed { + self.mouse_report(34, ElementState::Pressed); + } else if self.ctx.terminal().mode().contains(TermMode::MOUSE_MOTION) { + self.mouse_report(35, ElementState::Pressed); + } + } + } + + /// Check which side of a cell an X coordinate lies on. + fn cell_side(&self, x: usize) -> Side { + let size_info = self.ctx.size_info(); + + let cell_x = + x.saturating_sub(size_info.padding_x() as usize) % size_info.cell_width() as usize; + let half_cell_width = (size_info.cell_width() / 2.0) as usize; + + let additional_padding = + (size_info.width() - size_info.padding_x() * 2.) % size_info.cell_width(); + let end_of_grid = size_info.width() - size_info.padding_x() - additional_padding; + + if cell_x > half_cell_width + // Edge case when mouse leaves the window. + || x as f32 >= end_of_grid + { + Side::Right + } else { + Side::Left + } + } + + fn mouse_report(&mut self, button: u8, state: ElementState) { + let display_offset = self.ctx.terminal().grid().display_offset(); + let point = self.ctx.mouse().point(&self.ctx.size_info(), display_offset); + + // Assure the mouse point is not in the scrollback. + if point.line < 0 { + return; + } + + // Calculate modifiers value. + let mut mods = 0; + let modifiers = self.ctx.modifiers().state(); + if modifiers.shift_key() { + mods += 4; + } + if modifiers.alt_key() { + mods += 8; + } + if modifiers.control_key() { + mods += 16; + } + + // Report mouse events. + if self.ctx.terminal().mode().contains(TermMode::SGR_MOUSE) { + self.sgr_mouse_report(point, button + mods, state); + } else if let ElementState::Released = state { + self.normal_mouse_report(point, 3 + mods); + } else { + self.normal_mouse_report(point, button + mods); + } + } + + fn normal_mouse_report(&mut self, point: Point, button: u8) { + let Point { line, column } = point; + let utf8 = self.ctx.terminal().mode().contains(TermMode::UTF8_MOUSE); + + let max_point = if utf8 { 2015 } else { 223 }; + + if line >= max_point || column >= max_point { + return; + } + + let mut msg = vec![b'\x1b', b'[', b'M', 32 + button]; + + let mouse_pos_encode = |pos: usize| -> Vec<u8> { + let pos = 32 + 1 + pos; + let first = 0xC0 + pos / 64; + let second = 0x80 + (pos & 63); + vec![first as u8, second as u8] + }; + + if utf8 && column >= Column(95) { + msg.append(&mut mouse_pos_encode(column.0)); + } else { + msg.push(32 + 1 + column.0 as u8); + } + + if utf8 && line >= 95 { + msg.append(&mut mouse_pos_encode(line.0 as usize)); + } else { + msg.push(32 + 1 + line.0 as u8); + } + + self.ctx.write_to_pty(msg); + } + + fn sgr_mouse_report(&mut self, point: Point, button: u8, state: ElementState) { + let c = match state { + ElementState::Pressed => 'M', + ElementState::Released => 'm', + }; + + let msg = format!("\x1b[<{};{};{}{}", button, point.column + 1, point.line + 1, c); + self.ctx.write_to_pty(msg.into_bytes()); + } + + fn on_mouse_press(&mut self, button: MouseButton) { + // Handle mouse mode. + if !self.ctx.modifiers().state().shift_key() && self.ctx.mouse_mode() { + self.ctx.mouse_mut().click_state = ClickState::None; + + let code = match button { + MouseButton::Left => 0, + MouseButton::Middle => 1, + MouseButton::Right => 2, + // Can't properly report more than three buttons.. + MouseButton::Back | MouseButton::Forward | MouseButton::Other(_) => return, + }; + + self.mouse_report(code, ElementState::Pressed); + } else { + // Calculate time since the last click to handle double/triple clicks. + let now = Instant::now(); + let elapsed = now - self.ctx.mouse().last_click_timestamp; + self.ctx.mouse_mut().last_click_timestamp = now; + + // Update multi-click state. + self.ctx.mouse_mut().click_state = match self.ctx.mouse().click_state { + // Reset click state if button has changed. + _ if button != self.ctx.mouse().last_click_button => { + self.ctx.mouse_mut().last_click_button = button; + ClickState::Click + }, + ClickState::Click if elapsed < CLICK_THRESHOLD => ClickState::DoubleClick, + ClickState::DoubleClick if elapsed < CLICK_THRESHOLD => ClickState::TripleClick, + _ => ClickState::Click, + }; + + // Load mouse point, treating message bar and padding as the closest cell. + let display_offset = self.ctx.terminal().grid().display_offset(); + let point = self.ctx.mouse().point(&self.ctx.size_info(), display_offset); + + if let MouseButton::Left = button { + self.on_left_click(point) + } + } + } + + /// Handle left click selection and vi mode cursor movement. + fn on_left_click(&mut self, point: Point) { + let side = self.ctx.mouse().cell_side; + + match self.ctx.mouse().click_state { + ClickState::Click => { + // Don't launch URLs if this click cleared the selection. + self.ctx.mouse_mut().block_hint_launcher = !self.ctx.selection_is_empty(); + + self.ctx.clear_selection(); + + // Start new empty selection. + if self.ctx.modifiers().state().control_key() { + self.ctx.start_selection(SelectionType::Block, point, side); + } else { + self.ctx.start_selection(SelectionType::Simple, point, side); + } + }, + ClickState::DoubleClick => { + self.ctx.mouse_mut().block_hint_launcher = true; + self.ctx.start_selection(SelectionType::Semantic, point, side); + }, + ClickState::TripleClick => { + self.ctx.mouse_mut().block_hint_launcher = true; + self.ctx.start_selection(SelectionType::Lines, point, side); + }, + ClickState::None => (), + }; + + // Move vi mode cursor to mouse click position. + if self.ctx.terminal().mode().contains(TermMode::VI) && !self.ctx.search_active() { + self.ctx.terminal_mut().vi_mode_cursor.point = point; + self.ctx.mark_dirty(); + } + } + + fn on_mouse_release(&mut self, button: MouseButton) { + if !self.ctx.modifiers().state().shift_key() && self.ctx.mouse_mode() { + let code = match button { + MouseButton::Left => 0, + MouseButton::Middle => 1, + MouseButton::Right => 2, + // Can't properly report more than three buttons. + MouseButton::Back | MouseButton::Forward | MouseButton::Other(_) => return, + }; + self.mouse_report(code, ElementState::Released); + return; + } + + // Trigger hints highlighted by the mouse. + let hint = self.ctx.display().highlighted_hint.take(); + if let Some(hint) = hint.as_ref().filter(|_| button == MouseButton::Left) { + self.ctx.trigger_hint(hint); + } + self.ctx.display().highlighted_hint = hint; + + let timer_id = TimerId::new(Topic::SelectionScrolling, self.ctx.window().id()); + self.ctx.scheduler_mut().unschedule(timer_id); + + if let MouseButton::Left | MouseButton::Right = button { + // Copy selection on release, to prevent flooding the display server. + self.ctx.copy_selection(ClipboardType::Selection); + } + } + + pub fn mouse_wheel_input(&mut self, delta: MouseScrollDelta, phase: TouchPhase) { + let multiplier = self.ctx.config().scrolling.multiplier; + match delta { + MouseScrollDelta::LineDelta(columns, lines) => { + let new_scroll_px_x = columns * self.ctx.size_info().cell_width(); + let new_scroll_px_y = lines * self.ctx.size_info().cell_height(); + self.scroll_terminal( + new_scroll_px_x as f64, + new_scroll_px_y as f64, + multiplier as f64, + ); + }, + MouseScrollDelta::PixelDelta(mut lpos) => { + match phase { + TouchPhase::Started => { + // Reset offset to zero. + self.ctx.mouse_mut().accumulated_scroll = Default::default(); + }, + TouchPhase::Moved => { + // When the angle between (x, 0) and (x, y) is lower than ~25 degrees + // (cosine is larger that 0.9) we consider this scrolling as horizontal. + if lpos.x.abs() / lpos.x.hypot(lpos.y) > 0.9 { + lpos.y = 0.; + } else { + lpos.x = 0.; + } + + self.scroll_terminal(lpos.x, lpos.y, multiplier as f64); + }, + _ => (), + } + }, + } + } + + fn scroll_terminal(&mut self, new_scroll_x_px: f64, new_scroll_y_px: f64, multiplier: f64) { + const MOUSE_WHEEL_UP: u8 = 64; + const MOUSE_WHEEL_DOWN: u8 = 65; + const MOUSE_WHEEL_LEFT: u8 = 66; + const MOUSE_WHEEL_RIGHT: u8 = 67; + + let width = f64::from(self.ctx.size_info().cell_width()); + let height = f64::from(self.ctx.size_info().cell_height()); + + if self.ctx.mouse_mode() { + self.ctx.mouse_mut().accumulated_scroll.x += new_scroll_x_px; + self.ctx.mouse_mut().accumulated_scroll.y += new_scroll_y_px; + + let code = if new_scroll_y_px > 0. { MOUSE_WHEEL_UP } else { MOUSE_WHEEL_DOWN }; + let lines = (self.ctx.mouse().accumulated_scroll.y / height).abs() as i32; + + for _ in 0..lines { + self.mouse_report(code, ElementState::Pressed); + } + + let code = if new_scroll_x_px > 0. { MOUSE_WHEEL_LEFT } else { MOUSE_WHEEL_RIGHT }; + let columns = (self.ctx.mouse().accumulated_scroll.x / width).abs() as i32; + + for _ in 0..columns { + self.mouse_report(code, ElementState::Pressed); + } + } else if self + .ctx + .terminal() + .mode() + .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL) + && !self.ctx.modifiers().state().shift_key() + { + self.ctx.mouse_mut().accumulated_scroll.x += new_scroll_x_px * multiplier; + self.ctx.mouse_mut().accumulated_scroll.y += new_scroll_y_px * multiplier; + + // The chars here are the same as for the respective arrow keys. + let line_cmd = if new_scroll_y_px > 0. { b'A' } else { b'B' }; + let column_cmd = if new_scroll_x_px > 0. { b'D' } else { b'C' }; + + let lines = (self.ctx.mouse().accumulated_scroll.y / height).abs() as usize; + let columns = (self.ctx.mouse().accumulated_scroll.x / width).abs() as usize; + + let mut content = Vec::with_capacity(3 * (lines + columns)); + + for _ in 0..lines { + content.push(0x1b); + content.push(b'O'); + content.push(line_cmd); + } + + for _ in 0..columns { + content.push(0x1b); + content.push(b'O'); + content.push(column_cmd); + } + + self.ctx.write_to_pty(content); + } else { + self.ctx.mouse_mut().accumulated_scroll.y += new_scroll_y_px * multiplier; + + let lines = (self.ctx.mouse().accumulated_scroll.y / height) as i32; + + if lines != 0 { + self.ctx.scroll(Scroll::Delta(lines)); + } + } + + self.ctx.mouse_mut().accumulated_scroll.x %= width; + self.ctx.mouse_mut().accumulated_scroll.y %= height; + } + + pub fn on_focus_change(&mut self, is_focused: bool) { + if self.ctx.terminal().mode().contains(TermMode::FOCUS_IN_OUT) { + let chr = if is_focused { "I" } else { "O" }; + + let msg = format!("\x1b[{}", chr); + self.ctx.write_to_pty(msg.into_bytes()); + } + } + + /// 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::default(); + 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); + + // Use a fixed scroll factor for touchscreens, to accurately track finger motion. + self.scroll_terminal(0., delta_y, 1.0); + }, + 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 => (), + } + } + + /// Reset mouse cursor based on modifier and terminal state. + #[inline] + pub fn reset_mouse_cursor(&mut self) { + let mouse_state = self.cursor_state(); + self.ctx.window().set_mouse_cursor(mouse_state); + } + + /// Modifier state change. + pub fn modifiers_input(&mut self, modifiers: Modifiers) { + *self.ctx.modifiers() = modifiers; + + // Prompt hint highlight update. + self.ctx.mouse_mut().hint_highlight_dirty = true; + + // Update mouse state and check for URL change. + let mouse_state = self.cursor_state(); + self.ctx.window().set_mouse_cursor(mouse_state); + } + + pub fn mouse_input(&mut self, state: ElementState, button: MouseButton) { + match button { + MouseButton::Left => self.ctx.mouse_mut().left_button_state = state, + MouseButton::Middle => self.ctx.mouse_mut().middle_button_state = state, + MouseButton::Right => self.ctx.mouse_mut().right_button_state = state, + _ => (), + } + + // Skip normal mouse events if the message bar has been clicked. + if self.message_bar_cursor_state() == Some(CursorIcon::Pointer) + && state == ElementState::Pressed + { + let size = self.ctx.size_info(); + + let current_lines = self.ctx.message().map_or(0, |m| m.text(&size).len()); + + self.ctx.clear_selection(); + self.ctx.pop_message(); + + // Reset cursor when message bar height changed or all messages are gone. + let new_lines = self.ctx.message().map_or(0, |m| m.text(&size).len()); + + let new_icon = match current_lines.cmp(&new_lines) { + Ordering::Less => CursorIcon::Default, + Ordering::Equal => CursorIcon::Pointer, + Ordering::Greater => { + if self.ctx.mouse_mode() { + CursorIcon::Default + } else { + CursorIcon::Text + } + }, + }; + + self.ctx.window().set_mouse_cursor(new_icon); + } else { + match state { + ElementState::Pressed => { + // Process mouse press before bindings to update the `click_state`. + self.on_mouse_press(button); + self.process_mouse_bindings(button); + }, + ElementState::Released => self.on_mouse_release(button), + } + } + } + + /// Attempt to find a binding and execute its action. + /// + /// The provided mode, mods, and key must match what is allowed by a binding + /// for its action to be executed. + fn process_mouse_bindings(&mut self, button: MouseButton) { + let mode = BindingMode::new(self.ctx.terminal().mode(), self.ctx.search_active()); + let mouse_mode = self.ctx.mouse_mode(); + let mods = self.ctx.modifiers().state(); + let mouse_bindings = self.ctx.config().mouse_bindings().to_owned(); + + // If mouse mode is active, also look for bindings without shift. + let mut check_fallback = mouse_mode && mods.contains(ModifiersState::SHIFT); + + for binding in &mouse_bindings { + if binding.is_triggered_by(mode, mods, &button) { + binding.action.execute(&mut self.ctx); + check_fallback = false; + } + } + + if check_fallback { + let fallback_mods = mods & !ModifiersState::SHIFT; + for binding in &mouse_bindings { + if binding.is_triggered_by(mode, fallback_mods, &button) { + binding.action.execute(&mut self.ctx); + } + } + } + } + + /// Check mouse icon state in relation to the message bar. + fn message_bar_cursor_state(&self) -> Option<CursorIcon> { + // Since search is above the message bar, the button is offset by search's height. + let search_height = usize::from(self.ctx.search_active()); + + // Calculate Y position of the end of the last terminal line. + let size = self.ctx.size_info(); + let terminal_end = size.padding_y() as usize + + size.cell_height() as usize * (size.screen_lines() + search_height); + + let mouse = self.ctx.mouse(); + let display_offset = self.ctx.terminal().grid().display_offset(); + let point = self.ctx.mouse().point(&self.ctx.size_info(), display_offset); + + if self.ctx.message().is_none() || (mouse.y <= terminal_end) { + None + } else if mouse.y <= terminal_end + size.cell_height() as usize + && point.column + message_bar::CLOSE_BUTTON_TEXT.len() >= size.columns() + { + Some(CursorIcon::Pointer) + } else { + Some(CursorIcon::Default) + } + } + + /// Icon state of the cursor. + fn cursor_state(&mut self) -> CursorIcon { + let display_offset = self.ctx.terminal().grid().display_offset(); + let point = self.ctx.mouse().point(&self.ctx.size_info(), display_offset); + let hyperlink = self.ctx.terminal().grid()[point].hyperlink(); + + // Function to check if mouse is on top of a hint. + let hint_highlighted = |hint: &HintMatch| hint.should_highlight(point, hyperlink.as_ref()); + + if let Some(mouse_state) = self.message_bar_cursor_state() { + mouse_state + } else if self.ctx.display().highlighted_hint.as_ref().map_or(false, hint_highlighted) { + CursorIcon::Pointer + } else if !self.ctx.modifiers().state().shift_key() && self.ctx.mouse_mode() { + CursorIcon::Default + } else { + CursorIcon::Text + } + } + + /// Handle automatic scrolling when selecting above/below the window. + fn update_selection_scrolling(&mut self, mouse_y: i32) { + let scale_factor = self.ctx.window().scale_factor; + let size = self.ctx.size_info(); + let window_id = self.ctx.window().id(); + let scheduler = self.ctx.scheduler_mut(); + + // Scale constants by DPI. + let min_height = (MIN_SELECTION_SCROLLING_HEIGHT * scale_factor) as i32; + let step = (SELECTION_SCROLLING_STEP * scale_factor) as i32; + + // Compute the height of the scrolling areas. + let end_top = max(min_height, size.padding_y() as i32); + let text_area_bottom = size.padding_y() + size.screen_lines() as f32 * size.cell_height(); + let start_bottom = min(size.height() as i32 - min_height, text_area_bottom as i32); + + // Get distance from closest window boundary. + let delta = if mouse_y < end_top { + end_top - mouse_y + step + } else if mouse_y >= start_bottom { + start_bottom - mouse_y - step + } else { + scheduler.unschedule(TimerId::new(Topic::SelectionScrolling, window_id)); + return; + }; + + // Scale number of lines scrolled based on distance to boundary. + let event = Event::new(EventType::Scroll(Scroll::Delta(delta / step)), Some(window_id)); + + // Schedule event. + let timer_id = TimerId::new(Topic::SelectionScrolling, window_id); + scheduler.unschedule(timer_id); + scheduler.schedule(event, SELECTION_SCROLLING_INTERVAL, true, timer_id); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use winit::event::{DeviceId, Event as WinitEvent, WindowEvent}; + use winit::keyboard::Key; + use winit::window::WindowId; + + use alacritty_terminal::event::Event as TerminalEvent; + + use crate::config::Binding; + use crate::message_bar::MessageBuffer; + + const KEY: Key<&'static str> = Key::Character("0"); + + struct MockEventProxy; + impl EventListener for MockEventProxy {} + + struct ActionContext<'a, T> { + pub terminal: &'a mut Term<T>, + pub size_info: &'a SizeInfo, + pub mouse: &'a mut Mouse, + pub clipboard: &'a mut Clipboard, + pub message_buffer: &'a mut MessageBuffer, + pub modifiers: Modifiers, + config: &'a UiConfig, + inline_search_state: &'a mut InlineSearchState, + } + + impl<'a, T: EventListener> super::ActionContext<T> for ActionContext<'a, T> { + fn search_next( + &mut self, + _origin: Point, + _direction: Direction, + _side: Side, + ) -> Option<Match> { + None + } + + fn search_direction(&self) -> Direction { + Direction::Right + } + + fn inline_search_state(&mut self) -> &mut InlineSearchState { + self.inline_search_state + } + + fn search_active(&self) -> bool { + false + } + + fn terminal(&self) -> &Term<T> { + self.terminal + } + + fn terminal_mut(&mut self) -> &mut Term<T> { + self.terminal + } + + fn size_info(&self) -> SizeInfo { + *self.size_info + } + + fn selection_is_empty(&self) -> bool { + true + } + + fn scroll(&mut self, scroll: Scroll) { + self.terminal.scroll_display(scroll); + } + + fn mouse_mode(&self) -> bool { + false + } + + #[inline] + fn mouse_mut(&mut self) -> &mut Mouse { + self.mouse + } + + #[inline] + fn mouse(&self) -> &Mouse { + self.mouse + } + + #[inline] + fn touch_purpose(&mut self) -> &mut TouchPurpose { + unimplemented!(); + } + + fn modifiers(&mut self) -> &mut Modifiers { + &mut self.modifiers + } + + fn window(&mut self) -> &mut Window { + unimplemented!(); + } + + fn display(&mut self) -> &mut Display { + unimplemented!(); + } + + fn pop_message(&mut self) { + self.message_buffer.pop(); + } + + fn message(&self) -> Option<&Message> { + self.message_buffer.message() + } + + fn config(&self) -> &UiConfig { + self.config + } + + fn clipboard_mut(&mut self) -> &mut Clipboard { + self.clipboard + } + + fn event_loop(&self) -> &EventLoopWindowTarget<Event> { + unimplemented!(); + } + + fn scheduler_mut(&mut self) -> &mut Scheduler { + unimplemented!(); + } + } + + macro_rules! test_clickstate { + { + name: $name:ident, + initial_state: $initial_state:expr, + initial_button: $initial_button:expr, + input: $input:expr, + end_state: $end_state:expr, + input_delay: $input_delay:expr, + } => { + #[test] + fn $name() { + let mut clipboard = Clipboard::new_nop(); + let cfg = UiConfig::default(); + let size = SizeInfo::new( + 21.0, + 51.0, + 3.0, + 3.0, + 0., + 0., + false, + ); + + let mut terminal = Term::new(cfg.term_options(), &size, MockEventProxy); + + let mut mouse = Mouse { + click_state: $initial_state, + last_click_button: $initial_button, + last_click_timestamp: Instant::now() - $input_delay, + ..Mouse::default() + }; + + let mut inline_search_state = InlineSearchState::default(); + let mut message_buffer = MessageBuffer::default(); + + let context = ActionContext { + terminal: &mut terminal, + mouse: &mut mouse, + size_info: &size, + clipboard: &mut clipboard, + modifiers: Default::default(), + message_buffer: &mut message_buffer, + inline_search_state: &mut inline_search_state, + config: &cfg, + }; + + let mut processor = Processor::new(context); + + let event: WinitEvent::<TerminalEvent> = $input; + if let WinitEvent::WindowEvent { + event: WindowEvent::MouseInput { + state, + button, + .. + }, + .. + } = event + { + processor.mouse_input(state, button); + }; + + assert_eq!(processor.ctx.mouse.click_state, $end_state); + } + } + } + + macro_rules! test_process_binding { + { + name: $name:ident, + binding: $binding:expr, + triggers: $triggers:expr, + mode: $mode:expr, + mods: $mods:expr, + } => { + #[test] + fn $name() { + if $triggers { + assert!($binding.is_triggered_by($mode, $mods, &KEY)); + } else { + assert!(!$binding.is_triggered_by($mode, $mods, &KEY)); + } + } + } + } + + test_clickstate! { + name: single_click, + initial_state: ClickState::None, + initial_button: MouseButton::Other(0), + input: WinitEvent::WindowEvent { + event: WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Left, + device_id: unsafe { DeviceId::dummy() }, + }, + window_id: unsafe { WindowId::dummy() }, + }, + end_state: ClickState::Click, + input_delay: Duration::ZERO, + } + + test_clickstate! { + name: single_right_click, + initial_state: ClickState::None, + initial_button: MouseButton::Other(0), + input: WinitEvent::WindowEvent { + event: WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Right, + device_id: unsafe { DeviceId::dummy() }, + }, + window_id: unsafe { WindowId::dummy() }, + }, + end_state: ClickState::Click, + input_delay: Duration::ZERO, + } + + test_clickstate! { + name: single_middle_click, + initial_state: ClickState::None, + initial_button: MouseButton::Other(0), + input: WinitEvent::WindowEvent { + event: WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Middle, + device_id: unsafe { DeviceId::dummy() }, + }, + window_id: unsafe { WindowId::dummy() }, + }, + end_state: ClickState::Click, + input_delay: Duration::ZERO, + } + + test_clickstate! { + name: double_click, + initial_state: ClickState::Click, + initial_button: MouseButton::Left, + input: WinitEvent::WindowEvent { + event: WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Left, + device_id: unsafe { DeviceId::dummy() }, + }, + window_id: unsafe { WindowId::dummy() }, + }, + end_state: ClickState::DoubleClick, + input_delay: Duration::ZERO, + } + + test_clickstate! { + name: double_click_failed, + initial_state: ClickState::Click, + initial_button: MouseButton::Left, + input: WinitEvent::WindowEvent { + event: WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Left, + device_id: unsafe { DeviceId::dummy() }, + }, + window_id: unsafe { WindowId::dummy() }, + }, + end_state: ClickState::Click, + input_delay: CLICK_THRESHOLD, + } + + test_clickstate! { + name: triple_click, + initial_state: ClickState::DoubleClick, + initial_button: MouseButton::Left, + input: WinitEvent::WindowEvent { + event: WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Left, + device_id: unsafe { DeviceId::dummy() }, + }, + window_id: unsafe { WindowId::dummy() }, + }, + end_state: ClickState::TripleClick, + input_delay: Duration::ZERO, + } + + test_clickstate! { + name: triple_click_failed, + initial_state: ClickState::DoubleClick, + initial_button: MouseButton::Left, + input: WinitEvent::WindowEvent { + event: WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Left, + device_id: unsafe { DeviceId::dummy() }, + }, + window_id: unsafe { WindowId::dummy() }, + }, + end_state: ClickState::Click, + input_delay: CLICK_THRESHOLD, + } + + test_clickstate! { + name: multi_click_separate_buttons, + initial_state: ClickState::DoubleClick, + initial_button: MouseButton::Left, + input: WinitEvent::WindowEvent { + event: WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Right, + device_id: unsafe { DeviceId::dummy() }, + }, + window_id: unsafe { WindowId::dummy() }, + }, + end_state: ClickState::Click, + input_delay: Duration::ZERO, + } + + test_process_binding! { + name: process_binding_nomode_shiftmod_require_shift, + binding: Binding { trigger: KEY, mods: ModifiersState::SHIFT, action: Action::from("\x1b[1;2D"), mode: BindingMode::empty(), notmode: BindingMode::empty() }, + triggers: true, + mode: BindingMode::empty(), + mods: ModifiersState::SHIFT, + } + + test_process_binding! { + name: process_binding_nomode_nomod_require_shift, + binding: Binding { trigger: KEY, mods: ModifiersState::SHIFT, action: Action::from("\x1b[1;2D"), mode: BindingMode::empty(), notmode: BindingMode::empty() }, + triggers: false, + mode: BindingMode::empty(), + mods: ModifiersState::empty(), + } + + test_process_binding! { + name: process_binding_nomode_controlmod, + binding: Binding { trigger: KEY, mods: ModifiersState::CONTROL, action: Action::from("\x1b[1;5D"), mode: BindingMode::empty(), notmode: BindingMode::empty() }, + triggers: true, + mode: BindingMode::empty(), + mods: ModifiersState::CONTROL, + } + + test_process_binding! { + name: process_binding_nomode_nomod_require_not_appcursor, + binding: Binding { trigger: KEY, mods: ModifiersState::empty(), action: Action::from("\x1b[D"), mode: BindingMode::empty(), notmode: BindingMode::APP_CURSOR }, + triggers: true, + mode: BindingMode::empty(), + mods: ModifiersState::empty(), + } + + test_process_binding! { + name: process_binding_appcursormode_nomod_require_appcursor, + binding: Binding { trigger: KEY, mods: ModifiersState::empty(), action: Action::from("\x1bOD"), mode: BindingMode::APP_CURSOR, notmode: BindingMode::empty() }, + triggers: true, + mode: BindingMode::APP_CURSOR, + mods: ModifiersState::empty(), + } + + test_process_binding! { + name: process_binding_nomode_nomod_require_appcursor, + binding: Binding { trigger: KEY, mods: ModifiersState::empty(), action: Action::from("\x1bOD"), mode: BindingMode::APP_CURSOR, notmode: BindingMode::empty() }, + triggers: false, + mode: BindingMode::empty(), + mods: ModifiersState::empty(), + } + + test_process_binding! { + name: process_binding_appcursormode_appkeypadmode_nomod_require_appcursor, + binding: Binding { trigger: KEY, mods: ModifiersState::empty(), action: Action::from("\x1bOD"), mode: BindingMode::APP_CURSOR, notmode: BindingMode::empty() }, + triggers: true, + mode: BindingMode::APP_CURSOR | BindingMode::APP_KEYPAD, + mods: ModifiersState::empty(), + } + + test_process_binding! { + name: process_binding_fail_with_extra_mods, + binding: Binding { trigger: KEY, mods: ModifiersState::SUPER, action: Action::from("arst"), mode: BindingMode::empty(), notmode: BindingMode::empty() }, + triggers: false, + mode: BindingMode::empty(), + mods: ModifiersState::ALT | ModifiersState::SUPER, + } +} |