alacritty/alacritty/src/input.rs

1347 lines
50 KiB
Rust
Raw Normal View History

2020-05-05 22:50:23 +00:00
//! Handle input from glutin.
//!
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};
use std::ffi::OsStr;
use std::fmt::Debug;
use std::marker::PhantomData;
use std::time::{Duration, Instant};
use glutin::dpi::PhysicalPosition;
use glutin::event::{
ElementState, KeyboardInput, ModifiersState, MouseButton, MouseScrollDelta, TouchPhase,
};
use glutin::event_loop::EventLoopWindowTarget;
#[cfg(target_os = "macos")]
use glutin::platform::macos::EventLoopWindowTargetExtMacOS;
use glutin::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, BindingMode, Key, 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, Mouse, 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.;
/// Processes input from glutin.
///
/// 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 received_count(&mut self) -> &mut usize;
fn suppress_chars(&mut self) -> &mut bool;
fn modifiers(&mut self) -> &mut ModifiersState;
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) {}
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 paste(&mut self, _text: &str) {}
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.on_typing_start();
ctx.clear_selection();
ctx.scroll(Scroll::Bottom);
ctx.write_to_pty(s.clone().into_bytes())
},
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::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));
},
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);
2020-12-19 04:07:20 +00:00
},
Action::PasteSelection => {
let text = ctx.clipboard_mut().load(ClipboardType::Selection);
ctx.paste(&text);
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(),
Action::SpawnNewInstance => ctx.spawn_new_instance(),
Action::CreateNewWindow => ctx.create_new_window(),
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 = min(max(x, 0), size_info.width() as i32 - 1) as usize;
let y = min(max(y, 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().shift() || !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();
if modifiers.shift() {
mods += 4;
}
if modifiers.alt() {
mods += 8;
}
if modifiers.ctrl() {
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().shift() && 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::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.
let mouse_config = &self.ctx.config().mouse;
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
},
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
ClickState::Click if elapsed < mouse_config.double_click.threshold() => {
ClickState::DoubleClick
},
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
ClickState::DoubleClick if elapsed < mouse_config.triple_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().ctrl() {
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;
}
}
fn on_mouse_release(&mut self, button: MouseButton) {
if !self.ctx.modifiers().shift() && 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::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) {
match delta {
MouseScrollDelta::LineDelta(_columns, lines) => {
let new_scroll_px = lines * self.ctx.size_info().cell_height();
self.scroll_terminal(f64::from(new_scroll_px));
},
Upgrade Glutin to v0.19.0 Some changes include: • Use the with_hardware_acceleration function on the ContextBuilder to not require the discrete GPU • Remove the LMenu and RMenu virtual key codes (winit 0.16.0 removed these because Windows now generates LAlt and RAlt instead • Replace set_cursor_state with hide_cursor (winit 0.16.0 removed the set_cursor_state function) • Replace GlWindow::hidpi_factor with GlWindow::get_hidpi_factor and change to expecting an f64 • Use the glutin/winit dpi size and position types where possible Glutin's dpi change event has been implemented. All size events now return logical sizes. As a result of that, the logical sizes are translated in the `display::handle_rezize` method so DPI scaling works correctly. When the DPI is changed, the glyph cache is updated to make use of the correct font size again. Moving a window to a different screen which is a different DPI caused a racing condition where the logical size of the event was sent to the `handle_resize` method in `src/display.rs`, however if there was a DPI change event before `handle_resize` is able to process this message, it would incorrectly use the new DPI to scale the resize event. To solve this issue instead of sending the logical size to the `handle_resize` method and then converting it to a physical size in there, the `LogicalSize` of the resize event is transformed into a `PhysicalSize` as soon as it's received. This fixes potential racing conditions since all events are processed in order. The padding has been changed so it's also scaled by DPR. The `scale_with_dpi` config option has been removed. If it's not present a warning will be emitted. The `winit` dependency on Windows has been removed. All interactions with winit in Alacritty are handled through glutin.
2018-11-10 16:08:48 +00:00
MouseScrollDelta::PixelDelta(lpos) => {
match phase {
TouchPhase::Started => {
2020-05-05 22:50:23 +00:00
// Reset offset to zero.
self.ctx.mouse_mut().scroll_px = 0.;
},
TouchPhase::Moved => {
self.scroll_terminal(lpos.y);
},
_ => (),
}
2019-03-30 16:48:36 +00:00
},
}
}
fn scroll_terminal(&mut self, new_scroll_px: f64) {
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().scroll_px += new_scroll_px;
let code = if new_scroll_px > 0. { 64 } else { 65 };
let lines = (self.ctx.mouse().scroll_px / height).abs() as i32;
for _ in 0..lines {
self.mouse_report(code, ElementState::Pressed);
}
} else if self
.ctx
.terminal()
.mode()
.contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
&& !self.ctx.modifiers().shift()
{
let multiplier = f64::from(self.ctx.config().terminal_config.scrolling.multiplier);
self.ctx.mouse_mut().scroll_px += new_scroll_px * multiplier;
let cmd = if new_scroll_px > 0. { b'A' } else { b'B' };
let lines = (self.ctx.mouse().scroll_px / height).abs() as i32;
let mut content = Vec::with_capacity(lines as usize * 3);
for _ in 0..lines {
2018-03-09 10:17:47 +00:00
content.push(0x1b);
content.push(b'O');
content.push(cmd);
}
self.ctx.write_to_pty(content);
} else {
let multiplier = f64::from(self.ctx.config().terminal_config.scrolling.multiplier);
self.ctx.mouse_mut().scroll_px += new_scroll_px * multiplier;
let lines = self.ctx.mouse().scroll_px / height;
self.ctx.scroll(Scroll::Delta(lines as i32));
}
self.ctx.mouse_mut().scroll_px %= 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());
}
}
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::Hand)
&& state == ElementState::Pressed
{
let size = self.ctx.size_info();
let current_lines = self.ctx.message().map(|m| m.text(&size).len()).unwrap_or(0);
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(|m| m.text(&size).len()).unwrap_or(0);
let new_icon = match current_lines.cmp(&new_lines) {
Ordering::Less => CursorIcon::Default,
Ordering::Equal => CursorIcon::Hand,
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),
}
}
}
/// Process key input.
pub fn key_input(&mut self, input: KeyboardInput) {
// All key bindings are disabled while a hint is being selected.
if self.ctx.display().hint_state.active() {
*self.ctx.suppress_chars() = false;
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
}
}
2020-12-19 04:07:20 +00:00
match input.state {
ElementState::Pressed => {
*self.ctx.received_count() = 0;
self.process_key_bindings(input);
},
ElementState::Released => *self.ctx.suppress_chars() = false,
}
}
/// Modifier state change.
pub fn modifiers_input(&mut self, modifiers: ModifiersState) {
*self.ctx.modifiers() = modifiers;
// Prompt hint highlight update.
self.ctx.mouse_mut().hint_highlight_dirty = true;
2020-05-05 22:50:23 +00:00
// Update mouse state and check for URL change.
let mouse_state = self.cursor_state();
self.ctx.window().set_mouse_cursor(mouse_state);
}
/// 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);
}
/// Process a received character.
pub fn received_char(&mut self, c: char) {
let suppress_chars = *self.ctx.suppress_chars();
// Handle hint selection over anything else.
if self.ctx.display().hint_state.active() && !suppress_chars {
self.ctx.hint_input(c);
return;
}
// Pass keys to search and ignore them during `suppress_chars`.
let search_active = self.ctx.search_active();
if suppress_chars || search_active || self.ctx.terminal().mode().contains(TermMode::VI) {
2020-12-19 04:07:20 +00:00
if search_active && !suppress_chars {
self.ctx.search_input(c);
}
2019-02-16 20:23:23 +00:00
return;
}
self.ctx.on_typing_start();
if self.ctx.terminal().grid().display_offset() != 0 {
self.ctx.scroll(Scroll::Bottom);
}
2019-02-16 20:23:23 +00:00
self.ctx.clear_selection();
2019-02-16 20:23:23 +00:00
let utf8_len = c.len_utf8();
let mut bytes = vec![0; utf8_len];
c.encode_utf8(&mut bytes[..]);
if self.ctx.config().alt_send_esc
2019-02-16 20:23:23 +00:00
&& *self.ctx.received_count() == 0
&& self.ctx.modifiers().alt()
2019-02-16 20:23:23 +00:00
&& utf8_len == 1
{
bytes.insert(0, b'\x1b');
}
2019-02-16 20:23:23 +00:00
self.ctx.write_to_pty(bytes);
*self.ctx.received_count() += 1;
}
/// 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, input: KeyboardInput) {
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();
let mut suppress_chars = None;
for i in 0..self.ctx.config().key_bindings().len() {
let binding = &self.ctx.config().key_bindings()[i];
let key = match (binding.trigger, input.virtual_keycode) {
(Key::Scancode(_), _) => Key::Scancode(input.scancode),
2020-01-06 22:06:09 +00:00
(_, Some(key)) => Key::Keycode(key),
_ => continue,
};
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);
}
}
2020-05-05 22:50:23 +00:00
// Don't suppress char if no bindings were triggered.
*self.ctx.suppress_chars() = suppress_chars.unwrap_or(false);
}
/// 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) {
2020-12-19 04:07:20 +00:00
let mode = BindingMode::new(self.ctx.terminal().mode(), self.ctx.search_active());
let mouse_mode = self.ctx.mouse_mode();
2020-12-19 04:07:20 +00:00
let mods = *self.ctx.modifiers();
for i in 0..self.ctx.config().mouse_bindings().len() {
let mut binding = self.ctx.config().mouse_bindings()[i].clone();
2020-05-05 22:50:23 +00:00
// 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);
}
}
}
/// 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 = if self.ctx.search_active() { 1 } else { 0 };
// 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::Hand)
} 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::Hand
} else if !self.ctx.modifiers().shift() && 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 delta = delta as i32 / step as i32;
let event = Event::new(EventType::Scroll(Scroll::Delta(delta)), 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::*;
2022-07-08 22:37:22 +00:00
use glutin::event::{DeviceId, Event as GlutinEvent, VirtualKeyCode, WindowEvent};
use glutin::window::WindowId;
use alacritty_terminal::event::Event as TerminalEvent;
use crate::config::Binding;
use crate::message_bar::MessageBuffer;
const KEY: VirtualKeyCode = VirtualKeyCode::Key0;
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 received_count: usize,
pub suppress_chars: bool,
pub modifiers: ModifiersState,
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
}
fn received_count(&mut self) -> &mut usize {
&mut self.received_count
}
fn suppress_chars(&mut self) -> &mut bool {
&mut self.suppress_chars
}
fn modifiers(&mut self) -> &mut ModifiersState {
&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,
} => {
#[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,
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,
received_count: 0,
suppress_chars: false,
modifiers: Default::default(),
message_buffer: &mut message_buffer,
config: &cfg,
};
let mut processor = Processor::new(context);
let event: GlutinEvent::<'_, TerminalEvent> = $input;
if let GlutinEvent::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: GlutinEvent::WindowEvent {
event: WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Left,
2022-07-08 22:37:22 +00:00
device_id: unsafe { DeviceId::dummy() },
modifiers: ModifiersState::default(),
},
2022-07-08 22:37:22 +00:00
window_id: unsafe { WindowId::dummy() },
},
end_state: ClickState::Click,
}
test_clickstate! {
name: single_right_click,
initial_state: ClickState::None,
initial_button: MouseButton::Other(0),
input: GlutinEvent::WindowEvent {
event: WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Right,
2022-07-08 22:37:22 +00:00
device_id: unsafe { DeviceId::dummy() },
modifiers: ModifiersState::default(),
},
2022-07-08 22:37:22 +00:00
window_id: unsafe { WindowId::dummy() },
},
end_state: ClickState::Click,
}
test_clickstate! {
name: single_middle_click,
initial_state: ClickState::None,
initial_button: MouseButton::Other(0),
input: GlutinEvent::WindowEvent {
event: WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Middle,
2022-07-08 22:37:22 +00:00
device_id: unsafe { DeviceId::dummy() },
modifiers: ModifiersState::default(),
},
2022-07-08 22:37:22 +00:00
window_id: unsafe { WindowId::dummy() },
},
end_state: ClickState::Click,
}
test_clickstate! {
name: double_click,
initial_state: ClickState::Click,
initial_button: MouseButton::Left,
input: GlutinEvent::WindowEvent {
event: WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Left,
2022-07-08 22:37:22 +00:00
device_id: unsafe { DeviceId::dummy() },
modifiers: ModifiersState::default(),
},
2022-07-08 22:37:22 +00:00
window_id: unsafe { WindowId::dummy() },
},
end_state: ClickState::DoubleClick,
}
test_clickstate! {
name: triple_click,
initial_state: ClickState::DoubleClick,
initial_button: MouseButton::Left,
input: GlutinEvent::WindowEvent {
event: WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Left,
2022-07-08 22:37:22 +00:00
device_id: unsafe { DeviceId::dummy() },
modifiers: ModifiersState::default(),
},
2022-07-08 22:37:22 +00:00
window_id: unsafe { WindowId::dummy() },
},
end_state: ClickState::TripleClick,
}
test_clickstate! {
name: multi_click_separate_buttons,
initial_state: ClickState::DoubleClick,
initial_button: MouseButton::Left,
input: GlutinEvent::WindowEvent {
event: WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Right,
2022-07-08 22:37:22 +00:00
device_id: unsafe { DeviceId::dummy() },
modifiers: ModifiersState::default(),
},
2022-07-08 22:37:22 +00:00
window_id: unsafe { WindowId::dummy() },
},
end_state: ClickState::Click,
}
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,
2020-12-19 04:07:20 +00:00
binding: Binding { trigger: KEY, mods: ModifiersState::CTRL, 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::CTRL,
}
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,
2020-12-19 04:07:20 +00:00
binding: Binding { trigger: KEY, mods: ModifiersState::LOGO, 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::LOGO,
}
}