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

Mikayla Maki created

Terminal mouse mode

Change summary

assets/settings/default.json             |  62 ++-
crates/editor/src/element.rs             |   1 
crates/gpui/src/elements/flex.rs         |   1 
crates/gpui/src/elements/list.rs         |   1 
crates/gpui/src/elements/uniform_list.rs |   1 
crates/gpui/src/platform/event.rs        |   4 
crates/gpui/src/platform/mac/event.rs    |   6 
crates/gpui/src/presenter.rs             |  42 +++
crates/gpui/src/scene/mouse_region.rs    |  26 +
crates/settings/src/settings.rs          |  14 +
crates/terminal/README.md                |   5 
crates/terminal/src/connected_el.rs      | 348 ++++++++++++-------------
crates/terminal/src/connected_view.rs    |   6 
crates/terminal/src/mappings/keys.rs     |   1 
crates/terminal/src/mappings/mod.rs      |   1 
crates/terminal/src/mappings/mouse.rs    | 330 ++++++++++++++++++++++++
crates/terminal/src/terminal.rs          | 256 ++++++++++++------
crates/terminal/src/terminal_view.rs     |  16 +
styles/package-lock.json                 |   1 
19 files changed, 822 insertions(+), 300 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -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": {

crates/editor/src/element.rs 🔗

@@ -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),

crates/gpui/src/elements/flex.rs 🔗

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

crates/gpui/src/platform/event.rs 🔗

@@ -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)]

crates/gpui/src/platform/mac/event.rs 🔗

@@ -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

crates/gpui/src/presenter.rs 🔗

@@ -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) =

crates/gpui/src/scene/mouse_region.rs 🔗

@@ -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)]

crates/settings/src/settings.rs 🔗

@@ -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 {

crates/terminal/README.md 🔗

@@ -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

crates/terminal/src/connected_el.rs 🔗

@@ -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,
-                                );
-
-                                terminal.mouse_down(point, side);
-
-                                cx.notify();
-                            })
-                        }
+        let connection = self.terminal;
+
+        let mut region = MouseRegion::new(view_id, None, visible_bounds);
+
+        //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);
                     },
-                )
-                .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();
-                            });
-                        }
+                ),
+            )
+            //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_click(
+                .on_down(
+                    MouseButton::Middle,
+                    TerminalEl::generic_button_handler(
+                        connection,
+                        origin,
+                        move |terminal, origin, e, _cx| {
+                            terminal.mouse_down(&e, origin);
+                        },
+                    ),
+                )
+                .on_up(
                     MouseButton::Right,
-                    move |MouseButtonEvent { position, .. }, cx| {
-                        cx.dispatch_action(DeployContextMenu { position });
-                    },
+                    TerminalEl::generic_button_handler(
+                        connection,
+                        origin,
+                        move |terminal, origin, e, _cx| {
+                            terminal.mouse_up(&e, origin);
+                        },
+                    ),
                 )
-                .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()
-                            });
-                        }
-                    },
-                ),
-        );
+                .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),
-            )
-        );
-    }
-}

crates/terminal/src/connected_view.rs 🔗

@@ -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());
         });
     }
 

crates/terminal/src/mappings/keys.rs 🔗

@@ -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;
 

crates/terminal/src/mappings/mouse.rs 🔗

@@ -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),
+            )
+        );
+    }
+}

crates/terminal/src/terminal.rs 🔗

@@ -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,
-        };
+    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 mouse_mode(&self, shift: bool) -> bool {
+        self.last_mode.intersects(TermMode::MOUSE_MODE) && !shift
+    }
+
+    pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
+        dbg!("term mouse_move");
+        let position = e.position.sub(origin);
 
-        let selection =
-            selection_type.map(|selection_type| Selection::new(selection_type, point, side));
+        let point = mouse_point(position, self.cur_size, self.last_offset);
+        let side = mouse_side(position, self.cur_size);
 
-        self.events.push(InternalEvent::SetSelection(selection));
+        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 drag(&mut self, point: Point, side: Direction) {
-        self.events
-            .push(InternalEvent::UpdateSelection((point, side)));
+    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();
+        }
     }
 
-    ///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,
-            ))));
+    ///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));
+            }
+        }
     }
 }
 

crates/terminal/src/terminal_view.rs 🔗

@@ -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));

styles/package-lock.json 🔗

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