aboutsummaryrefslogtreecommitdiff
path: root/alacritty/src/input.rs
diff options
context:
space:
mode:
Diffstat (limited to 'alacritty/src/input.rs')
-rw-r--r--alacritty/src/input.rs1634
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,
- }
-}