alacritty/alacritty/src/input.rs

1172 lines
42 KiB
Rust
Raw Normal View History

2016-06-30 03:56:12 +00:00
// Copyright 2016 Joe Wilm, The Alacritty Project Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//! Handle input from glutin
//!
//! 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::marker::PhantomData;
use std::mem;
2019-03-30 16:48:36 +00:00
use std::time::Instant;
use glutin::event::{
ElementState, KeyboardInput, ModifiersState, MouseButton, MouseScrollDelta, TouchPhase,
VirtualKeyCode,
};
use glutin::window::CursorIcon;
use log::{debug, trace, warn};
use alacritty_terminal::ansi::{ClearMode, Handler};
use alacritty_terminal::clipboard::ClipboardType;
use alacritty_terminal::event::EventListener;
use alacritty_terminal::grid::Scroll;
use alacritty_terminal::index::{Column, Line, Point, Side};
use alacritty_terminal::message_bar::{self, Message};
use alacritty_terminal::term::mode::TermMode;
use alacritty_terminal::term::{SizeInfo, Term};
use alacritty_terminal::url::Url;
use alacritty_terminal::util::start_daemon;
use crate::config::{Action, Binding, Config, Key, RelaxedEq};
use crate::display::FONT_SIZE_STEP;
use crate::event::{ClickState, Mouse};
use crate::window::Window;
/// Processes input from glutin.
///
/// An escape sequence may be emitted in case specific keys or key combinations
/// are activated.
pub struct Processor<'a, T: EventListener, A: ActionContext<T> + 'a> {
pub ctx: A,
pub config: &'a mut Config,
_phantom: PhantomData<T>,
}
pub trait ActionContext<T: EventListener> {
fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&mut self, _: B);
fn size_info(&self) -> SizeInfo;
fn copy_selection(&mut self, _: ClipboardType);
fn clear_selection(&mut self);
fn update_selection(&mut self, point: Point, side: Side);
fn simple_selection(&mut self, point: Point, side: Side);
fn block_selection(&mut self, point: Point, side: Side);
fn semantic_selection(&mut self, point: Point);
fn line_selection(&mut self, point: Point);
fn selection_is_empty(&self) -> bool;
fn mouse_mut(&mut self) -> &mut Mouse;
fn mouse(&self) -> &Mouse;
fn mouse_coords(&self) -> Option<Point>;
fn received_count(&mut self) -> &mut usize;
fn suppress_chars(&mut self) -> &mut bool;
fn modifiers(&mut self) -> &mut Modifiers;
fn scroll(&mut self, scroll: Scroll);
fn window(&self) -> &Window;
fn window_mut(&mut self) -> &mut Window;
fn terminal(&self) -> &Term<T>;
fn terminal_mut(&mut self) -> &mut Term<T>;
fn spawn_new_instance(&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>;
}
#[derive(Debug, Default, Copy, Clone)]
pub struct Modifiers {
mods: ModifiersState,
lshift: bool,
rshift: bool,
}
impl Modifiers {
pub fn update(&mut self, input: KeyboardInput) {
match input.virtual_keycode {
Some(VirtualKeyCode::LShift) => self.lshift = input.state == ElementState::Pressed,
Some(VirtualKeyCode::RShift) => self.rshift = input.state == ElementState::Pressed,
_ => (),
}
self.mods = input.modifiers;
}
pub fn shift(self) -> bool {
self.lshift || self.rshift
}
pub fn ctrl(self) -> bool {
self.mods.ctrl
}
pub fn logo(self) -> bool {
self.mods.logo
}
pub fn alt(self) -> bool {
self.mods.alt
}
}
impl From<&mut Modifiers> for ModifiersState {
fn from(mods: &mut Modifiers) -> ModifiersState {
ModifiersState { shift: mods.shift(), ..mods.mods }
}
}
trait Execute<T: EventListener> {
fn execute<A: ActionContext<T>>(&self, ctx: &mut A, mouse_mode: bool);
}
impl<T, U: EventListener> Execute<U> for Binding<T> {
/// Execute the action associate with this binding
#[inline]
fn execute<A: ActionContext<U>>(&self, ctx: &mut A, mouse_mode: bool) {
self.action.execute(ctx, mouse_mode)
}
}
impl<T: EventListener> Execute<T> for Action {
#[inline]
fn execute<A: ActionContext<T>>(&self, ctx: &mut A, mouse_mode: bool) {
match *self {
Action::Esc(ref s) => {
ctx.clear_selection();
ctx.scroll(Scroll::Bottom);
ctx.write_to_pty(s.clone().into_bytes())
},
Action::Copy => {
ctx.copy_selection(ClipboardType::Clipboard);
},
Action::Paste => {
let text = ctx.terminal_mut().clipboard().load(ClipboardType::Clipboard);
paste(ctx, &text);
},
Action::PasteSelection => {
// Only paste if mouse events are not captured by an application
if !mouse_mode {
let text = ctx.terminal_mut().clipboard().load(ClipboardType::Selection);
paste(ctx, &text);
}
},
Action::Command(ref program, ref args) => {
trace!("Running command {} with args {:?}", program, args);
match start_daemon(program, args) {
Ok(_) => debug!("Spawned new proc"),
Err(err) => warn!("Couldn't run command {}", err),
}
},
Action::ToggleFullscreen => ctx.window_mut().toggle_fullscreen(),
#[cfg(target_os = "macos")]
Action::ToggleSimpleFullscreen => ctx.window_mut().toggle_simple_fullscreen(),
Action::Hide => ctx.window().set_visible(false),
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 => ctx.scroll(Scroll::PageUp),
Action::ScrollPageDown => ctx.scroll(Scroll::PageDown),
Action::ScrollLineUp => ctx.scroll(Scroll::Lines(1)),
Action::ScrollLineDown => ctx.scroll(Scroll::Lines(-1)),
Action::ScrollToTop => ctx.scroll(Scroll::Top),
Action::ScrollToBottom => ctx.scroll(Scroll::Bottom),
Action::ClearHistory => ctx.terminal_mut().clear_screen(ClearMode::Saved),
Action::ClearLogNotice => ctx.pop_message(),
Action::SpawnNewInstance => ctx.spawn_new_instance(),
Action::ReceiveChar | Action::None => (),
}
}
}
fn paste<T: EventListener, A: ActionContext<T>>(ctx: &mut A, contents: &str) {
if ctx.terminal().mode().contains(TermMode::BRACKETED_PASTE) {
ctx.write_to_pty(&b"\x1b[200~"[..]);
ctx.write_to_pty(contents.replace("\x1b", "").into_bytes());
ctx.write_to_pty(&b"\x1b[201~"[..]);
} else {
// In non-bracketed (ie: normal) mode, terminal applications cannot distinguish
// pasted data from keystrokes.
// In theory, we should construct the keystrokes needed to produce the data we are
// pasting... since that's neither practical nor sensible (and probably an impossible
// task to solve in a general way), we'll just replace line breaks (windows and unix
// style) with a single carriage return (\r, which is what the Enter key produces).
ctx.write_to_pty(contents.replace("\r\n", "\r").replace("\n", "\r").into_bytes());
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum MouseState {
Url(Url),
MessageBar,
MessageBarButton,
Mouse,
Text,
}
impl From<MouseState> for CursorIcon {
fn from(mouse_state: MouseState) -> CursorIcon {
match mouse_state {
MouseState::Url(_) | MouseState::MessageBarButton => CursorIcon::Hand,
MouseState::Text => CursorIcon::Text,
_ => CursorIcon::Default,
}
}
}
impl<'a, T: EventListener, A: ActionContext<T> + 'a> Processor<'a, T, A> {
pub fn new(ctx: A, config: &'a mut Config) -> Self {
Self { ctx, config, _phantom: Default::default() }
}
fn mouse_state(&mut self, point: Point, mods: ModifiersState) -> MouseState {
let mouse_mode =
TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG | TermMode::MOUSE_REPORT_CLICK;
// Check message bar before URL to ignore URLs in the message bar
if let Some(message) = self.message_at_point(Some(point)) {
if self.message_close_at_point(point, message) {
return MouseState::MessageBarButton;
} else {
return MouseState::MessageBar;
}
}
// Check for URL at point with required modifiers held
if self.config.ui_config.mouse.url.mods().relaxed_eq(mods)
&& (!self.ctx.terminal().mode().intersects(mouse_mode) || mods.shift)
&& self.config.ui_config.mouse.url.launcher.is_some()
&& self.ctx.selection_is_empty()
&& self.ctx.mouse().left_button_state != ElementState::Pressed
{
let buffer_point = self.ctx.terminal().visible_to_buffer(point);
if let Some(url) =
self.ctx.terminal().urls().drain(..).find(|url| url.contains(buffer_point))
{
return MouseState::Url(url);
}
}
if self.ctx.terminal().mode().intersects(mouse_mode) && !self.ctx.modifiers().shift() {
MouseState::Mouse
} else {
MouseState::Text
}
}
#[inline]
pub fn mouse_moved(&mut self, x: usize, y: usize, modifiers: ModifiersState) {
self.ctx.mouse_mut().x = x;
self.ctx.mouse_mut().y = y;
let size_info = self.ctx.size_info();
let point = size_info.pixels_to_coords(x, y);
let cell_side = self.get_mouse_side();
let prev_side = mem::replace(&mut self.ctx.mouse_mut().cell_side, cell_side);
let prev_line = mem::replace(&mut self.ctx.mouse_mut().line, point.line);
let prev_col = mem::replace(&mut self.ctx.mouse_mut().column, point.col);
let motion_mode = TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG;
let report_mode = TermMode::MOUSE_REPORT_CLICK | motion_mode;
let cell_changed =
prev_line != self.ctx.mouse().line || prev_col != self.ctx.mouse().column;
// If the mouse hasn't changed cells, do nothing
if !cell_changed && prev_side == cell_side {
return;
}
// Don't launch URLs if mouse has moved
self.ctx.mouse_mut().block_url_launcher = true;
let mouse_state = self.mouse_state(point, modifiers);
self.update_mouse_cursor(mouse_state);
match mouse_state {
MouseState::Url(url) => {
let url_bounds = url.linear_bounds(self.ctx.terminal());
self.ctx.terminal_mut().set_url_highlight(url_bounds);
},
MouseState::MessageBar | MouseState::MessageBarButton => {
self.ctx.terminal_mut().reset_url_highlight();
return;
},
_ => self.ctx.terminal_mut().reset_url_highlight(),
}
if self.ctx.mouse().left_button_state == ElementState::Pressed
&& (modifiers.shift || !self.ctx.terminal().mode().intersects(report_mode))
{
2019-03-30 16:48:36 +00:00
self.ctx.update_selection(Point { line: point.line, col: point.col }, cell_side);
} else if self.ctx.terminal().mode().intersects(motion_mode)
&& size_info.contains_point(x, y, false)
&& cell_changed
{
if self.ctx.mouse().left_button_state == ElementState::Pressed {
self.mouse_report(32, ElementState::Pressed, modifiers);
} else if self.ctx.mouse().middle_button_state == ElementState::Pressed {
self.mouse_report(33, ElementState::Pressed, modifiers);
} else if self.ctx.mouse().right_button_state == ElementState::Pressed {
self.mouse_report(34, ElementState::Pressed, modifiers);
} else if self.ctx.terminal().mode().contains(TermMode::MOUSE_MOTION) {
self.mouse_report(35, ElementState::Pressed, modifiers);
}
}
}
fn get_mouse_side(&self) -> Side {
let size_info = self.ctx.size_info();
let x = self.ctx.mouse().x;
let cell_x = x.saturating_sub(size_info.padding_x as usize) % size_info.cell_width as usize;
let half_cell_width = (size_info.cell_width / 2.0) as usize;
let additional_padding =
(size_info.width - size_info.padding_x * 2.) % size_info.cell_width;
let end_of_grid = size_info.width - size_info.padding_x - additional_padding;
if cell_x > half_cell_width
// Edge case when mouse leaves the window
|| x as f32 >= end_of_grid
{
Side::Right
} else {
Side::Left
}
}
pub fn normal_mouse_report(&mut self, button: u8) {
let (line, column) = (self.ctx.mouse().line, self.ctx.mouse().column);
if line < Line(223) && column < Column(223) {
let msg = vec![
b'\x1b',
b'[',
b'M',
32 + button,
32 + 1 + column.0 as u8,
32 + 1 + line.0 as u8,
];
self.ctx.write_to_pty(msg);
}
}
pub fn sgr_mouse_report(&mut self, button: u8, state: ElementState) {
let (line, column) = (self.ctx.mouse().line, self.ctx.mouse().column);
let c = match state {
ElementState::Pressed => 'M',
ElementState::Released => 'm',
};
let msg = format!("\x1b[<{};{};{}{}", button, column + 1, line + 1, c);
self.ctx.write_to_pty(msg.into_bytes());
}
pub fn mouse_report(&mut self, button: u8, state: ElementState, modifiers: ModifiersState) {
// Calculate modifiers value
let mut mods = 0;
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(button + mods, state);
2018-01-26 22:37:48 +00:00
} else if let ElementState::Released = state {
self.normal_mouse_report(3 + mods);
} else {
self.normal_mouse_report(button + mods);
}
}
pub fn on_mouse_double_click(&mut self, button: MouseButton, point: Option<Point>) {
if let (Some(point), true) = (point, button == MouseButton::Left) {
self.ctx.semantic_selection(point);
}
}
pub fn on_mouse_triple_click(&mut self, button: MouseButton, point: Option<Point>) {
if let (Some(point), true) = (point, button == MouseButton::Left) {
self.ctx.line_selection(point);
}
}
pub fn on_mouse_press(
&mut self,
button: MouseButton,
modifiers: ModifiersState,
point: Option<Point>,
) {
let now = Instant::now();
let elapsed = self.ctx.mouse().last_click_timestamp.elapsed();
self.ctx.mouse_mut().last_click_timestamp = now;
let button_changed = self.ctx.mouse().last_button != button;
self.ctx.mouse_mut().click_state = match self.ctx.mouse().click_state {
ClickState::Click
if !button_changed
&& elapsed < self.config.ui_config.mouse.double_click.threshold =>
{
self.ctx.mouse_mut().block_url_launcher = true;
self.on_mouse_double_click(button, point);
ClickState::DoubleClick
2019-03-30 16:48:36 +00:00
}
ClickState::DoubleClick
if !button_changed
&& elapsed < self.config.ui_config.mouse.triple_click.threshold =>
{
self.ctx.mouse_mut().block_url_launcher = true;
self.on_mouse_triple_click(button, point);
ClickState::TripleClick
2019-03-30 16:48:36 +00:00
}
_ => {
// Don't launch URLs if this click cleared the selection
self.ctx.mouse_mut().block_url_launcher = !self.ctx.selection_is_empty();
self.ctx.clear_selection();
// Start new empty selection
let side = self.ctx.mouse().cell_side;
if let Some(point) = point {
if modifiers.ctrl {
self.ctx.block_selection(point, side);
} else {
self.ctx.simple_selection(point, side);
}
}
let report_modes =
TermMode::MOUSE_REPORT_CLICK | TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION;
if !modifiers.shift && self.ctx.terminal().mode().intersects(report_modes) {
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::Pressed, modifiers);
return;
}
ClickState::Click
2019-03-30 16:48:36 +00:00
},
};
}
pub fn on_mouse_release(
&mut self,
button: MouseButton,
modifiers: ModifiersState,
point: Option<Point>,
) {
let report_modes =
TermMode::MOUSE_REPORT_CLICK | TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION;
if !modifiers.shift && self.ctx.terminal().mode().intersects(report_modes) {
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, modifiers);
return;
} else if let (Some(point), true) = (point, button == MouseButton::Left) {
let mouse_state = self.mouse_state(point, modifiers);
self.update_mouse_cursor(mouse_state);
if let MouseState::Url(url) = mouse_state {
let url_bounds = url.linear_bounds(self.ctx.terminal());
self.ctx.terminal_mut().set_url_highlight(url_bounds);
self.launch_url(url);
}
}
self.copy_selection();
}
/// Spawn URL launcher when clicking on URLs.
fn launch_url(&self, url: Url) {
if self.ctx.mouse().block_url_launcher {
return;
}
if let Some(ref launcher) = self.config.ui_config.mouse.url.launcher {
let mut args = launcher.args().to_vec();
args.push(self.ctx.terminal().url_to_string(url));
match start_daemon(launcher.program(), &args) {
Ok(_) => debug!("Launched {} with args {:?}", launcher.program(), args),
Err(_) => warn!("Unable to launch {} with args {:?}", launcher.program(), args),
}
}
}
pub fn on_mouse_wheel(
&mut self,
delta: MouseScrollDelta,
phase: TouchPhase,
modifiers: ModifiersState,
) {
match delta {
MouseScrollDelta::LineDelta(_columns, lines) => {
let new_scroll_px = lines * self.ctx.size_info().cell_height;
self.scroll_terminal(modifiers, new_scroll_px as i32);
},
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 => {
// Reset offset to zero
self.ctx.mouse_mut().scroll_px = 0;
},
TouchPhase::Moved => {
self.scroll_terminal(modifiers, lpos.y as i32);
},
_ => (),
}
2019-03-30 16:48:36 +00:00
},
}
}
fn scroll_terminal(&mut self, modifiers: ModifiersState, new_scroll_px: i32) {
let mouse_modes =
TermMode::MOUSE_REPORT_CLICK | TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION;
let alt_scroll_modes = TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL;
let height = self.ctx.size_info().cell_height as i32;
2018-03-09 22:02:45 +00:00
if self.ctx.terminal().mode().intersects(mouse_modes) {
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();
for _ in 0..lines {
self.mouse_report(code, ElementState::Pressed, modifiers);
}
} else if self.ctx.terminal().mode().contains(alt_scroll_modes) && !modifiers.shift {
let multiplier = i32::from(
self.config
.scrolling
.faux_multiplier()
.unwrap_or_else(|| self.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();
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 = i32::from(self.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::Lines(lines as isize));
}
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,
2019-03-30 16:48:36 +00:00
modifiers: ModifiersState,
) {
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,
_ => (),
}
let point = self.ctx.mouse_coords();
// Skip normal mouse events if the message bar has been clicked
if let Some(message) = self.message_at_point(point) {
// Message should never be `Some` if point is `None`
debug_assert!(point.is_some());
self.on_message_bar_click(state, point.unwrap(), message, modifiers);
} else {
match state {
ElementState::Pressed => {
self.process_mouse_bindings(modifiers, button);
self.on_mouse_press(button, modifiers, point);
},
ElementState::Released => self.on_mouse_release(button, modifiers, point),
}
}
self.ctx.mouse_mut().last_button = button;
}
/// Process key input.
pub fn process_key(&mut self, input: KeyboardInput) {
self.ctx.modifiers().update(input);
// Update mouse cursor for temporarily disabling mouse mode
if input.virtual_keycode == Some(VirtualKeyCode::LShift)
|| input.virtual_keycode == Some(VirtualKeyCode::RShift)
{
if let Some(point) = self.ctx.mouse_coords() {
let mods = self.ctx.modifiers().into();
let mouse_state = self.mouse_state(point, mods);
self.update_mouse_cursor(mouse_state);
}
}
match input.state {
ElementState::Pressed => {
*self.ctx.received_count() = 0;
self.process_key_bindings(input);
},
ElementState::Released => *self.ctx.suppress_chars() = false,
}
}
/// Process a received character.
pub fn received_char(&mut self, c: char) {
2019-02-16 20:23:23 +00:00
if *self.ctx.suppress_chars() {
return;
}
2019-02-16 20:23:23 +00:00
self.ctx.scroll(Scroll::Bottom);
self.ctx.clear_selection();
2019-02-16 20:23:23 +00:00
let utf8_len = c.len_utf8();
let mut bytes = Vec::with_capacity(utf8_len);
unsafe {
bytes.set_len(utf8_len);
c.encode_utf8(&mut bytes[..]);
}
if self.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;
self.ctx.terminal_mut().dirty = 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_key_bindings(&mut self, input: KeyboardInput) {
let mut suppress_chars = None;
for binding in &self.config.ui_config.key_bindings {
let key = match (binding.trigger, input.virtual_keycode) {
(Key::Scancode(_), _) => Key::Scancode(input.scancode),
(_, Some(key)) => Key::from_glutin_input(key),
_ => continue,
};
if binding.is_triggered_by(*self.ctx.terminal().mode(), input.modifiers, &key, false) {
// Binding was triggered; run the action
binding.execute(&mut self.ctx, false);
// Don't suppress when there has been a `ReceiveChar` action
*suppress_chars.get_or_insert(true) &= binding.action != Action::ReceiveChar;
}
}
// 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, mods: ModifiersState, button: MouseButton) {
for binding in &self.config.ui_config.mouse_bindings {
if binding.is_triggered_by(*self.ctx.terminal().mode(), mods, &button, true) {
// binding was triggered; run the action
2019-03-30 16:48:36 +00:00
let mouse_mode = !mods.shift
&& self.ctx.terminal().mode().intersects(
TermMode::MOUSE_REPORT_CLICK
| TermMode::MOUSE_DRAG
| TermMode::MOUSE_MOTION,
);
binding.execute(&mut self.ctx, mouse_mode);
}
}
}
/// Return the message bar's message if there is some at the specified point
fn message_at_point(&mut self, point: Option<Point>) -> Option<Message> {
let size = &self.ctx.size_info();
if let (Some(point), Some(message)) = (point, self.ctx.message()) {
if point.line.0 >= size.lines().saturating_sub(message.text(size).len()) {
return Some(message.to_owned());
}
}
None
}
/// Whether the point is over the message bar's close button
fn message_close_at_point(&self, point: Point, message: Message) -> bool {
let size = self.ctx.size_info();
point.col + message_bar::CLOSE_BUTTON_TEXT.len() >= size.cols()
&& point.line == size.lines() - message.text(&size).len()
}
/// Handle clicks on the message bar.
fn on_message_bar_click(
&mut self,
button_state: ElementState,
point: Point,
message: Message,
mods: ModifiersState,
) {
match button_state {
ElementState::Released => self.copy_selection(),
ElementState::Pressed => {
if self.message_close_at_point(point, message) {
let mouse_state = self.mouse_state(point, mods);
self.update_mouse_cursor(mouse_state);
self.ctx.pop_message();
}
self.ctx.clear_selection();
2019-03-30 16:48:36 +00:00
},
}
}
/// Copy text selection.
fn copy_selection(&mut self) {
if self.config.selection.save_to_clipboard {
self.ctx.copy_selection(ClipboardType::Clipboard);
}
self.ctx.copy_selection(ClipboardType::Selection);
}
#[inline]
fn update_mouse_cursor(&mut self, mouse_state: MouseState) {
self.ctx.window_mut().set_mouse_cursor(mouse_state.into());
}
#[inline]
pub fn reset_mouse_cursor(&mut self) {
if let Some(point) = self.ctx.mouse_coords() {
let mods = self.ctx.modifiers().into();
let mouse_state = self.mouse_state(point, mods);
self.update_mouse_cursor(mouse_state);
}
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use std::time::Duration;
use glutin::event::{
ElementState, Event, ModifiersState, MouseButton, VirtualKeyCode, WindowEvent,
};
use alacritty_terminal::clipboard::{Clipboard, ClipboardType};
use alacritty_terminal::event::{Event as TerminalEvent, EventListener};
use alacritty_terminal::grid::Scroll;
use alacritty_terminal::index::{Point, Side};
use alacritty_terminal::message_bar::{Message, MessageBuffer};
use alacritty_terminal::selection::Selection;
use alacritty_terminal::term::{SizeInfo, Term, TermMode};
use crate::config::{ClickHandler, Config};
use crate::event::{ClickState, Mouse};
use crate::window::Window;
use super::{Action, Binding, Modifiers, Processor};
const KEY: VirtualKeyCode = VirtualKeyCode::Key0;
struct MockEventProxy;
impl EventListener for MockEventProxy {
fn send_event(&self, _event: TerminalEvent) {}
}
#[derive(PartialEq)]
enum MultiClick {
DoubleClick,
TripleClick,
None,
}
struct ActionContext<'a, T> {
pub terminal: &'a mut Term<T>,
2017-06-17 17:29:59 +00:00
pub selection: &'a mut Option<Selection>,
pub size_info: &'a SizeInfo,
pub mouse: &'a mut Mouse,
pub message_buffer: &'a mut MessageBuffer,
pub last_action: MultiClick,
pub received_count: usize,
pub suppress_chars: bool,
pub modifiers: Modifiers,
}
impl<'a, T: EventListener> super::ActionContext<T> for ActionContext<'a, T> {
fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&mut self, _val: B) {}
2019-03-30 16:48:36 +00:00
fn update_selection(&mut self, _point: Point, _side: Side) {}
2019-03-30 16:48:36 +00:00
fn simple_selection(&mut self, _point: Point, _side: Side) {}
2019-03-30 16:48:36 +00:00
fn block_selection(&mut self, _point: Point, _side: Side) {}
fn copy_selection(&mut self, _: ClipboardType) {}
2019-03-30 16:48:36 +00:00
fn clear_selection(&mut self) {}
2019-03-30 16:48:36 +00:00
fn spawn_new_instance(&mut self) {}
fn change_font_size(&mut self, _delta: f32) {}
fn reset_font_size(&mut self) {}
fn terminal(&self) -> &Term<T> {
&self.terminal
}
fn terminal_mut(&mut self) -> &mut Term<T> {
&mut self.terminal
}
fn size_info(&self) -> SizeInfo {
*self.size_info
}
fn semantic_selection(&mut self, _point: Point) {
// set something that we can check for here
self.last_action = MultiClick::DoubleClick;
}
fn line_selection(&mut self, _point: Point) {
self.last_action = MultiClick::TripleClick;
}
fn selection_is_empty(&self) -> bool {
true
}
fn scroll(&mut self, scroll: Scroll) {
self.terminal.scroll_display(scroll);
}
fn mouse_coords(&self) -> Option<Point> {
let x = self.mouse.x as usize;
let y = self.mouse.y as usize;
if self.size_info.contains_point(x, y, true) {
Some(self.size_info.pixels_to_coords(x, y))
} else {
None
}
}
#[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 Modifiers {
&mut self.modifiers
}
fn window(&self) -> &Window {
unimplemented!();
}
fn window_mut(&mut self) -> &mut Window {
unimplemented!();
}
fn pop_message(&mut self) {
self.message_buffer.pop();
}
fn message(&self) -> Option<&Message> {
self.message_buffer.message()
}
}
macro_rules! test_clickstate {
{
name: $name:ident,
initial_state: $initial_state:expr,
initial_button: $initial_button:expr,
input: $input:expr,
end_state: $end_state:pat,
last_action: $last_action:expr
} => {
#[test]
fn $name() {
let mut cfg = Config::default();
cfg.ui_config.mouse = crate::config::Mouse {
double_click: ClickHandler {
threshold: Duration::from_millis(1000),
},
triple_click: ClickHandler {
threshold: Duration::from_millis(1000),
},
hide_when_typing: false,
url: Default::default(),
};
let size = SizeInfo {
width: 21.0,
height: 51.0,
cell_width: 3.0,
cell_height: 3.0,
padding_x: 0.0,
padding_y: 0.0,
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
dpr: 1.0,
};
let mut terminal = Term::new(&cfg, &size, Clipboard::new_nop(), MockEventProxy);
let mut mouse = Mouse::default();
mouse.click_state = $initial_state;
mouse.last_button = $initial_button;
2017-06-17 17:29:59 +00:00
let mut selection = None;
let mut message_buffer = MessageBuffer::new();
let context = ActionContext {
terminal: &mut terminal,
2017-06-17 17:29:59 +00:00
selection: &mut selection,
mouse: &mut mouse,
size_info: &size,
last_action: MultiClick::None,
received_count: 0,
suppress_chars: false,
modifiers: Modifiers::default(),
message_buffer: &mut message_buffer,
};
let mut processor = Processor::new(context, &mut cfg);
if let Event::WindowEvent { event: WindowEvent::MouseInput { state, button, modifiers, .. }, .. } = $input {
processor.mouse_input(state, button, modifiers);
};
assert!(match processor.ctx.mouse.click_state {
$end_state => processor.ctx.last_action == $last_action,
_ => false
});
}
}
}
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, false));
} else {
assert!(!$binding.is_triggered_by($mode, $mods, &KEY, false));
}
}
}
}
test_clickstate! {
name: single_click,
initial_state: ClickState::None,
initial_button: MouseButton::Other(0),
input: Event::<TerminalEvent>::WindowEvent {
event: WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Left,
device_id: unsafe { ::std::mem::transmute_copy(&0) },
modifiers: ModifiersState::default(),
},
window_id: unsafe { ::std::mem::transmute_copy(&0) },
},
end_state: ClickState::Click,
last_action: MultiClick::None
}
test_clickstate! {
name: double_click,
initial_state: ClickState::Click,
initial_button: MouseButton::Left,
input: Event::<TerminalEvent>::WindowEvent {
event: WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Left,
device_id: unsafe { ::std::mem::transmute_copy(&0) },
modifiers: ModifiersState::default(),
},
window_id: unsafe { ::std::mem::transmute_copy(&0) },
},
end_state: ClickState::DoubleClick,
last_action: MultiClick::DoubleClick
}
test_clickstate! {
name: triple_click,
initial_state: ClickState::DoubleClick,
initial_button: MouseButton::Left,
input: Event::<TerminalEvent>::WindowEvent {
event: WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Left,
device_id: unsafe { ::std::mem::transmute_copy(&0) },
modifiers: ModifiersState::default(),
},
window_id: unsafe { ::std::mem::transmute_copy(&0) },
},
end_state: ClickState::TripleClick,
last_action: MultiClick::TripleClick
}
test_clickstate! {
name: multi_click_separate_buttons,
initial_state: ClickState::DoubleClick,
initial_button: MouseButton::Left,
input: Event::<TerminalEvent>::WindowEvent {
event: WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Right,
device_id: unsafe { ::std::mem::transmute_copy(&0) },
modifiers: ModifiersState::default(),
},
window_id: unsafe { ::std::mem::transmute_copy(&0) },
},
end_state: ClickState::Click,
last_action: MultiClick::None
}
test_process_binding! {
name: process_binding_nomode_shiftmod_require_shift,
binding: Binding { trigger: KEY, mods: ModifiersState { shift: true, ctrl: false, alt: false, logo: false }, action: Action::from("\x1b[1;2D"), mode: TermMode::NONE, notmode: TermMode::NONE },
triggers: true,
mode: TermMode::NONE,
mods: ModifiersState { shift: true, ctrl: false, alt: false, logo: false }
}
test_process_binding! {
name: process_binding_nomode_nomod_require_shift,
binding: Binding { trigger: KEY, mods: ModifiersState { shift: true, ctrl: false, alt: false, logo: false }, action: Action::from("\x1b[1;2D"), mode: TermMode::NONE, notmode: TermMode::NONE },
triggers: false,
mode: TermMode::NONE,
mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }
}
test_process_binding! {
name: process_binding_nomode_controlmod,
binding: Binding { trigger: KEY, mods: ModifiersState { ctrl: true, shift: false, alt: false, logo: false }, action: Action::from("\x1b[1;5D"), mode: TermMode::NONE, notmode: TermMode::NONE },
triggers: true,
mode: TermMode::NONE,
mods: ModifiersState { ctrl: true, shift: false, alt: false, logo: false }
}
test_process_binding! {
name: process_binding_nomode_nomod_require_not_appcursor,
binding: Binding { trigger: KEY, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }, action: Action::from("\x1b[D"), mode: TermMode::NONE, notmode: TermMode::APP_CURSOR },
triggers: true,
mode: TermMode::NONE,
mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }
}
test_process_binding! {
name: process_binding_appcursormode_nomod_require_appcursor,
binding: Binding { trigger: KEY, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }, action: Action::from("\x1bOD"), mode: TermMode::APP_CURSOR, notmode: TermMode::NONE },
triggers: true,
mode: TermMode::APP_CURSOR,
mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }
}
test_process_binding! {
name: process_binding_nomode_nomod_require_appcursor,
binding: Binding { trigger: KEY, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }, action: Action::from("\x1bOD"), mode: TermMode::APP_CURSOR, notmode: TermMode::NONE },
triggers: false,
mode: TermMode::NONE,
mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }
}
test_process_binding! {
name: process_binding_appcursormode_appkeypadmode_nomod_require_appcursor,
binding: Binding { trigger: KEY, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }, action: Action::from("\x1bOD"), mode: TermMode::APP_CURSOR, notmode: TermMode::NONE },
triggers: true,
mode: TermMode::APP_CURSOR | TermMode::APP_KEYPAD,
mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }
}
test_process_binding! {
name: process_binding_fail_with_extra_mods,
binding: Binding { trigger: KEY, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: true }, action: Action::from("arst"), mode: TermMode::NONE, notmode: TermMode::NONE },
triggers: false,
mode: TermMode::NONE,
mods: ModifiersState { shift: false, ctrl: false, alt: true, logo: true }
}
}