Merge pull request #1522 from zed-industries/terminal-mouse

Terminal mouse mode
This commit is contained in:
Mikayla Maki 2022-08-19 15:19:27 -07:00 committed by GitHub
commit 6122bc863d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 825 additions and 303 deletions

View File

@ -102,27 +102,37 @@
//
//
"working_directory": "current_project_directory",
//Set the cursor blinking behavior in the terminal.
//May take 4 values:
// 1. Never blink the cursor, ignoring the terminal mode
// "blinking": "off",
// 2. Default the cursor blink to off, but allow the terminal to
// set blinking
// "blinking": "terminal_controlled",
// 3. Always blink the cursor, ignoring the terminal mode
// "blinking": "on",
// Set the cursor blinking behavior in the terminal.
// May take 4 values:
// 1. Never blink the cursor, ignoring the terminal mode
// "blinking": "off",
// 2. Default the cursor blink to off, but allow the terminal to
// set blinking
// "blinking": "terminal_controlled",
// 3. Always blink the cursor, ignoring the terminal mode
// "blinking": "on",
"blinking": "terminal_controlled",
//Any key-value pairs added to this list will be added to the terminal's
//enviroment. Use `:` to seperate multiple values.
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
// Alternate Scroll mode converts mouse scroll events into up / down key
// presses when in the alternate screen (e.g. when running applications
// like vim or less). The terminal can still set and unset this mode.
// May take 2 values:
// 1. Default alternate scroll mode to on
// "alternate_scroll": "on",
// 2. Default alternate scroll mode to off
// "alternate_scroll": "off",
"alternate_scroll": "off",
// Any key-value pairs added to this list will be added to the terminal's
// enviroment. Use `:` to seperate multiple values.
"env": {
//"KEY": "value1:value2"
// "KEY": "value1:value2"
}
//Set the terminal's font size. If this option is not included,
//the terminal will default to matching the buffer's font size.
//"font_size": "15"
//Set the terminal's font family. If this option is not included,
//the terminal will default to matching the buffer's font family.
//"font_family": "Zed Mono"
// Set the terminal's font size. If this option is not included,
// the terminal will default to matching the buffer's font size.
// "font_size": "15"
// Set the terminal's font family. If this option is not included,
// the terminal will default to matching the buffer's font family.
// "font_family": "Zed Mono"
},
// Different settings for specific languages.
"languages": {
@ -155,15 +165,15 @@
"tab_size": 2
}
},
//LSP Specific settings.
// LSP Specific settings.
"lsp": {
//Specify the LSP name as a key here.
//As of 8/10/22, supported LSPs are:
//pyright
//gopls
//rust-analyzer
//typescript-language-server
//vscode-json-languageserver
// Specify the LSP name as a key here.
// As of 8/10/22, supported LSPs are:
// pyright
// gopls
// rust-analyzer
// typescript-language-server
// vscode-json-languageserver
// "rust_analyzer": {
// //These initialization options are merged into Zed's defaults
// "initialization_options": {

View File

@ -1610,6 +1610,7 @@ impl Element for EditorElement {
position,
delta,
precise,
..
}) => self.scroll(*position, *delta, *precise, layout, paint, cx),
&Event::ModifiersChanged(event) => self.modifiers_changed(event, cx),

View File

