diff options
Diffstat (limited to 'alacritty/src/input.rs')
-rw-r--r-- | alacritty/src/input.rs | 1634 |
1 files changed, 0 insertions, 1634 deletions
diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs deleted file mode 100644 index 2c853488..00000000 --- a/alacritty/src/input.rs +++ /dev/null @@ -1,1634 +0,0 @@ -//! 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, KeyEvent, Modifiers, MouseButton, MouseScrollDelta, Touch as TouchEvent, - TouchPhase, -}; -use winit::event_loop::EventLoopWindowTarget; -#[cfg(target_os = "macos")] -use winit::keyboard::ModifiersKeyState; -use winit::keyboard::{Key, ModifiersState}; -#[cfg(target_os = "macos")] -use winit::platform::macos::{EventLoopWindowTargetExtMacOS, OptionAsAlt}; -use winit::platform::modifier_supplement::KeyEventExtModifierSupplement; -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, BindingKey, 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, - TYPING_SEARCH_DELAY, -}; -use crate::message_bar::{self, Message}; -use crate::scheduler::{Scheduler, TimerId, Topic}; - -/// 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); - } - } - } - } - - /// Process key input. - pub fn key_input(&mut self, key: KeyEvent) { - // IME input will be applied on commit and shouldn't trigger key bindings. - if key.state == ElementState::Released || self.ctx.display().ime.preedit().is_some() { - return; - } - - let text = key.text_with_all_modifiers().unwrap_or_default(); - - // All key bindings are disabled while a hint is being selected. - if self.ctx.display().hint_state.active() { - for character in text.chars() { - self.ctx.hint_input(character); - } - return; - } - - // First key after inline search is captured. - let inline_state = self.ctx.inline_search_state(); - if mem::take(&mut inline_state.char_pending) { - if let Some(c) = text.chars().next() { - inline_state.character = Some(c); - - // Immediately move to the captured character. - self.ctx.inline_search_next(); - } - - // Ignore all other characters in `text`. - return; - } - - // Reset search delay when the user is still typing. - if self.ctx.search_active() { - let timer_id = TimerId::new(Topic::DelayedSearch, self.ctx.window().id()); - let scheduler = self.ctx.scheduler_mut(); - if let Some(timer) = scheduler.unschedule(timer_id) { - scheduler.schedule(timer.event, TYPING_SEARCH_DELAY, false, timer.id); - } - } - - // Key bindings suppress the character input. - if self.process_key_bindings(&key) { - return; - } - - if self.ctx.search_active() { - for character in text.chars() { - self.ctx.search_input(character); - } - - return; - } - - // Vi mode on its own doesn't have any input, the search input was done before. - if self.ctx.terminal().mode().contains(TermMode::VI) || text.is_empty() { - return; - } - - self.ctx.on_terminal_input_start(); - - let mut bytes = Vec::with_capacity(text.len() + 1); - if self.alt_send_esc() && text.len() == 1 { - bytes.push(b'\x1b'); - } - bytes.extend_from_slice(text.as_bytes()); - - self.ctx.write_to_pty(bytes); - } - - /// Whether we should send `ESC` due to `Alt` being pressed. - #[cfg(not(target_os = "macos"))] - fn alt_send_esc(&mut self) -> bool { - self.ctx.modifiers().state().alt_key() - } - - #[cfg(target_os = "macos")] - fn alt_send_esc(&mut self) -> bool { - let option_as_alt = self.ctx.config().window.option_as_alt(); - self.ctx.modifiers().state().alt_key() - && (option_as_alt == OptionAsAlt::Both - || (option_as_alt == OptionAsAlt::OnlyLeft - && self.ctx.modifiers().lalt_state() == ModifiersKeyState::Pressed) - || (option_as_alt == OptionAsAlt::OnlyRight - && self.ctx.modifiers().ralt_state() == ModifiersKeyState::Pressed)) - } - - /// 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_key_bindings(&mut self, key: &KeyEvent) -> bool { - let mode = BindingMode::new(self.ctx.terminal().mode(), self.ctx.search_active()); - let mods = self.ctx.modifiers().state(); - - // Don't suppress char if no bindings were triggered. - let mut suppress_chars = None; - - for i in 0..self.ctx.config().key_bindings().len() { - let binding = &self.ctx.config().key_bindings()[i]; - - // We don't want the key without modifier, because it means something else most of - // the time. However what we want is to manually lowercase the character to account - // for both small and capital letters on regular characters at the same time. - let logical_key = if let Key::Character(ch) = key.logical_key.as_ref() { - Key::Character(ch.to_lowercase().into()) - } else { - key.logical_key.clone() - }; - - let key = match (&binding.trigger, logical_key) { - (BindingKey::Scancode(_), _) => BindingKey::Scancode(key.physical_key), - (_, code) => BindingKey::Keycode { key: code, location: key.location }, - }; - - if binding.is_triggered_by(mode, mods, &key) { - // Pass through the key if any of the bindings has the `ReceiveChar` action. - *suppress_chars.get_or_insert(true) &= binding.action != Action::ReceiveChar; - - // Binding was triggered; run the action. - binding.action.clone().execute(&mut self.ctx); - } - } - - suppress_chars.unwrap_or(false) - } - - /// 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, - } -} |