alacritty/alacritty/src/input.rs

1592 lines
61 KiB
Rust
Raw Normal View History

//! Handle input from winit.
//!
2020-05-05 22:50:23 +00:00
//! 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};
2023-02-13 22:35:18 +00:00
use std::collections::HashSet;
use std::ffi::OsStr;
use std::fmt::Debug;
use std::marker::PhantomData;
2023-02-13 22:35:18 +00:00
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::ansi::{ClearMode, Handler};
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 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};
2023-02-13 22:35:18 +00:00
use crate::event::{
ClickState, Event, EventType, Mouse, TouchPurpose, TouchZoom, TYPING_SEARCH_DELAY,
};
use crate::message_bar::{self, Message};
use crate::scheduler::{Scheduler, TimerId, Topic};
2020-05-05 22:50:23 +00:00
/// 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.;
2023-02-13 22:35:18 +00:00
/// 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;
2023-02-13 22:35:18 +00:00
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 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);
2020-05-05 22:50:23 +00:00
// 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();
},
2021-07-03 03:06:52 +00:00
Action::Vi(ViAction::ToggleNormalSelection) => {
Self::toggle_selection(ctx, SelectionType::Simple);
},
2021-07-03 03:06:52 +00:00
Action::Vi(ViAction::ToggleLineSelection) => {
Self::toggle_selection(ctx, SelectionType::Lines);
},
2021-07-03 03:06:52 +00:00
Action::Vi(ViAction::ToggleBlockSelection) => {
Self::toggle_selection(ctx, SelectionType::Block);
},
2021-07-03 03:06:52 +00:00
Action::Vi(ViAction::ToggleSemanticSelection) => {
Self::toggle_selection(ctx, SelectionType::Semantic);
},
2021-07-03 03:06:52 +00:00
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;
},
2021-07-03 03:06:52 +00:00
Action::Vi(ViAction::SearchNext) => {
ctx.on_typing_start();
2021-01-03 11:24:04 +00:00
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();
}
},
2021-07-03 03:06:52 +00:00
Action::Vi(ViAction::SearchPrevious) => {
ctx.on_typing_start();
2021-01-03 11:24:04 +00:00
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();
}
},
2021-07-03 03:06:52 +00:00
Action::Vi(ViAction::SearchStart) => {
let terminal = ctx.terminal();
let origin = terminal.vi_mode_cursor.point.sub(terminal, Boundary::None, 1);
2021-01-03 11:24:04 +00:00
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();
}
},
2021-07-03 03:06:52 +00:00
Action::Vi(ViAction::SearchEnd) => {
let terminal = ctx.terminal();
let origin = terminal.vi_mode_cursor.point.add(terminal, Boundary::None, 1);
2021-01-03 11:24:04 +00:00
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 @ Action::Search(_) if !ctx.search_active() => {
debug!("Ignoring {action:?}: Search mode inactive");
},
2021-07-03 03:06:52 +00:00
Action::Search(SearchAction::SearchFocusNext) => {
2020-12-19 04:07:20 +00:00
ctx.advance_search_origin(ctx.search_direction());
},
2021-07-03 03:06:52 +00:00
Action::Search(SearchAction::SearchFocusPrevious) => {
2020-12-19 04:07:20 +00:00
let direction = ctx.search_direction().opposite();
ctx.advance_search_origin(direction);
},
2021-07-03 03:06:52 +00:00
Action::Search(SearchAction::SearchConfirm) => ctx.confirm_search(),
Action::Search(SearchAction::SearchCancel) => ctx.cancel_search(),
Action::Search(SearchAction::SearchClear) => {
2020-12-19 04:07:20 +00:00
let direction = ctx.search_direction();
ctx.cancel_search();
ctx.start_search(direction);
},
2021-07-03 03:06:52 +00:00
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),
2020-12-19 04:07:20 +00:00
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);
2020-12-19 04:07:20 +00:00
},
Action::PasteSelection => {
let text = ctx.clipboard_mut().load(ClipboardType::Selection);
ctx.paste(&text, true);
2020-12-19 04:07:20 +00:00
},
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 => {
2020-05-05 22:50:23 +00:00
// Move vi mode cursor.
let term = ctx.terminal_mut();
let scroll_lines = term.screen_lines() as i32;
term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines);
ctx.scroll(Scroll::PageUp);
},
Action::ScrollPageDown => {
2020-05-05 22:50:23 +00:00
// Move vi mode cursor.
let term = ctx.terminal_mut();
let scroll_lines = -(term.screen_lines() as i32);
term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines);
ctx.scroll(Scroll::PageDown);
},
Action::ScrollHalfPageUp => {
2020-05-05 22:50:23 +00:00
// Move vi mode cursor.
let term = ctx.terminal_mut();
let scroll_lines = term.screen_lines() as i32 / 2;
term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines);
ctx.scroll(Scroll::Delta(scroll_lines));
},
Action::ScrollHalfPageDown => {
2020-05-05 22:50:23 +00:00
// Move vi mode cursor.
let term = ctx.terminal_mut();
let scroll_lines = -(term.screen_lines() as i32 / 2);
term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines);
ctx.scroll(Scroll::Delta(scroll_lines));
},
Action::ScrollLineUp => ctx.scroll(Scroll::Delta(1)),
Action::ScrollLineDown => ctx.scroll(Scroll::Delta(-1)),
Action::ScrollToTop => {
ctx.scroll(Scroll::Top);
2020-05-05 22:50:23 +00:00
// 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);
2020-05-05 22:50:23 +00:00
// 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;
2020-05-05 22:50:23 +00:00
// 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;
2020-05-05 22:50:23 +00:00
// 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
2020-05-05 22:50:23 +00:00
// 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) {
2020-05-05 22:50:23 +00:00
// 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,
2020-05-05 22:50:23 +00:00
// 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 => {
2020-05-05 22:50:23 +00:00
// 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();
2020-05-05 22:50:23 +00:00
// 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);
}
2019-03-30 16:48:36 +00:00
},
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().terminal_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 => {
2020-05-05 22:50:23 +00:00
// 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);
},
_ => (),
}
2019-03-30 16:48:36 +00:00
},
}
}
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());
2018-03-09 22:02:45 +00:00
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 {
2018-03-09 10:17:47 +00:00
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);
2018-03-09 10:17:47 +00:00
}
2018-03-09 10:17:47 +00:00
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) {
2019-03-30 16:48:36 +00:00
let chr = if is_focused { "I" } else { "O" };
let msg = format!("\x1b[{}", chr);
self.ctx.write_to_pty(msg.into_bytes());
}
}
2023-02-13 22:35:18 +00:00
/// 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();
2023-02-13 22:35:18 +00:00
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);
2023-02-13 22:35:18 +00:00
},
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,
_ => (),
}
2020-05-05 22:50:23 +00:00
// 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();
2020-05-05 22:50:23 +00:00
// 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();
for i in 0..self.ctx.config().mouse_bindings().len() {
let mut binding = self.ctx.config().mouse_bindings()[i].clone();
// Require shift for all modifiers when mouse mode is active.
if mouse_mode {
binding.mods |= ModifiersState::SHIFT;
}
if binding.is_triggered_by(mode, 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;
}
2020-12-19 04:07:20 +00:00
// 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);
2020-12-19 04:07:20 +00:00
}
}
// 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 {
2020-12-19 04:07:20 +00:00
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 latters 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 },
};
2020-12-19 04:07:20 +00:00
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,
}
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 search_active(&self) -> bool {
false
}
fn terminal(&self) -> &Term<T> {
2021-07-03 03:06:52 +00:00
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
}
2023-02-13 22:35:18 +00:00
#[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() {
Replace serde's derive with custom proc macro This replaces the existing `Deserialize` derive from serde with a `ConfigDeserialize` derive. The goal of this new proc macro is to allow a more error-friendly deserialization for the Alacritty configuration file without having to manage a lot of boilerplate code inside the configuration modules. The first part of the derive macro is for struct deserialization. This takes structs which have `Default` implemented and will only replace fields which can be successfully deserialized. Otherwise the `log` crate is used for printing errors. Since this deserialization takes the default value from the struct instead of the value, it removes the necessity for creating new types just to implement `Default` on them for deserialization. Additionally, the struct deserialization also checks for `Option` values and makes sure that explicitly specifying `none` as text literal is allowed for all options. The other part of the derive macro is responsible for deserializing enums. While only enums with Unit variants are supported, it will automatically implement a deserializer for these enums which accepts any form of capitalization. Since this custom derive prevents us from using serde's attributes on fields, some of the attributes have been reimplemented for `ConfigDeserialize`. These include `#[config(flatten)]`, `#[config(skip)]` and `#[config(alias = "alias)]`. The flatten attribute is currently limited to at most one per struct. Additionally the `#[config(deprecated = "optional message")]` attribute allows easily defining uniform deprecation messages for fields on structs.
2020-12-21 02:44:38 +00:00
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.terminal_config, &size, MockEventProxy);
2021-01-01 05:07:39 +00:00
let mut mouse = Mouse {
click_state: $initial_state,
last_click_button: $initial_button,
last_click_timestamp: Instant::now() - $input_delay,
2021-01-01 05:07:39 +00:00
..Mouse::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,
config: &cfg,
};
let mut processor = Processor::new(context);
let event: WinitEvent::<TerminalEvent> = $input;
if let WinitEvent::WindowEvent {
event: WindowEvent::MouseInput {
state,
button,
..
},
..
2020-01-10 01:51:37 +00:00
} = 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,
2022-07-08 22:37:22 +00:00
device_id: unsafe { DeviceId::dummy() },
},
2022-07-08 22:37:22 +00:00
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,
2022-07-08 22:37:22 +00:00
device_id: unsafe { DeviceId::dummy() },
},
2022-07-08 22:37:22 +00:00
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,
2022-07-08 22:37:22 +00:00
device_id: unsafe { DeviceId::dummy() },
},
2022-07-08 22:37:22 +00:00
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,
2022-07-08 22:37:22 +00:00
device_id: unsafe { DeviceId::dummy() },
},
2022-07-08 22:37:22 +00:00
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,
2022-07-08 22:37:22 +00:00
device_id: unsafe { DeviceId::dummy() },
},
2022-07-08 22:37:22 +00:00
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,
2022-07-08 22:37:22 +00:00
device_id: unsafe { DeviceId::dummy() },
},
2022-07-08 22:37:22 +00:00
window_id: unsafe { WindowId::dummy() },
},
end_state: ClickState::Click,
input_delay: Duration::ZERO,
}
test_process_binding! {
name: process_binding_nomode_shiftmod_require_shift,
2020-12-19 04:07:20 +00:00
binding: Binding { trigger: KEY, mods: ModifiersState::SHIFT, action: Action::from("\x1b[1;2D"), mode: BindingMode::empty(), notmode: BindingMode::empty() },
triggers: true,
2020-12-19 04:07:20 +00:00
mode: BindingMode::empty(),
mods: ModifiersState::SHIFT,
}
test_process_binding! {
name: process_binding_nomode_nomod_require_shift,
2020-12-19 04:07:20 +00:00
binding: Binding { trigger: KEY, mods: ModifiersState::SHIFT, action: Action::from("\x1b[1;2D"), mode: BindingMode::empty(), notmode: BindingMode::empty() },
triggers: false,
2020-12-19 04:07:20 +00:00
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,
2020-12-19 04:07:20 +00:00
mode: BindingMode::empty(),
mods: ModifiersState::CONTROL,
}
test_process_binding! {
name: process_binding_nomode_nomod_require_not_appcursor,
2020-12-19 04:07:20 +00:00
binding: Binding { trigger: KEY, mods: ModifiersState::empty(), action: Action::from("\x1b[D"), mode: BindingMode::empty(), notmode: BindingMode::APP_CURSOR },
triggers: true,
2020-12-19 04:07:20 +00:00
mode: BindingMode::empty(),
mods: ModifiersState::empty(),
}
test_process_binding! {
name: process_binding_appcursormode_nomod_require_appcursor,
2020-12-19 04:07:20 +00:00
binding: Binding { trigger: KEY, mods: ModifiersState::empty(), action: Action::from("\x1bOD"), mode: BindingMode::APP_CURSOR, notmode: BindingMode::empty() },
triggers: true,
2020-12-19 04:07:20 +00:00
mode: BindingMode::APP_CURSOR,
mods: ModifiersState::empty(),
}
test_process_binding! {
name: process_binding_nomode_nomod_require_appcursor,
2020-12-19 04:07:20 +00:00
binding: Binding { trigger: KEY, mods: ModifiersState::empty(), action: Action::from("\x1bOD"), mode: BindingMode::APP_CURSOR, notmode: BindingMode::empty() },
triggers: false,
2020-12-19 04:07:20 +00:00
mode: BindingMode::empty(),
mods: ModifiersState::empty(),
}
test_process_binding! {
name: process_binding_appcursormode_appkeypadmode_nomod_require_appcursor,
2020-12-19 04:07:20 +00:00
binding: Binding { trigger: KEY, mods: ModifiersState::empty(), action: Action::from("\x1bOD"), mode: BindingMode::APP_CURSOR, notmode: BindingMode::empty() },
triggers: true,
2020-12-19 04:07:20 +00:00
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,
2020-12-19 04:07:20 +00:00
mode: BindingMode::empty(),
mods: ModifiersState::ALT | ModifiersState::SUPER,
}
}