@ -293,6 +293,7 @@ impl Element for Flex {
position,
delta,
precise,
..
}) = event
{
if *remaining_space < 0. && bounds.contains_point(position) {

View File

@ -316,6 +316,7 @@ impl Element for List {
position,
delta,
precise,
..
}) = event
{
if bounds.contains_point(*position)

View File

@ -315,6 +315,7 @@ impl Element for UniformList {
position,
delta,
precise,
..
}) = event
{
if bounds.contains_point(*position)

View File

@ -24,6 +24,10 @@ pub struct ScrollWheelEvent {
pub position: Vector2F,
pub delta: Vector2F,
pub precise: bool,
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
pub cmd: bool,
}
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]

View File

@ -148,6 +148,8 @@ impl Event {
})
}
NSEventType::NSScrollWheel => window_height.map(|window_height| {
let modifiers = native_event.modifierFlags();
Self::ScrollWheel(ScrollWheelEvent {
position: vec2f(
native_event.locationInWindow().x as f32,
@ -158,6 +160,10 @@ impl Event {
native_event.scrollingDeltaY() as f32,
),
precise: native_event.hasPreciseScrollingDeltas() == YES,
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
})
}),
NSEventType::NSLeftMouseDragged

View File

@ -235,7 +235,9 @@ impl Presenter {
if let Some(root_view_id) = cx.root_view_id(self.window_id) {
let mut invalidated_views = Vec::new();
let mut mouse_down_out_handlers = Vec::new();
let mut mouse_moved_region = None;
let mut mouse_down_region = None;
let mut mouse_up_region = None;
let mut clicked_region = None;
let mut dragged_region = None;
@ -282,6 +284,15 @@ impl Presenter {
}
}
for (region, _) in self.mouse_regions.iter().rev() {
if region.bounds.contains_point(position) {
invalidated_views.push(region.view_id);
mouse_up_region =
Some((region.clone(), MouseRegionEvent::Up(e.clone())));
break;
}
}
if let Some(moved) = &mut self.last_mouse_moved_event {
if moved.pressed_button == Some(button) {
moved.pressed_button = None;
@ -302,6 +313,15 @@ impl Presenter {
*prev_drag_position = *position;
}
for (region, _) in self.mouse_regions.iter().rev() {
if region.bounds.contains_point(*position) {
invalidated_views.push(region.view_id);
mouse_moved_region =
Some((region.clone(), MouseRegionEvent::Move(e.clone())));
break;
}
}
self.last_mouse_moved_event = Some(e.clone());
}
@ -329,6 +349,28 @@ impl Presenter {
}
}
if let Some((move_moved_region, region_event)) = mouse_moved_region {
handled = true;
if let Some(mouse_moved_callback) =
move_moved_region.handlers.get(&region_event.handler_key())
{
event_cx.with_current_view(move_moved_region.view_id, |event_cx| {
mouse_moved_callback(region_event, event_cx);
})
}
}
if let Some((mouse_up_region, region_event)) = mouse_up_region {
handled = true;
if let Some(mouse_up_callback) =
mouse_up_region.handlers.get(&region_event.handler_key())
{
event_cx.with_current_view(mouse_up_region.view_id, |event_cx| {
mouse_up_callback(region_event, event_cx);
})
}
}
if let Some((clicked_region, region_event)) = clicked_region {
handled = true;
if let Some(click_callback) =

View File

@ -1,6 +1,7 @@
use std::{any::TypeId, mem::Discriminant, rc::Rc};
use collections::HashMap;
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
use crate::{EventContext, MouseButton, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
@ -97,6 +98,14 @@ impl MouseRegion {
self.handlers = self.handlers.on_hover(handler);
self
}
pub fn on_move(
mut self,
handler: impl Fn(MouseMovedEvent, &mut EventContext) + 'static,
) -> Self {
self.handlers = self.handlers.on_move(handler);
self
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
@ -267,6 +276,23 @@ impl HandlerSet {
}));
self
}
pub fn on_move(
mut self,
handler: impl Fn(MouseMovedEvent, &mut EventContext) + 'static,
) -> Self {
self.set.insert((MouseRegionEvent::move_disc(), None),
Rc::new(move |region_event, cx| {
if let MouseRegionEvent::Move(move_event)= region_event {
handler(move_event, cx);
} else {
panic!(
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Move, found {:?}",
region_event);
}
}));
self
}
}
#[derive(Debug)]

View File

@ -84,6 +84,7 @@ pub struct TerminalSettings {
pub font_family: Option<String>,
pub env: Option<HashMap<String, String>>,
pub blinking: Option<TerminalBlink>,
pub alternate_scroll: Option<AlternateScroll>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
@ -114,6 +115,19 @@ impl Default for Shell {
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AlternateScroll {
On,
Off,
}
impl Default for AlternateScroll {
fn default() -> Self {
AlternateScroll::On
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WorkingDirectory {

View File

@ -10,7 +10,7 @@ The TerminalView struct abstracts over failed and successful terminals, passing
#Input
There are currently 3 distinct paths for getting keystrokes to the terminal:
There are currently many distinct paths for getting keystrokes to the terminal:
1. Terminal specific characters and bindings. Things like ctrl-a mapping to ASCII control character 1, ANSI escape codes associated with the function keys, etc. These are caught with a raw key-down handler in the element and are processed immediately. This is done with the `try_keystroke()` method on Terminal
@ -18,3 +18,6 @@ There are currently 3 distinct paths for getting keystrokes to the terminal:
3. IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the `View::replace_text_in_range()` method, and we then send that to the terminal directly, bypassing `try_keystroke()`.
4. Pasted text has a seperate pathway.
Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal

View File

@ -1,23 +1,26 @@
use alacritty_terminal::{
ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor},
grid::{Dimensions, Scroll},
index::{Column as GridCol, Line as GridLine, Point, Side},
grid::Dimensions,
index::Point,
selection::SelectionRange,
term::cell::{Cell, Flags},
term::{
cell::{Cell, Flags},
TermMode,
},
};
use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
use gpui::{
color::Color,
elements::*,
fonts::{Properties, Style::Italic, TextStyle, Underline, Weight},
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::json,
serde_json::json,
text_layout::{Line, RunStyle},
Event, FontCache, KeyDownEvent, MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion,
PaintContext, Quad, ScrollWheelEvent, TextLayoutCache, WeakModelHandle, WeakViewHandle,
Element, Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton,
MouseButtonEvent, MouseRegion, PaintContext, Quad, TextLayoutCache, WeakModelHandle,
WeakViewHandle,
};
use itertools::Itertools;
use ordered_float::OrderedFloat;
@ -25,12 +28,11 @@ use settings::Settings;
use theme::TerminalStyle;
use util::ResultExt;
use std::fmt::Debug;
use std::{
cmp::min,
mem,
ops::{Deref, Range},
};
use std::{fmt::Debug, ops::Sub};
use crate::{
connected_view::{ConnectedView, DeployContextMenu},
@ -38,11 +40,6 @@ use crate::{
Terminal, TerminalSize,
};
///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
///Scroll multiplier that is set to 3 by default. This will be removed when I
///Implement scroll bars.
pub const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
///The information generated during layout that is nescessary for painting
pub struct LayoutState {
cells: Vec<LayoutCell>,
@ -52,7 +49,7 @@ pub struct LayoutState {
background_color: Color,
selection_color: Color,
size: TerminalSize,
display_offset: usize,
mode: TermMode,
}
#[derive(Debug)]
@ -413,90 +410,159 @@ impl TerminalEl {
}
}
fn generic_button_handler(
connection: WeakModelHandle<Terminal>,
origin: Vector2F,
f: impl Fn(&mut Terminal, Vector2F, MouseButtonEvent, &mut ModelContext<Terminal>),
) -> impl Fn(MouseButtonEvent, &mut EventContext) {
move |event, cx| {
cx.focus_parent_view();
if let Some(conn_handle) = connection.upgrade(cx.app) {
conn_handle.update(cx.app, |terminal, cx| {
f(terminal, origin, event, cx);
cx.notify();
})
}
}
}
fn attach_mouse_handlers(
&self,
origin: Vector2F,
view_id: usize,
visible_bounds: RectF,
cur_size: TerminalSize,
display_offset: usize,
mode: TermMode,
cx: &mut PaintContext,
) {
let mouse_down_connection = self.terminal;
let click_connection = self.terminal;
let drag_connection = self.terminal;
cx.scene.push_mouse_region(
MouseRegion::new(view_id, None, visible_bounds)
.on_down(
MouseButton::Left,
move |MouseButtonEvent { position, .. }, cx| {
if let Some(conn_handle) = mouse_down_connection.upgrade(cx.app) {
conn_handle.update(cx.app, |terminal, cx| {
let (point, side) = TerminalEl::mouse_to_cell_data(
position,
origin,
cur_size,
display_offset,
);
let connection = self.terminal;
terminal.mouse_down(point, side);
let mut region = MouseRegion::new(view_id, None, visible_bounds);
cx.notify();
})
}
},
)
.on_click(
MouseButton::Left,
move |MouseButtonEvent {
position,
click_count,
..
},
cx| {
cx.focus_parent_view();
if let Some(conn_handle) = click_connection.upgrade(cx.app) {
conn_handle.update(cx.app, |terminal, cx| {
let (point, side) = TerminalEl::mouse_to_cell_data(
position,
origin,
cur_size,
display_offset,
);
terminal.click(point, side, click_count);
cx.notify();
});
}
},
)
.on_click(
MouseButton::Right,
move |MouseButtonEvent { position, .. }, cx| {
cx.dispatch_action(DeployContextMenu { position });
},
)
.on_drag(
MouseButton::Left,
move |_, MouseMovedEvent { position, .. }, cx| {
if let Some(conn_handle) = drag_connection.upgrade(cx.app) {
conn_handle.update(cx.app, |terminal, cx| {
let (point, side) = TerminalEl::mouse_to_cell_data(
position,
origin,
cur_size,
display_offset,
);
terminal.drag(point, side);
cx.notify()
});
}
//Terminal Emulator controlled behavior:
region = region
//Start selections
.on_down(
MouseButton::Left,
TerminalEl::generic_button_handler(
connection,
origin,
move |terminal, origin, e, _cx| {
terminal.mouse_down(&e, origin);
},
),
);
)
//Update drag selections
.on_drag(MouseButton::Left, move |_prev, event, cx| {
if cx.is_parent_view_focused() {
if let Some(conn_handle) = connection.upgrade(cx.app) {
conn_handle.update(cx.app, |terminal, cx| {
terminal.mouse_drag(event, origin);
cx.notify();
})
}
}
})
//Copy on up behavior
.on_up(
MouseButton::Left,
TerminalEl::generic_button_handler(
connection,
origin,
move |terminal, origin, e, _cx| {
terminal.mouse_up(&e, origin);
},
),
)
//Handle click based selections
.on_click(
MouseButton::Left,
TerminalEl::generic_button_handler(
connection,
origin,
move |terminal, origin, e, _cx| {
terminal.left_click(&e, origin);
},
),
)
//Context menu
.on_click(
MouseButton::Right,
move |e @ MouseButtonEvent { position, .. }, cx| {
let mouse_mode = if let Some(conn_handle) = connection.upgrade(cx.app) {
conn_handle.update(cx.app, |terminal, _cx| terminal.mouse_mode(e.shift))
} else {
//If we can't get the model handle, probably can't deploy the context menu
true
};
if !mouse_mode {
cx.dispatch_action(DeployContextMenu { position });
}
},
)
//This handles both drag mode and mouse motion mode
//Mouse Move TODO
//This cannot be done conditionally for unknown reasons. Pending drag and drop rework.
//This also does not fire on right-mouse-down-move events wild.
.on_move(move |event, cx| {
dbg!(event);
if cx.is_parent_view_focused() {
if let Some(conn_handle) = connection.upgrade(cx.app) {
conn_handle.update(cx.app, |terminal, cx| {
terminal.mouse_move(&event, origin);
cx.notify();
})
}
}
});
if mode.contains(TermMode::MOUSE_MODE) {
region = region
.on_down(
MouseButton::Right,
TerminalEl::generic_button_handler(
connection,
origin,
move |terminal, origin, e, _cx| {
terminal.mouse_down(&e, origin);
},
),
)
.on_down(
MouseButton::Middle,
TerminalEl::generic_button_handler(
connection,
origin,
move |terminal, origin, e, _cx| {
terminal.mouse_down(&e, origin);
},
),
)
.on_up(
MouseButton::Right,
TerminalEl::generic_button_handler(
connection,
origin,
move |terminal, origin, e, _cx| {
terminal.mouse_up(&e, origin);
},
),
)
.on_up(
MouseButton::Middle,
TerminalEl::generic_button_handler(
connection,
origin,
move |terminal, origin, e, _cx| {
terminal.mouse_up(&e, origin);
},
),
)
}
//TODO: Mouse drag isn't correct
//TODO: Nor is mouse motion. Move events aren't happening??
cx.scene.push_mouse_region(region);
}
///Configures a text style from the current settings.
@ -530,47 +596,6 @@ impl TerminalEl {
underline: Default::default(),
}
}
pub fn mouse_to_cell_data(
pos: Vector2F,
origin: Vector2F,
cur_size: TerminalSize,
display_offset: usize,
) -> (Point, alacritty_terminal::index::Direction) {
let pos = pos.sub(origin);
let point = {
let col = pos.x() / cur_size.cell_width; //TODO: underflow...
let col = min(GridCol(col as usize), cur_size.last_column());
let line = pos.y() / cur_size.line_height;
let line = min(line as i32, cur_size.bottommost_line().0);
Point::new(GridLine(line - display_offset as i32), col)
};
//Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side()
let side = {
let x = pos.0.x() as usize;
let cell_x =
x.saturating_sub(cur_size.cell_width as usize) % cur_size.cell_width as usize;
let half_cell_width = (cur_size.cell_width / 2.0) as usize;
let additional_padding =
(cur_size.width() - cur_size.cell_width * 2.) % cur_size.cell_width;
let end_of_grid = cur_size.width() - cur_size.cell_width - additional_padding;
//Width: Pixels or columns?
if cell_x > half_cell_width
// Edge case when mouse leaves the window.
|| x as f32 >= end_of_grid
{
Side::Right
} else {
Side::Left
}
};
(point, side)
}
}
impl Element for TerminalEl {
@ -601,7 +626,7 @@ impl Element for TerminalEl {
terminal_theme.colors.background
};
let (cells, selection, cursor, display_offset, cursor_text) = self
let (cells, selection, cursor, display_offset, cursor_text, mode) = self
.terminal
.upgrade(cx)
.unwrap()
@ -624,13 +649,13 @@ impl Element for TerminalEl {
cell: ic.cell.clone(),
}),
);
(
cells,
content.selection,
content.cursor,
content.display_offset,
cursor_text,
content.mode,
)
})
});
@ -709,7 +734,7 @@ impl Element for TerminalEl {
size: dimensions,
rects,
highlights,
display_offset,
mode,
},
)
}
@ -728,14 +753,7 @@ impl Element for TerminalEl {
let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
//Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
self.attach_mouse_handlers(
origin,
self.view.id(),
visible_bounds,
layout.size,
layout.display_offset,
cx,
);
self.attach_mouse_handlers(origin, self.view.id(), visible_bounds, layout.mode, cx);
cx.paint_layer(clip_bounds, |cx| {
//Start with a background color
@ -799,28 +817,22 @@ impl Element for TerminalEl {
fn dispatch_event(
&mut self,
event: &gpui::Event,
_bounds: gpui::geometry::rect::RectF,
bounds: gpui::geometry::rect::RectF,
visible_bounds: gpui::geometry::rect::RectF,
layout: &mut Self::LayoutState,
_paint: &mut Self::PaintState,
cx: &mut gpui::EventContext,
) -> bool {
match event {
Event::ScrollWheel(ScrollWheelEvent {
delta, position, ..
}) => visible_bounds
.contains_point(*position)
Event::ScrollWheel(e) => visible_bounds
.contains_point(e.position)
.then(|| {
let vertical_scroll =
(delta.y() / layout.size.line_height) * ALACRITTY_SCROLL_MULTIPLIER;
let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
if let Some(terminal) = self.terminal.upgrade(cx.app) {
terminal.update(cx.app, |term, _| {
term.scroll(Scroll::Delta(vertical_scroll.round() as i32))
});
terminal.update(cx.app, |term, _| term.scroll(e, origin));
cx.notify();
}
cx.notify();
})
.is_some(),
Event::KeyDown(KeyDownEvent { keystroke, .. }) => {
@ -828,7 +840,6 @@ impl Element for TerminalEl {
return false;
}
//TODO Talk to keith about how to catch events emitted from an element.
if let Some(view) = self.view.upgrade(cx.app) {
view.update(cx.app, |view, cx| {
view.clear_bel(cx);
@ -884,36 +895,3 @@ impl Element for TerminalEl {
Some(layout.cursor.as_ref()?.bounding_rect(origin))
}
}
mod test {
#[test]
fn test_mouse_to_selection() {
let term_width = 100.;
let term_height = 200.;
let cell_width = 10.;
let line_height = 20.;
let mouse_pos_x = 100.; //Window relative
let mouse_pos_y = 100.; //Window relative
let origin_x = 10.;
let origin_y = 20.;
let cur_size = crate::connected_el::TerminalSize::new(
line_height,
cell_width,
gpui::geometry::vector::vec2f(term_width, term_height),
);
let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
let (point, _) =
crate::connected_el::TerminalEl::mouse_to_cell_data(mouse_pos, origin, cur_size, 0);
assert_eq!(
point,
alacritty_terminal::index::Point::new(
alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
)
);
}
}

View File

@ -251,7 +251,8 @@ impl ConnectedView {
///Attempt to paste the clipboard into the terminal
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
if let Some(item) = cx.read_from_clipboard() {
self.terminal.read(cx).paste(item.text());
self.terminal
.update(cx, |terminal, _cx| terminal.paste(item.text()));
}
}
@ -359,8 +360,7 @@ impl View for ConnectedView {
cx: &mut ViewContext<Self>,
) {
self.terminal.update(cx, |terminal, _| {
terminal.write_to_pty(text.into());
terminal.scroll(alacritty_terminal::grid::Scroll::Bottom);
terminal.input(text.into());
});
}

View File

@ -1,3 +1,4 @@
/// The mappings defined in this file where created from reading the alacritty source
use alacritty_terminal::term::TermMode;
use gpui::keymap::Keystroke;

View File

@ -1,2 +1,3 @@
pub mod colors;
pub mod keys;
pub mod mouse;

View File

@ -0,0 +1,330 @@
use std::cmp::{max, min};
use std::iter::repeat;
use alacritty_terminal::grid::Dimensions;
/// Most of the code, and specifically the constants, in this are copied from Alacritty,
/// with modifications for our circumstances
use alacritty_terminal::index::{Column as GridCol, Line as GridLine, Point, Side};
use alacritty_terminal::term::TermMode;
use gpui::{geometry::vector::Vector2F, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
use crate::TerminalSize;
struct Modifiers {
ctrl: bool,
shift: bool,
alt: bool,
}
impl Modifiers {
fn from_moved(e: &MouseMovedEvent) -> Self {
Modifiers {
ctrl: e.ctrl,
shift: e.shift,
alt: e.alt,
}
}
fn from_button(e: &MouseButtonEvent) -> Self {
Modifiers {
ctrl: e.ctrl,
shift: e.shift,
alt: e.alt,
}
}
//TODO: Determine if I should add modifiers into the ScrollWheelEvent type
fn from_scroll() -> Self {
Modifiers {
ctrl: false,
shift: false,
alt: false,
}
}
}
enum MouseFormat {
SGR,
Normal(bool),
}
impl MouseFormat {
fn from_mode(mode: TermMode) -> Self {
if mode.contains(TermMode::SGR_MOUSE) {
MouseFormat::SGR
} else if mode.contains(TermMode::UTF8_MOUSE) {
MouseFormat::Normal(true)
} else {
MouseFormat::Normal(false)
}
}
}
#[derive(Debug)]
enum MouseButton {
LeftButton = 0,
MiddleButton = 1,
RightButton = 2,
LeftMove = 32,
MiddleMove = 33,
RightMove = 34,
NoneMove = 35,
ScrollUp = 64,
ScrollDown = 65,
Other = 99,
}
impl MouseButton {
fn from_move(e: &MouseMovedEvent) -> Self {
match e.pressed_button {
Some(b) => match b {
gpui::MouseButton::Left => MouseButton::LeftMove,
gpui::MouseButton::Middle => MouseButton::MiddleMove,
gpui::MouseButton::Right => MouseButton::RightMove,
gpui::MouseButton::Navigate(_) => MouseButton::Other,
},
None => MouseButton::NoneMove,
}
}
fn from_button(e: &MouseButtonEvent) -> Self {
match e.button {
gpui::MouseButton::Left => MouseButton::LeftButton,
gpui::MouseButton::Right => MouseButton::MiddleButton,
gpui::MouseButton::Middle => MouseButton::RightButton,
gpui::MouseButton::Navigate(_) => MouseButton::Other,
}
}
fn from_scroll(e: &ScrollWheelEvent) -> Self {
if e.delta.y() > 0. {
MouseButton::ScrollUp
} else {
MouseButton::ScrollDown
}
}
fn is_other(&self) -> bool {
match self {
MouseButton::Other => true,
_ => false,
}
}
}
pub fn scroll_report(
point: Point,
scroll_lines: i32,
e: &ScrollWheelEvent,
mode: TermMode,
) -> Option<impl Iterator<Item = Vec<u8>>> {
if mode.intersects(TermMode::MOUSE_MODE) {
mouse_report(
point,
MouseButton::from_scroll(e),
true,
Modifiers::from_scroll(),
MouseFormat::from_mode(mode),
)
.map(|report| repeat(report).take(max(scroll_lines, 1) as usize))
} else {
None
}
}
pub fn alt_scroll(scroll_lines: i32) -> Vec<u8> {
let cmd = if scroll_lines > 0 { b'A' } else { b'B' };
let mut content = Vec::with_capacity(scroll_lines.abs() as usize * 3);
for _ in 0..scroll_lines.abs() {
content.push(0x1b);
content.push(b'O');
content.push(cmd);
}
content
}
pub fn mouse_button_report(
point: Point,
e: &MouseButtonEvent,
pressed: bool,
mode: TermMode,
) -> Option<Vec<u8>> {
let button = MouseButton::from_button(e);
if !button.is_other() && mode.intersects(TermMode::MOUSE_MODE) {
mouse_report(
point,
button,
pressed,
Modifiers::from_button(e),
MouseFormat::from_mode(mode),
)
} else {
None
}
}
pub fn mouse_moved_report(point: Point, e: &MouseMovedEvent, mode: TermMode) -> Option<Vec<u8>> {
let button = MouseButton::from_move(e);
dbg!(&button);
if !button.is_other() && mode.intersects(TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG) {
//Only drags are reported in drag mode, so block NoneMove.
if mode.contains(TermMode::MOUSE_DRAG) && matches!(button, MouseButton::NoneMove) {
None
} else {
mouse_report(
point,
button,
true,
Modifiers::from_moved(e),
MouseFormat::from_mode(mode),
)
}
} else {
None
}
}
pub fn mouse_side(pos: Vector2F, cur_size: TerminalSize) -> alacritty_terminal::index::Direction {
let x = pos.0.x() as usize;
let cell_x = x.saturating_sub(cur_size.cell_width as usize) % cur_size.cell_width as usize;
let half_cell_width = (cur_size.cell_width / 2.0) as usize;
let additional_padding = (cur_size.width() - cur_size.cell_width * 2.) % cur_size.cell_width;
let end_of_grid = cur_size.width() - cur_size.cell_width - additional_padding;
//Width: Pixels or columns?
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 mouse_point(pos: Vector2F, cur_size: TerminalSize, display_offset: usize) -> Point {
let col = pos.x() / cur_size.cell_width;
let col = min(GridCol(col as usize), cur_size.last_column());
let line = pos.y() / cur_size.line_height;
let line = min(line as i32, cur_size.bottommost_line().0);
Point::new(GridLine(line - display_offset as i32), col)
}
///Generate the bytes to send to the terminal, from the cell location, a mouse event, and the terminal mode
fn mouse_report(
point: Point,
button: MouseButton,
pressed: bool,
modifiers: Modifiers,
format: MouseFormat,
) -> Option<Vec<u8>> {
if point.line < 0 {
return None;
}
let mut mods = 0;
if modifiers.shift {
mods += 4;
}
if modifiers.alt {
mods += 8;
}
if modifiers.ctrl {
mods += 16;
}
match format {
MouseFormat::SGR => {
Some(sgr_mouse_report(point, button as u8 + mods, pressed).into_bytes())
}
MouseFormat::Normal(utf8) => {
if pressed {
normal_mouse_report(point, button as u8 + mods, utf8)
} else {
normal_mouse_report(point, 3 + mods, utf8)
}
}
}
}
fn normal_mouse_report(point: Point, button: u8, utf8: bool) -> Option<Vec<u8>> {
let Point { line, column } = point;
let max_point = if utf8 { 2015 } else { 223 };
if line >= max_point || column >= max_point {
return None;
}
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 >= 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);
}
Some(msg)
}
fn sgr_mouse_report(point: Point, button: u8, pressed: bool) -> String {
let c = if pressed { 'M' } else { 'm' };
let msg = format!(
"\x1b[<{};{};{}{}",
button,
point.column + 1,
point.line + 1,
c
);
msg
}
#[cfg(test)]
mod test {
use crate::mappings::mouse::mouse_point;
#[test]
fn test_mouse_to_selection() {
let term_width = 100.;
let term_height = 200.;
let cell_width = 10.;
let line_height = 20.;
let mouse_pos_x = 100.; //Window relative
let mouse_pos_y = 100.; //Window relative
let origin_x = 10.;
let origin_y = 20.;
let cur_size = crate::TerminalSize::new(
line_height,
cell_width,
gpui::geometry::vector::vec2f(term_width, term_height),
);
let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
let mouse_pos = mouse_pos - origin;
let point = mouse_point(mouse_pos, cur_size, 0);
assert_eq!(
point,
alacritty_terminal::index::Point::new(
alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
)
);
}
}

View File

@ -24,15 +24,19 @@ use futures::{
FutureExt,
};
use mappings::mouse::{
alt_scroll, mouse_button_report, mouse_moved_report, mouse_point, mouse_side, scroll_report,
};
use modal::deploy_modal;
use settings::{Settings, Shell, TerminalBlink};
use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc, time::Duration};
use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
use std::{collections::HashMap, fmt::Display, ops::Sub, path::PathBuf, sync::Arc, time::Duration};
use thiserror::Error;
use gpui::{
geometry::vector::{vec2f, Vector2F},
keymap::Keystroke,
ClipboardItem, Entity, ModelContext, MutableAppContext,
ClipboardItem, Entity, ModelContext, MouseButton, MouseButtonEvent, MouseMovedEvent,
MutableAppContext, ScrollWheelEvent,
};
use crate::mappings::{
@ -48,12 +52,15 @@ pub fn init(cx: &mut MutableAppContext) {
connected_view::init(cx);
}
///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
///Scroll multiplier that is set to 3 by default. This will be removed when I
///Implement scroll bars.
pub const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
const DEBUG_TERMINAL_WIDTH: f32 = 500.;
const DEBUG_TERMINAL_HEIGHT: f32 = 30.; //This needs to be wide enough that the CI & a local dev's prompt can fill the whole space.
const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
const DEBUG_CELL_WIDTH: f32 = 5.;
const DEBUG_LINE_HEIGHT: f32 = 5.;
// const MAX_FRAME_RATE: f32 = 60.;
// const BACK_BUFFER_SIZE: usize = 5000;
///Upward flowing events, for changing the title and such
#[derive(Clone, Copy, Debug)]
@ -256,6 +263,7 @@ impl TerminalBuilder {
env: Option<HashMap<String, String>>,
initial_size: TerminalSize,
blink_settings: Option<TerminalBlink>,
alternate_scroll: &AlternateScroll,
) -> Result<TerminalBuilder> {
let pty_config = {
let alac_shell = shell.clone().and_then(|shell| match shell {
@ -299,6 +307,14 @@ impl TerminalBuilder {
term.set_mode(alacritty_terminal::ansi::Mode::BlinkingCursor)
}
//Start alternate_scroll if we need to
if let AlternateScroll::On = alternate_scroll {
term.set_mode(alacritty_terminal::ansi::Mode::AlternateScroll)
} else {
//Alacritty turns it on by default, so we need to turn it off.
term.unset_mode(alacritty_terminal::ansi::Mode::AlternateScroll)
}
let term = Arc::new(FairMutex::new(term));
//Setup the pty...
@ -348,7 +364,8 @@ impl TerminalBuilder {
default_title: shell_txt,
last_mode: TermMode::NONE,
cur_size: initial_size,
// utilization: 0.,
last_mouse: None,
last_offset: 0,
};
Ok(TerminalBuilder {
@ -406,27 +423,6 @@ impl TerminalBuilder {
})
.detach();
// //Render loop
// cx.spawn_weak(|this, mut cx| async move {
// loop {
// let utilization = match this.upgrade(&cx) {
// Some(this) => this.update(&mut cx, |this, cx| {
// cx.notify();
// this.utilization()
// }),
// None => break,
// };
// let utilization = (1. - utilization).clamp(0.1, 1.);
// let delay = cx.background().timer(Duration::from_secs_f32(
// 1.0 / (Terminal::default_fps() * utilization),
// ));
// delay.await;
// }
// })
// .detach();
self.terminal
}
}
@ -439,19 +435,11 @@ pub struct Terminal {
title: String,
cur_size: TerminalSize,
last_mode: TermMode,
//Percentage, between 0 and 1
// utilization: f32,
last_offset: usize,
last_mouse: Option<(Point, Direction)>,
}
impl Terminal {
// fn default_fps() -> f32 {
// MAX_FRAME_RATE
// }
// fn utilization(&self) -> f32 {
// self.utilization
// }
fn process_event(&mut self, event: &AlacTermEvent, cx: &mut ModelContext<Self>) {
match event {
AlacTermEvent::Title(title) => {
@ -494,12 +482,6 @@ impl Terminal {
}
}
// fn process_events(&mut self, events: Vec<AlacTermEvent>, cx: &mut ModelContext<Self>) {
// for event in events.into_iter() {
// self.process_event(&event, cx);
// }
// }
///Takes events from Alacritty and translates them to behavior on this view
fn process_terminal_event(
&mut self,
@ -507,7 +489,6 @@ impl Terminal {
term: &mut Term<ZedListener>,
cx: &mut ModelContext<Self>,
) {
// TODO: Handle is_self_focused in subscription on terminal view
match event {
InternalEvent::TermEvent(term_event) => {
if let AlacTermEvent::ColorRequest(index, format) = term_event {
@ -546,8 +527,14 @@ impl Terminal {
}
}
pub fn input(&mut self, input: String) {
self.events.push(InternalEvent::Scroll(Scroll::Bottom));
self.events.push(InternalEvent::SetSelection(None));
self.write_to_pty(input);
}
///Write the Input payload to the tty.
pub fn write_to_pty(&self, input: String) {
fn write_to_pty(&self, input: String) {
self.pty_tx.notify(input.into_bytes());
}
@ -563,8 +550,7 @@ impl Terminal {
pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool {
let esc = to_esc_str(keystroke, &self.last_mode);
if let Some(esc) = esc {
self.write_to_pty(esc);
self.scroll(Scroll::Bottom);
self.input(esc);
true
} else {
false
@ -572,14 +558,13 @@ impl Terminal {
}
///Paste text into the terminal
pub fn paste(&self, text: &str) {
if self.last_mode.contains(TermMode::BRACKETED_PASTE) {
self.write_to_pty("\x1b[200~".to_string());
self.write_to_pty(text.replace('\x1b', ""));
self.write_to_pty("\x1b[201~".to_string());
pub fn paste(&mut self, text: &str) {
let paste_text = if self.last_mode.contains(TermMode::BRACKETED_PASTE) {
format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~")
} else {
self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r"));
}
text.replace("\r\n", "\r").replace('\n', "\r")
};
self.input(paste_text)
}
pub fn copy(&mut self) {
@ -597,21 +582,17 @@ impl Terminal {
self.process_terminal_event(&e, &mut term, cx)
}
// self.utilization = Self::estimate_utilization(term.take_last_processed_bytes());
self.last_mode = *term.mode();
let content = term.renderable_content();
self.last_offset = content.display_offset;
let cursor_text = term.grid()[content.cursor.point].c;
f(content, cursor_text)
}
///Scroll the terminal
pub fn scroll(&mut self, scroll: Scroll) {
self.events.push(InternalEvent::Scroll(scroll));
}
pub fn focus_in(&self) {
if self.last_mode.contains(TermMode::FOCUS_IN_OUT) {
self.write_to_pty("\x1b[I".to_string());
@ -624,34 +605,143 @@ impl Terminal {
}
}
pub fn click(&mut self, point: Point, side: Direction, clicks: usize) {
let selection_type = match clicks {
0 => return, //This is a release
1 => Some(SelectionType::Simple),
2 => Some(SelectionType::Semantic),
3 => Some(SelectionType::Lines),
_ => None,
};
let selection =
selection_type.map(|selection_type| Selection::new(selection_type, point, side));
self.events.push(InternalEvent::SetSelection(selection));
pub fn mouse_changed(&mut self, point: Point, side: Direction) -> bool {
match self.last_mouse {
Some((old_point, old_side)) => {
if old_point == point && old_side == side {
false
} else {
self.last_mouse = Some((point, side));
true
}
}
None => {
self.last_mouse = Some((point, side));
true
}
}
}
pub fn drag(&mut self, point: Point, side: Direction) {
self.events
.push(InternalEvent::UpdateSelection((point, side)));
pub fn mouse_mode(&self, shift: bool) -> bool {
self.last_mode.intersects(TermMode::MOUSE_MODE) && !shift
}
///TODO: Check if the mouse_down-then-click assumption holds, so this code works as expected
pub fn mouse_down(&mut self, point: Point, side: Direction) {
self.events
.push(InternalEvent::SetSelection(Some(Selection::new(
SelectionType::Simple,
point,
side,
))));
pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
dbg!("term mouse_move");
let position = e.position.sub(origin);
let point = mouse_point(position, self.cur_size, self.last_offset);
let side = mouse_side(position, self.cur_size);
if self.mouse_changed(point, side) && self.mouse_mode(e.shift) {
if let Some(bytes) = mouse_moved_report(point, e, self.last_mode) {
self.pty_tx.notify(bytes);
}
}
}
pub fn mouse_drag(&mut self, e: MouseMovedEvent, origin: Vector2F) {
let position = e.position.sub(origin);
if !self.mouse_mode(e.shift) {
let point = mouse_point(position, self.cur_size, self.last_offset);
let side = mouse_side(position, self.cur_size);
self.events
.push(InternalEvent::UpdateSelection((point, side)));
}
}
pub fn mouse_down(&mut self, e: &MouseButtonEvent, origin: Vector2F) {
let position = e.position.sub(origin);
let point = mouse_point(position, self.cur_size, self.last_offset);
let side = mouse_side(position, self.cur_size);
if self.mouse_mode(e.shift) {
if let Some(bytes) = mouse_button_report(point, e, true, self.last_mode) {
self.pty_tx.notify(bytes);
}
} else if e.button == MouseButton::Left {
self.events
.push(InternalEvent::SetSelection(Some(Selection::new(
SelectionType::Simple,
point,
side,
))));
}
}
pub fn left_click(&mut self, e: &MouseButtonEvent, origin: Vector2F) {
let position = e.position.sub(origin);
if !self.mouse_mode(e.shift) {
let point = mouse_point(position, self.cur_size, self.last_offset);
let side = mouse_side(position, self.cur_size);
let selection_type = match e.click_count {
0 => return, //This is a release
1 => Some(SelectionType::Simple),
2 => Some(SelectionType::Semantic),
3 => Some(SelectionType::Lines),
_ => None,
};
let selection =
selection_type.map(|selection_type| Selection::new(selection_type, point, side));
self.events.push(InternalEvent::SetSelection(selection));
}
}
pub fn mouse_up(&mut self, e: &MouseButtonEvent, origin: Vector2F) {
let position = e.position.sub(origin);
if self.mouse_mode(e.shift) {
let point = mouse_point(position, self.cur_size, self.last_offset);
if let Some(bytes) = mouse_button_report(point, e, false, self.last_mode) {
self.pty_tx.notify(bytes);
}
} else if e.button == MouseButton::Left {
// Seems pretty standard to automatically copy on mouse_up for terminals,
// so let's do that here
self.copy();
}
}
///Scroll the terminal
pub fn scroll(&mut self, scroll: &ScrollWheelEvent, origin: Vector2F) {
if self.mouse_mode(scroll.shift) {
//TODO: Currently this only sends the current scroll reports as they come in. Alacritty
//Sends the *entire* scroll delta on *every* scroll event, only resetting it when
//The scroll enters 'TouchPhase::Started'. Do I need to replicate this?
//This would be consistent with a scroll model based on 'distance from origin'...
let scroll_lines = (scroll.delta.y() / self.cur_size.line_height) as i32;
let point = mouse_point(scroll.position.sub(origin), self.cur_size, self.last_offset);
if let Some(scrolls) = scroll_report(point, scroll_lines as i32, scroll, self.last_mode)
{
for scroll in scrolls {
self.pty_tx.notify(scroll);
}
};
} else if self
.last_mode
.contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
&& !scroll.shift
{
//TODO: See above TODO, also applies here.
let scroll_lines = ((scroll.delta.y() * ALACRITTY_SCROLL_MULTIPLIER)
/ self.cur_size.line_height) as i32;
self.pty_tx.notify(alt_scroll(scroll_lines))
} else {
let scroll_lines = ((scroll.delta.y() * ALACRITTY_SCROLL_MULTIPLIER)
/ self.cur_size.line_height) as i32;
if scroll_lines != 0 {
let scroll = Scroll::Delta(scroll_lines);
self.events.push(InternalEvent::Scroll(scroll));
}
}
}
}

View File

@ -10,7 +10,7 @@ use workspace::{Item, Workspace};
use crate::TerminalSize;
use project::{LocalWorktree, Project, ProjectPath};
use settings::{Settings, WorkingDirectory};
use settings::{AlternateScroll, Settings, WorkingDirectory};
use smallvec::SmallVec;
use std::path::{Path, PathBuf};
@ -94,12 +94,26 @@ impl TerminalView {
let shell = settings.terminal_overrides.shell.clone();
let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
//TODO: move this pattern to settings
let scroll = settings
.terminal_overrides
.alternate_scroll
.as_ref()
.unwrap_or(
settings
.terminal_defaults
.alternate_scroll
.as_ref()
.unwrap_or_else(|| &AlternateScroll::On),
);
let content = match TerminalBuilder::new(
working_directory.clone(),
shell,
envs,
size_info,
settings.terminal_overrides.blinking.clone(),
scroll,
) {
Ok(terminal) => {
let terminal = cx.add_model(|cx| terminal.subscribe(cx));

View File

@ -5,7 +5,6 @@
"requires": true,
"packages": {
"": {
"name": "styles",
"version": "1.0.0",
"license": "ISC",
"dependencies": {