Terminal mouse improvements (#25104)

Conrad Irwin and Mikayla Maki created

Closes #24911
Closes #17983
Closes #7073

Release Notes:

- Terminal: Fix cmd-click on links/files when terminal is not focused
- Terminal: Remove hover treatment after Zed hides/re-opens

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>

Change summary

crates/repl/src/outputs/plain.rs               |  19 
crates/terminal/src/mappings/mouse.rs          |  43 +-
crates/terminal/src/terminal.rs                | 267 ++++++++++---------
crates/terminal_view/src/terminal_element.rs   |  96 ++----
crates/terminal_view/src/terminal_scrollbar.rs |   2 
crates/terminal_view/src/terminal_view.rs      |  32 +-
6 files changed, 229 insertions(+), 230 deletions(-)

Detailed changes

crates/repl/src/outputs/plain.rs 🔗

@@ -22,7 +22,7 @@ use alacritty_terminal::{
     term::Config,
     vte::ansi::Processor,
 };
-use gpui::{canvas, size, ClipboardItem, Entity, FontStyle, TextStyle, WhiteSpace};
+use gpui::{canvas, size, Bounds, ClipboardItem, Entity, FontStyle, TextStyle, WhiteSpace};
 use language::Buffer;
 use settings::Settings as _;
 use terminal_view::terminal_element::TerminalElement;
@@ -85,7 +85,7 @@ pub fn text_style(window: &mut Window, cx: &mut App) -> TextStyle {
 }
 
 /// Returns the default terminal size for the terminal output.
-pub fn terminal_size(window: &mut Window, cx: &mut App) -> terminal::TerminalSize {
+pub fn terminal_size(window: &mut Window, cx: &mut App) -> terminal::TerminalBounds {
     let text_style = text_style(window, cx);
     let text_system = window.text_system();
 
@@ -106,10 +106,13 @@ pub fn terminal_size(window: &mut Window, cx: &mut App) -> terminal::TerminalSiz
     let width = columns as f32 * cell_width;
     let height = num_lines as f32 * window.line_height();
 
-    terminal::TerminalSize {
+    terminal::TerminalBounds {
         cell_width,
         line_height,
-        size: size(width, height),
+        bounds: Bounds {
+            origin: gpui::Point::default(),
+            size: size(width, height),
+        },
     }
 }
 
@@ -277,10 +280,10 @@ impl Render for TerminalOutput {
                 for rect in rects {
                     rect.paint(
                         bounds.origin,
-                        &terminal::TerminalSize {
+                        &terminal::TerminalBounds {
                             cell_width,
                             line_height: text_line_height,
-                            size: bounds.size,
+                            bounds,
                         },
                         window,
                     );
@@ -289,10 +292,10 @@ impl Render for TerminalOutput {
                 for cell in cells {
                     cell.paint(
                         bounds.origin,
-                        &terminal::TerminalSize {
+                        &terminal::TerminalBounds {
                             cell_width,
                             line_height: text_line_height,
-                            size: bounds.size,
+                            bounds,
                         },
                         bounds,
                         window,

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

@@ -6,9 +6,9 @@ use alacritty_terminal::grid::Dimensions;
 /// with modifications for our circumstances
 use alacritty_terminal::index::{Column as GridCol, Line as GridLine, Point as AlacPoint, Side};
 use alacritty_terminal::term::TermMode;
-use gpui::{px, Modifiers, MouseButton, MouseMoveEvent, Pixels, Point, ScrollWheelEvent};
+use gpui::{px, Modifiers, MouseButton, Pixels, Point, ScrollWheelEvent};
 
-use crate::TerminalSize;
+use crate::TerminalBounds;
 
 enum MouseFormat {
     Sgr,
@@ -42,14 +42,12 @@ enum AlacMouseButton {
 }
 
 impl AlacMouseButton {
-    fn from_move(e: &MouseMoveEvent) -> Self {
-        match e.pressed_button {
-            Some(b) => match b {
-                gpui::MouseButton::Left => AlacMouseButton::LeftMove,
-                gpui::MouseButton::Middle => AlacMouseButton::MiddleMove,
-                gpui::MouseButton::Right => AlacMouseButton::RightMove,
-                gpui::MouseButton::Navigate(_) => AlacMouseButton::Other,
-            },
+    fn from_move_button(e: Option<MouseButton>) -> Self {
+        match e {
+            Some(gpui::MouseButton::Left) => AlacMouseButton::LeftMove,
+            Some(gpui::MouseButton::Middle) => AlacMouseButton::MiddleMove,
+            Some(gpui::MouseButton::Right) => AlacMouseButton::RightMove,
+            Some(gpui::MouseButton::Navigate(_)) => AlacMouseButton::Other,
             None => AlacMouseButton::NoneMove,
         }
     }
@@ -134,34 +132,37 @@ pub fn mouse_button_report(
     }
 }
 
-pub fn mouse_moved_report(point: AlacPoint, e: &MouseMoveEvent, mode: TermMode) -> Option<Vec<u8>> {
-    let button = AlacMouseButton::from_move(e);
+pub fn mouse_moved_report(
+    point: AlacPoint,
+    button: Option<MouseButton>,
+    modifiers: Modifiers,
+    mode: TermMode,
+) -> Option<Vec<u8>> {
+    let button = AlacMouseButton::from_move_button(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, AlacMouseButton::NoneMove) {
             None
         } else {
-            mouse_report(
-                point,
-                button,
-                true,
-                e.modifiers,
-                MouseFormat::from_mode(mode),
-            )
+            mouse_report(point, button, true, modifiers, MouseFormat::from_mode(mode))
         }
     } else {
         None
     }
 }
 
-pub fn grid_point(pos: Point<Pixels>, cur_size: TerminalSize, display_offset: usize) -> AlacPoint {
+pub fn grid_point(
+    pos: Point<Pixels>,
+    cur_size: TerminalBounds,
+    display_offset: usize,
+) -> AlacPoint {
     grid_point_and_side(pos, cur_size, display_offset).0
 }
 
 pub fn grid_point_and_side(
     pos: Point<Pixels>,
-    cur_size: TerminalSize,
+    cur_size: TerminalBounds,
     display_offset: usize,
 ) -> (AlacPoint, Side) {
     let mut col = GridCol((pos.x / cur_size.cell_width) as usize);

crates/terminal/src/terminal.rs 🔗

@@ -61,6 +61,7 @@ use gpui::{
     actions, black, px, AnyWindowHandle, App, AppContext as _, Bounds, ClipboardItem, Context,
     EventEmitter, Hsla, Keystroke, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent,
     MouseUpEvent, Pixels, Point, Rgba, ScrollWheelEvent, SharedString, Size, Task, TouchPhase,
+    Window,
 };
 
 use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str};
@@ -131,7 +132,7 @@ pub enum MaybeNavigationTarget {
 
 #[derive(Clone)]
 enum InternalEvent {
-    Resize(TerminalSize),
+    Resize(TerminalBounds),
     Clear,
     // FocusNextMatch,
     Scroll(AlacScroll),
@@ -161,35 +162,35 @@ pub fn init(cx: &mut App) {
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
-pub struct TerminalSize {
+pub struct TerminalBounds {
     pub cell_width: Pixels,
     pub line_height: Pixels,
-    pub size: Size<Pixels>,
+    pub bounds: Bounds<Pixels>,
 }
 
-impl TerminalSize {
-    pub fn new(line_height: Pixels, cell_width: Pixels, size: Size<Pixels>) -> Self {
-        TerminalSize {
+impl TerminalBounds {
+    pub fn new(line_height: Pixels, cell_width: Pixels, bounds: Bounds<Pixels>) -> Self {
+        TerminalBounds {
             cell_width,
             line_height,
-            size,
+            bounds,
         }
     }
 
     pub fn num_lines(&self) -> usize {
-        (self.size.height / self.line_height).floor() as usize
+        (self.bounds.size.height / self.line_height).floor() as usize
     }
 
     pub fn num_columns(&self) -> usize {
-        (self.size.width / self.cell_width).floor() as usize
+        (self.bounds.size.width / self.cell_width).floor() as usize
     }
 
     pub fn height(&self) -> Pixels {
-        self.size.height
+        self.bounds.size.height
     }
 
     pub fn width(&self) -> Pixels {
-        self.size.width
+        self.bounds.size.width
     }
 
     pub fn cell_width(&self) -> Pixels {
@@ -201,21 +202,24 @@ impl TerminalSize {
     }
 }
 
-impl Default for TerminalSize {
+impl Default for TerminalBounds {
     fn default() -> Self {
-        TerminalSize::new(
+        TerminalBounds::new(
             DEBUG_LINE_HEIGHT,
             DEBUG_CELL_WIDTH,
-            Size {
-                width: DEBUG_TERMINAL_WIDTH,
-                height: DEBUG_TERMINAL_HEIGHT,
+            Bounds {
+                origin: Point::default(),
+                size: Size {
+                    width: DEBUG_TERMINAL_WIDTH,
+                    height: DEBUG_TERMINAL_HEIGHT,
+                },
             },
         )
     }
 }
 
-impl From<TerminalSize> for WindowSize {
-    fn from(val: TerminalSize) -> Self {
+impl From<TerminalBounds> for WindowSize {
+    fn from(val: TerminalBounds) -> Self {
         WindowSize {
             num_lines: val.num_lines() as u16,
             num_cols: val.num_columns() as u16,
@@ -225,7 +229,7 @@ impl From<TerminalSize> for WindowSize {
     }
 }
 
-impl Dimensions for TerminalSize {
+impl Dimensions for TerminalBounds {
     /// Note: this is supposed to be for the back buffer's length,
     /// but we exclusively use it to resize the terminal, which does not
     /// use this method. We still have to implement it for the trait though,
@@ -406,7 +410,7 @@ impl TerminalBuilder {
         //Set up the terminal...
         let mut term = Term::new(
             config.clone(),
-            &TerminalSize::default(),
+            &TerminalBounds::default(),
             ZedListener(events_tx.clone()),
         );
 
@@ -420,7 +424,7 @@ impl TerminalBuilder {
         //Setup the pty...
         let pty = match tty::new(
             &pty_options,
-            TerminalSize::default().into(),
+            TerminalBounds::default().into(),
             window.window_id().as_u64(),
         ) {
             Ok(pty) => pty,
@@ -463,11 +467,9 @@ impl TerminalBuilder {
             pty_info,
             breadcrumb_text: String::new(),
             scroll_px: px(0.),
-            last_mouse_position: None,
             next_link_id: 0,
             selection_phase: SelectionPhase::Ended,
-            secondary_pressed: false,
-            hovered_word: false,
+            // hovered_word: false,
             url_regex: RegexSearch::new(URL_REGEX).unwrap(),
             word_regex: RegexSearch::new(WORD_REGEX).unwrap(),
             vi_mode_enabled: false,
@@ -569,7 +571,7 @@ pub struct TerminalContent {
     pub selection: Option<SelectionRange>,
     pub cursor: RenderableCursor,
     pub cursor_char: char,
-    pub size: TerminalSize,
+    pub terminal_bounds: TerminalBounds,
     pub last_hovered_word: Option<HoveredWord>,
 }
 
@@ -593,7 +595,7 @@ impl Default for TerminalContent {
                 point: AlacPoint::new(Line(0), Column(0)),
             },
             cursor_char: Default::default(),
-            size: Default::default(),
+            terminal_bounds: Default::default(),
             last_hovered_word: None,
         }
     }
@@ -613,8 +615,6 @@ pub struct Terminal {
     events: VecDeque<InternalEvent>,
     /// This is only used for mouse mode cell change detection
     last_mouse: Option<(AlacPoint, AlacDirection)>,
-    /// This is only used for terminal hovered word checking
-    last_mouse_position: Option<Point<Pixels>>,
     pub matches: Vec<RangeInclusive<AlacPoint>>,
     pub last_content: TerminalContent,
     pub selection_head: Option<AlacPoint>,
@@ -625,8 +625,6 @@ pub struct Terminal {
     scroll_px: Pixels,
     next_link_id: usize,
     selection_phase: SelectionPhase,
-    secondary_pressed: bool,
-    hovered_word: bool,
     url_regex: RegexSearch,
     word_regex: RegexSearch,
     task: Option<TaskState>,
@@ -697,7 +695,7 @@ impl Terminal {
             }
             AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
             AlacTermEvent::TextAreaSizeRequest(format) => {
-                self.write_to_pty(format(self.last_content.size.into()))
+                self.write_to_pty(format(self.last_content.terminal_bounds.into()))
             }
             AlacTermEvent::CursorBlinkingChange => {
                 let terminal = self.term.lock();
@@ -746,18 +744,20 @@ impl Terminal {
         &mut self,
         event: &InternalEvent,
         term: &mut Term<ZedListener>,
+        window: &mut Window,
         cx: &mut Context<Self>,
     ) {
         match event {
-            InternalEvent::Resize(mut new_size) => {
-                new_size.size.height = cmp::max(new_size.line_height, new_size.height());
-                new_size.size.width = cmp::max(new_size.cell_width, new_size.width());
+            InternalEvent::Resize(mut new_bounds) => {
+                new_bounds.bounds.size.height =
+                    cmp::max(new_bounds.line_height, new_bounds.height());
+                new_bounds.bounds.size.width = cmp::max(new_bounds.cell_width, new_bounds.width());
 
-                self.last_content.size = new_size;
+                self.last_content.terminal_bounds = new_bounds;
 
-                self.pty_tx.0.send(Msg::Resize(new_size.into())).ok();
+                self.pty_tx.0.send(Msg::Resize(new_bounds.into())).ok();
 
-                term.resize(new_size);
+                term.resize(new_bounds);
             }
             InternalEvent::Clear => {
                 // Clear back buffer
@@ -793,7 +793,7 @@ impl Terminal {
             }
             InternalEvent::Scroll(scroll) => {
                 term.scroll_display(*scroll);
-                self.refresh_hovered_word();
+                self.refresh_hovered_word(window);
 
                 if self.vi_mode_enabled {
                     match *scroll {
@@ -849,7 +849,7 @@ impl Terminal {
                 if let Some(mut selection) = term.selection.take() {
                     let (point, side) = grid_point_and_side(
                         *position,
-                        self.last_content.size,
+                        self.last_content.terminal_bounds,
                         term.grid().display_offset(),
                     );
 
@@ -873,7 +873,7 @@ impl Terminal {
             }
             InternalEvent::ScrollToAlacPoint(point) => {
                 term.scroll_to_point(*point);
-                self.refresh_hovered_word();
+                self.refresh_hovered_word(window);
             }
             InternalEvent::ToggleViMode => {
                 self.vi_mode_enabled = !self.vi_mode_enabled;
@@ -887,7 +887,7 @@ impl Terminal {
 
                 let point = grid_point(
                     *position,
-                    self.last_content.size,
+                    self.last_content.terminal_bounds,
                     term.grid().display_offset(),
                 )
                 .grid_clamp(term, Boundary::Grid);
@@ -977,13 +977,9 @@ impl Terminal {
                                 cx,
                             );
                         }
-                        self.hovered_word = true;
                     }
                     None => {
-                        if self.hovered_word {
-                            cx.emit(Event::NewNavigationTarget(None));
-                        }
-                        self.hovered_word = false;
+                        cx.emit(Event::NewNavigationTarget(None));
                     }
                 }
             }
@@ -1015,6 +1011,7 @@ impl Terminal {
             id: self.next_link_id(),
         });
         cx.emit(Event::NewNavigationTarget(Some(navigation_target)));
+        cx.notify()
     }
 
     fn next_link_id(&mut self) -> usize {
@@ -1134,9 +1131,9 @@ impl Terminal {
     }
 
     ///Resize the terminal and the PTY.
-    pub fn set_size(&mut self, new_size: TerminalSize) {
-        if self.last_content.size != new_size {
-            self.events.push_back(InternalEvent::Resize(new_size))
+    pub fn set_size(&mut self, new_bounds: TerminalBounds) {
+        if self.last_content.terminal_bounds != new_bounds {
+            self.events.push_back(InternalEvent::Resize(new_bounds))
         }
     }
 
@@ -1200,8 +1197,8 @@ impl Terminal {
         if let Some(motion) = motion {
             let cursor = self.last_content.cursor.point;
             let cursor_pos = Point {
-                x: cursor.column.0 as f32 * self.last_content.size.cell_width,
-                y: cursor.line.0 as f32 * self.last_content.size.line_height,
+                x: cursor.column.0 as f32 * self.last_content.terminal_bounds.cell_width,
+                y: cursor.line.0 as f32 * self.last_content.terminal_bounds.line_height,
             };
             self.events
                 .push_back(InternalEvent::UpdateSelection(cursor_pos));
@@ -1215,11 +1212,11 @@ impl Terminal {
             "b" if keystroke.modifiers.control => Some(AlacScroll::PageUp),
             "f" if keystroke.modifiers.control => Some(AlacScroll::PageDown),
             "d" if keystroke.modifiers.control => {
-                let amount = self.last_content.size.line_height().to_f64() as i32 / 2;
+                let amount = self.last_content.terminal_bounds.line_height().to_f64() as i32 / 2;
                 Some(AlacScroll::Delta(-amount))
             }
             "u" if keystroke.modifiers.control => {
-                let amount = self.last_content.size.line_height().to_f64() as i32 / 2;
+                let amount = self.last_content.terminal_bounds.line_height().to_f64() as i32 / 2;
                 Some(AlacScroll::Delta(amount))
             }
             _ => None,
@@ -1277,13 +1274,22 @@ impl Terminal {
         }
     }
 
-    pub fn try_modifiers_change(&mut self, modifiers: &Modifiers) -> bool {
-        let changed = self.secondary_pressed != modifiers.secondary();
-        if !self.secondary_pressed && modifiers.secondary() {
-            self.refresh_hovered_word();
+    pub fn try_modifiers_change(
+        &mut self,
+        modifiers: &Modifiers,
+        window: &Window,
+        cx: &mut Context<Self>,
+    ) {
+        if self
+            .last_content
+            .terminal_bounds
+            .bounds
+            .contains(&window.mouse_position())
+            && modifiers.secondary()
+        {
+            self.refresh_hovered_word(window);
         }
-        self.secondary_pressed = modifiers.secondary();
-        changed
+        cx.notify();
     }
 
     ///Paste text into the terminal
@@ -1297,12 +1303,12 @@ impl Terminal {
         self.input(paste_text);
     }
 
-    pub fn sync(&mut self, cx: &mut Context<Self>) {
+    pub fn sync(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         let term = self.term.clone();
         let mut terminal = term.lock_unfair();
         //Note that the ordering of events matters for event processing
         while let Some(e) = self.events.pop_front() {
-            self.process_terminal_event(&e, &mut terminal, cx)
+            self.process_terminal_event(&e, &mut terminal, window, cx)
         }
 
         self.last_content = Self::make_content(&terminal, &self.last_content);
@@ -1331,7 +1337,7 @@ impl Terminal {
             selection: content.selection,
             cursor: content.cursor,
             cursor_char: term.grid()[content.cursor.point].c,
-            size: last_content.size,
+            terminal_bounds: last_content.terminal_bounds,
             last_hovered_word: last_content.last_hovered_word.clone(),
         }
     }
@@ -1368,7 +1374,6 @@ impl Terminal {
     }
 
     pub fn focus_out(&mut self) {
-        self.last_mouse_position = None;
         if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
             self.write_to_pty("\x1b[O".to_string());
         }
@@ -1395,44 +1400,48 @@ impl Terminal {
         self.last_content.mode.intersects(TermMode::MOUSE_MODE) && !shift
     }
 
-    pub fn mouse_move(&mut self, e: &MouseMoveEvent, origin: Point<Pixels>) {
-        let position = e.position - origin;
-        self.last_mouse_position = Some(position);
+    pub fn mouse_move(&mut self, e: &MouseMoveEvent, cx: &mut Context<Self>) {
+        let position = e.position - self.last_content.terminal_bounds.bounds.origin;
         if self.mouse_mode(e.modifiers.shift) {
             let (point, side) = grid_point_and_side(
                 position,
-                self.last_content.size,
+                self.last_content.terminal_bounds,
                 self.last_content.display_offset,
             );
 
             if self.mouse_changed(point, side) {
-                if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
+                if let Some(bytes) =
+                    mouse_moved_report(point, e.pressed_button, e.modifiers, self.last_content.mode)
+                {
                     self.pty_tx.notify(bytes);
                 }
             }
-        } else if self.secondary_pressed {
-            self.word_from_position(Some(position));
+        } else if e.modifiers.secondary() {
+            self.word_from_position(e.position);
         }
+        cx.notify();
     }
 
-    fn word_from_position(&mut self, position: Option<Point<Pixels>>) {
+    fn word_from_position(&mut self, position: Point<Pixels>) {
         if self.selection_phase == SelectionPhase::Selecting {
             self.last_content.last_hovered_word = None;
-        } else if let Some(position) = position {
-            self.events
-                .push_back(InternalEvent::FindHyperlink(position, false));
+        } else if self.last_content.terminal_bounds.bounds.contains(&position) {
+            self.events.push_back(InternalEvent::FindHyperlink(
+                position - self.last_content.terminal_bounds.bounds.origin,
+                false,
+            ));
+        } else {
+            self.last_content.last_hovered_word = None;
         }
     }
 
     pub fn mouse_drag(
         &mut self,
         e: &MouseMoveEvent,
-        origin: Point<Pixels>,
         region: Bounds<Pixels>,
+        cx: &mut Context<Self>,
     ) {
-        let position = e.position - origin;
-        self.last_mouse_position = Some(position);
-
+        let position = e.position - self.last_content.terminal_bounds.bounds.origin;
         if !self.mouse_mode(e.modifiers.shift) {
             self.selection_phase = SelectionPhase::Selecting;
             // Alacritty has the same ordering, of first updating the selection
@@ -1447,18 +1456,21 @@ impl Terminal {
                     None => return,
                 };
 
-                let scroll_lines = (scroll_delta / self.last_content.size.line_height) as i32;
+                let scroll_lines =
+                    (scroll_delta / self.last_content.terminal_bounds.line_height) as i32;
 
                 self.events
                     .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
             }
+
+            cx.notify();
         }
     }
 
     fn drag_line_delta(&self, e: &MouseMoveEvent, region: Bounds<Pixels>) -> Option<Pixels> {
         //TODO: Why do these need to be doubled? Probably the same problem that the IME has
-        let top = region.origin.y + (self.last_content.size.line_height * 2.);
-        let bottom = region.bottom_left().y - (self.last_content.size.line_height * 2.);
+        let top = region.origin.y + (self.last_content.terminal_bounds.line_height * 2.);
+        let bottom = region.bottom_left().y - (self.last_content.terminal_bounds.line_height * 2.);
         let scroll_delta = if e.position.y < top {
             (top - e.position.y).pow(1.1)
         } else if e.position.y > bottom {
@@ -1469,16 +1481,11 @@ impl Terminal {
         Some(scroll_delta)
     }
 
-    pub fn mouse_down(
-        &mut self,
-        e: &MouseDownEvent,
-        origin: Point<Pixels>,
-        _cx: &mut Context<Self>,
-    ) {
-        let position = e.position - origin;
+    pub fn mouse_down(&mut self, e: &MouseDownEvent, _cx: &mut Context<Self>) {
+        let position = e.position - self.last_content.terminal_bounds.bounds.origin;
         let point = grid_point(
             position,
-            self.last_content.size,
+            self.last_content.terminal_bounds,
             self.last_content.display_offset,
         );
 
@@ -1491,10 +1498,9 @@ impl Terminal {
         } else {
             match e.button {
                 MouseButton::Left => {
-                    let position = e.position - origin;
                     let (point, side) = grid_point_and_side(
                         position,
-                        self.last_content.size,
+                        self.last_content.terminal_bounds,
                         self.last_content.display_offset,
                     );
 
@@ -1526,14 +1532,14 @@ impl Terminal {
         }
     }
 
-    pub fn mouse_up(&mut self, e: &MouseUpEvent, origin: Point<Pixels>, cx: &Context<Self>) {
+    pub fn mouse_up(&mut self, e: &MouseUpEvent, cx: &Context<Self>) {
         let setting = TerminalSettings::get_global(cx);
 
-        let position = e.position - origin;
+        let position = e.position - self.last_content.terminal_bounds.bounds.origin;
         if self.mouse_mode(e.modifiers.shift) {
             let point = grid_point(
                 position,
-                self.last_content.size,
+                self.last_content.terminal_bounds,
                 self.last_content.display_offset,
             );
 
@@ -1549,10 +1555,11 @@ impl Terminal {
 
             //Hyperlinks
             if self.selection_phase == SelectionPhase::Ended {
-                let mouse_cell_index = content_index_for_mouse(position, &self.last_content.size);
+                let mouse_cell_index =
+                    content_index_for_mouse(position, &self.last_content.terminal_bounds);
                 if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() {
                     cx.open_url(link.uri());
-                } else if self.secondary_pressed {
+                } else if e.modifiers.secondary() {
                     self.events
                         .push_back(InternalEvent::FindHyperlink(position, true));
                 }
@@ -1564,14 +1571,14 @@ impl Terminal {
     }
 
     ///Scroll the terminal
-    pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, origin: Point<Pixels>) {
+    pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent) {
         let mouse_mode = self.mouse_mode(e.shift);
 
         if let Some(scroll_lines) = self.determine_scroll_lines(e, mouse_mode) {
             if mouse_mode {
                 let point = grid_point(
-                    e.position - origin,
-                    self.last_content.size,
+                    e.position - self.last_content.terminal_bounds.bounds.origin,
+                    self.last_content.terminal_bounds,
                     self.last_content.display_offset,
                 );
 
@@ -1596,13 +1603,13 @@ impl Terminal {
         }
     }
 
-    fn refresh_hovered_word(&mut self) {
-        self.word_from_position(self.last_mouse_position);
+    fn refresh_hovered_word(&mut self, window: &Window) {
+        self.word_from_position(window.mouse_position());
     }
 
     fn determine_scroll_lines(&mut self, e: &ScrollWheelEvent, mouse_mode: bool) -> Option<i32> {
         let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER };
-        let line_height = self.last_content.size.line_height;
+        let line_height = self.last_content.terminal_bounds.line_height;
         match e.touch_phase {
             /* Reset scroll state on started */
             TouchPhase::Started => {
@@ -1619,7 +1626,7 @@ impl Terminal {
 
                 // Whenever we hit the edges, reset our stored scroll to 0
                 // so we can respond to changes in direction quickly
-                self.scroll_px %= self.last_content.size.height();
+                self.scroll_px %= self.last_content.terminal_bounds.height();
 
                 Some(new_offset - old_offset)
             }
@@ -1714,10 +1721,6 @@ impl Terminal {
         }
     }
 
-    pub fn can_navigate_to_selected_word(&self) -> bool {
-        self.secondary_pressed && self.hovered_word
-    }
-
     pub fn task(&self) -> Option<&TaskState> {
         self.task.as_ref()
     }
@@ -1899,12 +1902,12 @@ fn all_search_matches<'a, T>(
     RegexIter::new(start, end, AlacDirection::Right, term, regex)
 }
 
-fn content_index_for_mouse(pos: Point<Pixels>, size: &TerminalSize) -> usize {
-    let col = (pos.x / size.cell_width()).round() as usize;
-    let clamped_col = min(col, size.columns() - 1);
-    let row = (pos.y / size.line_height()).round() as usize;
-    let clamped_row = min(row, size.screen_lines() - 1);
-    clamped_row * size.columns() + clamped_col
+fn content_index_for_mouse(pos: Point<Pixels>, terminal_bounds: &TerminalBounds) -> usize {
+    let col = (pos.x / terminal_bounds.cell_width()).round() as usize;
+    let clamped_col = min(col, terminal_bounds.columns() - 1);
+    let row = (pos.y / terminal_bounds.line_height()).round() as usize;
+    let clamped_row = min(row, terminal_bounds.screen_lines() - 1);
+    clamped_row * terminal_bounds.columns() + clamped_col
 }
 
 /// Converts an 8 bit ANSI color to its GPUI equivalent.
@@ -1997,11 +2000,11 @@ mod tests {
         index::{Column, Line, Point as AlacPoint},
         term::cell::Cell,
     };
-    use gpui::{point, size, Pixels};
+    use gpui::{bounds, point, size, Pixels, Point};
     use rand::{distributions::Alphanumeric, rngs::ThreadRng, thread_rng, Rng};
 
     use crate::{
-        content_index_for_mouse, rgb_for_index, IndexedCell, TerminalContent, TerminalSize,
+        content_index_for_mouse, rgb_for_index, IndexedCell, TerminalBounds, TerminalContent,
     };
 
     #[test]
@@ -2023,12 +2026,15 @@ mod tests {
             let viewport_cells = rng.gen_range(15..20);
             let cell_size = rng.gen_range(5 * PRECISION..20 * PRECISION) as f32 / PRECISION as f32;
 
-            let size = crate::TerminalSize {
+            let size = crate::TerminalBounds {
                 cell_width: Pixels::from(cell_size),
                 line_height: Pixels::from(cell_size),
-                size: size(
-                    Pixels::from(cell_size * (viewport_cells as f32)),
-                    Pixels::from(cell_size * (viewport_cells as f32)),
+                bounds: bounds(
+                    Point::default(),
+                    size(
+                        Pixels::from(cell_size * (viewport_cells as f32)),
+                        Pixels::from(cell_size * (viewport_cells as f32)),
+                    ),
                 ),
             };
 
@@ -2048,7 +2054,8 @@ mod tests {
                         Pixels::from(row as f32 * cell_size + row_offset),
                     );
 
-                    let content_index = content_index_for_mouse(mouse_pos, &content.size);
+                    let content_index =
+                        content_index_for_mouse(mouse_pos, &content.terminal_bounds);
                     let mouse_cell = content.cells[content_index].c;
                     let real_cell = cells[row][col];
 
@@ -2062,10 +2069,13 @@ mod tests {
     fn test_mouse_to_cell_clamp() {
         let mut rng = thread_rng();
 
-        let size = crate::TerminalSize {
+        let size = crate::TerminalBounds {
             cell_width: Pixels::from(10.),
             line_height: Pixels::from(10.),
-            size: size(Pixels::from(100.), Pixels::from(100.)),
+            bounds: bounds(
+                Point::default(),
+                size(Pixels::from(100.), Pixels::from(100.)),
+            ),
         };
 
         let cells = get_cells(size, &mut rng);
@@ -2074,7 +2084,7 @@ mod tests {
         assert_eq!(
             content.cells[content_index_for_mouse(
                 point(Pixels::from(-10.), Pixels::from(-10.)),
-                &content.size,
+                &content.terminal_bounds,
             )]
             .c,
             cells[0][0]
@@ -2082,14 +2092,14 @@ mod tests {
         assert_eq!(
             content.cells[content_index_for_mouse(
                 point(Pixels::from(1000.), Pixels::from(1000.)),
-                &content.size,
+                &content.terminal_bounds,
             )]
             .c,
             cells[9][9]
         );
     }
 
-    fn get_cells(size: TerminalSize, rng: &mut ThreadRng) -> Vec<Vec<char>> {
+    fn get_cells(size: TerminalBounds, rng: &mut ThreadRng) -> Vec<Vec<char>> {
         let mut cells = Vec::new();
 
         for _ in 0..((size.height() / size.line_height()) as usize) {
@@ -2104,7 +2114,10 @@ mod tests {
         cells
     }
 
-    fn convert_cells_to_content(size: TerminalSize, cells: &[Vec<char>]) -> TerminalContent {
+    fn convert_cells_to_content(
+        terminal_bounds: TerminalBounds,
+        cells: &[Vec<char>],
+    ) -> TerminalContent {
         let mut ic = Vec::new();
 
         for (index, row) in cells.iter().enumerate() {
@@ -2121,7 +2134,7 @@ mod tests {
 
         TerminalContent {
             cells: ic,
-            size,
+            terminal_bounds,
             ..Default::default()
         }
     }

crates/terminal_view/src/terminal_element.rs 🔗

@@ -21,7 +21,7 @@ use terminal::{
         },
     },
     terminal_settings::TerminalSettings,
-    HoveredWord, IndexedCell, Terminal, TerminalContent, TerminalSize,
+    HoveredWord, IndexedCell, Terminal, TerminalBounds, TerminalContent,
 };
 use theme::{ActiveTheme, Theme, ThemeSettings};
 use ui::{ParentElement, Tooltip};
@@ -40,7 +40,7 @@ pub struct LayoutState {
     relative_highlighted_ranges: Vec<(RangeInclusive<AlacPoint>, Hsla)>,
     cursor: Option<CursorLayout>,
     background_color: Hsla,
-    dimensions: TerminalSize,
+    dimensions: TerminalBounds,
     mode: TermMode,
     display_offset: usize,
     hyperlink_tooltip: Option<AnyElement>,
@@ -86,7 +86,7 @@ impl LayoutCell {
     pub fn paint(
         &self,
         origin: Point<Pixels>,
-        dimensions: &TerminalSize,
+        dimensions: &TerminalBounds,
         _visible_bounds: Bounds<Pixels>,
         window: &mut Window,
         cx: &mut App,
@@ -130,7 +130,7 @@ impl LayoutRect {
         }
     }
 
-    pub fn paint(&self, origin: Point<Pixels>, dimensions: &TerminalSize, window: &mut Window) {
+    pub fn paint(&self, origin: Point<Pixels>, dimensions: &TerminalBounds, window: &mut Window) {
         let position = {
             let alac_point = self.point;
             point(
@@ -313,7 +313,7 @@ impl TerminalElement {
     /// the same position for sequential indexes. Use em_width instead
     fn shape_cursor(
         cursor_point: DisplayCursor,
-        size: TerminalSize,
+        size: TerminalBounds,
         text_fragment: &ShapedLine,
     ) -> Option<(Point<Pixels>, Pixels)> {
         if cursor_point.line() < size.total_lines() as i32 {
@@ -412,27 +412,20 @@ impl TerminalElement {
 
     fn generic_button_handler<E>(
         connection: Entity<Terminal>,
-        origin: Point<Pixels>,
         focus_handle: FocusHandle,
-        f: impl Fn(&mut Terminal, Point<Pixels>, &E, &mut Context<Terminal>),
+        f: impl Fn(&mut Terminal, &E, &mut Context<Terminal>),
     ) -> impl Fn(&E, &mut Window, &mut App) {
         move |event, window, cx| {
             window.focus(&focus_handle);
             connection.update(cx, |terminal, cx| {
-                f(terminal, origin, event, cx);
+                f(terminal, event, cx);
 
                 cx.notify();
             })
         }
     }
 
-    fn register_mouse_listeners(
-        &mut self,
-        origin: Point<Pixels>,
-        mode: TermMode,
-        hitbox: &Hitbox,
-        window: &mut Window,
-    ) {
+    fn register_mouse_listeners(&mut self, mode: TermMode, hitbox: &Hitbox, window: &mut Window) {
         let focus = self.focus.clone();
         let terminal = self.terminal.clone();
 
@@ -442,29 +435,26 @@ impl TerminalElement {
             move |e, window, cx| {
                 window.focus(&focus);
                 terminal.update(cx, |terminal, cx| {
-                    terminal.mouse_down(e, origin, cx);
+                    terminal.mouse_down(e, cx);
                     cx.notify();
                 })
             }
         });
 
         window.on_mouse_event({
-            let focus = self.focus.clone();
             let terminal = self.terminal.clone();
             let hitbox = hitbox.clone();
+            let focus = focus.clone();
             move |e: &MouseMoveEvent, phase, window, cx| {
-                if phase != DispatchPhase::Bubble || !focus.is_focused(window) {
+                if phase != DispatchPhase::Bubble {
                     return;
                 }
 
-                if e.pressed_button.is_some() && !cx.has_active_drag() {
+                if e.pressed_button.is_some() && !cx.has_active_drag() && focus.is_focused(window) {
                     let hovered = hitbox.is_hovered(window);
                     terminal.update(cx, |terminal, cx| {
-                        if terminal.selection_started() {
-                            terminal.mouse_drag(e, origin, hitbox.bounds);
-                            cx.notify();
-                        } else if hovered {
-                            terminal.mouse_drag(e, origin, hitbox.bounds);
+                        if terminal.selection_started() || hovered {
+                            terminal.mouse_drag(e, hitbox.bounds, cx);
                             cx.notify();
                         }
                     })
@@ -472,8 +462,7 @@ impl TerminalElement {
 
                 if hitbox.is_hovered(window) {
                     terminal.update(cx, |terminal, cx| {
-                        terminal.mouse_move(e, origin);
-                        cx.notify();
+                        terminal.mouse_move(e, cx);
                     })
                 }
             }
@@ -483,10 +472,9 @@ impl TerminalElement {
             MouseButton::Left,
             TerminalElement::generic_button_handler(
                 terminal.clone(),
-                origin,
                 focus.clone(),
-                move |terminal, origin, e, cx| {
-                    terminal.mouse_up(e, origin, cx);
+                move |terminal, e, cx| {
+                    terminal.mouse_up(e, cx);
                 },
             ),
         );
@@ -494,10 +482,9 @@ impl TerminalElement {
             MouseButton::Middle,
             TerminalElement::generic_button_handler(
                 terminal.clone(),
-                origin,
                 focus.clone(),
-                move |terminal, origin, e, cx| {
-                    terminal.mouse_down(e, origin, cx);
+                move |terminal, e, cx| {
+                    terminal.mouse_down(e, cx);
                 },
             ),
         );
@@ -506,7 +493,7 @@ impl TerminalElement {
             move |e, _, cx| {
                 terminal_view
                     .update(cx, |terminal_view, cx| {
-                        terminal_view.scroll_wheel(e, origin, cx);
+                        terminal_view.scroll_wheel(e, cx);
                         cx.notify();
                     })
                     .ok();
@@ -520,10 +507,9 @@ impl TerminalElement {
                 MouseButton::Right,
                 TerminalElement::generic_button_handler(
                     terminal.clone(),
-                    origin,
                     focus.clone(),
-                    move |terminal, origin, e, cx| {
-                        terminal.mouse_down(e, origin, cx);
+                    move |terminal, e, cx| {
+                        terminal.mouse_down(e, cx);
                     },
                 ),
             );
@@ -531,23 +517,17 @@ impl TerminalElement {
                 MouseButton::Right,
                 TerminalElement::generic_button_handler(
                     terminal.clone(),
-                    origin,
                     focus.clone(),
-                    move |terminal, origin, e, cx| {
-                        terminal.mouse_up(e, origin, cx);
+                    move |terminal, e, cx| {
+                        terminal.mouse_up(e, cx);
                     },
                 ),
             );
             self.interactivity.on_mouse_up(
                 MouseButton::Middle,
-                TerminalElement::generic_button_handler(
-                    terminal,
-                    origin,
-                    focus,
-                    move |terminal, origin, e, cx| {
-                        terminal.mouse_up(e, origin, cx);
-                    },
-                ),
+                TerminalElement::generic_button_handler(terminal, focus, move |terminal, e, cx| {
+                    terminal.mouse_up(e, cx);
+                }),
             );
         }
     }
@@ -705,7 +685,10 @@ impl Element for TerminalElement {
                         size.width = cell_width * 2.0;
                     }
 
-                    TerminalSize::new(line_height, cell_width, size)
+                    let mut origin = bounds.origin;
+                    origin.x += gutter;
+
+                    TerminalBounds::new(line_height, cell_width, Bounds { origin, size })
                 };
 
                 let search_matches = self.terminal.read(cx).matches.clone();
@@ -714,9 +697,11 @@ impl Element for TerminalElement {
 
                 let last_hovered_word = self.terminal.update(cx, |terminal, cx| {
                     terminal.set_size(dimensions);
-                    terminal.sync(cx);
+                    terminal.sync(window, cx);
+
                     if self.can_navigate_to_selected_word
-                        && terminal.can_navigate_to_selected_word()
+                        && window.modifiers().secondary()
+                        && bounds.contains(&window.mouse_position())
                     {
                         terminal.last_content.last_hovered_word.clone()
                     } else {
@@ -898,7 +883,7 @@ impl Element for TerminalElement {
                 workspace: self.workspace.clone(),
             };
 
-            self.register_mouse_listeners(origin, layout.mode, &layout.hitbox, window);
+            self.register_mouse_listeners(layout.mode, &layout.hitbox, window);
             if self.can_navigate_to_selected_word && layout.last_hovered_word.is_some() {
                 window.set_cursor_style(gpui::CursorStyle::PointingHand, &layout.hitbox);
             } else {
@@ -924,12 +909,9 @@ impl Element for TerminalElement {
                                 return;
                             }
 
-                            let handled = this
-                                .update(cx, |term, _| term.try_modifiers_change(&event.modifiers));
-
-                            if handled {
-                                window.refresh();
-                            }
+                            this.update(cx, |term, cx| {
+                                term.try_modifiers_change(&event.modifiers, window, cx)
+                            });
                         }
                     });
 

crates/terminal_view/src/terminal_scrollbar.rs 🔗

@@ -19,7 +19,7 @@ struct ScrollHandleState {
 impl ScrollHandleState {
     fn new(terminal: &Terminal) -> Self {
         Self {
-            line_height: terminal.last_content().size.line_height,
+            line_height: terminal.last_content().terminal_bounds.line_height,
             total_lines: terminal.total_lines(),
             viewport_lines: terminal.viewport_lines(),
             display_offset: terminal.last_content().display_offset,

crates/terminal_view/src/terminal_view.rs 🔗

@@ -23,7 +23,7 @@ use terminal::{
     terminal_settings::{self, CursorShape, TerminalBlink, TerminalSettings, WorkingDirectory},
     Clear, Copy, Event, MaybeNavigationTarget, Paste, ScrollLineDown, ScrollLineUp, ScrollPageDown,
     ScrollPageUp, ScrollToBottom, ScrollToTop, ShowCharacterPalette, TaskStatus, Terminal,
-    TerminalSize, ToggleViMode,
+    TerminalBounds, ToggleViMode,
 };
 use terminal_element::{is_blank, TerminalElement};
 use terminal_panel::TerminalPanel;
@@ -101,7 +101,7 @@ pub struct BlockProperties {
 pub struct BlockContext<'a, 'b> {
     pub window: &'a mut Window,
     pub context: &'b mut App,
-    pub dimensions: TerminalSize,
+    pub dimensions: TerminalBounds,
 }
 
 ///A terminal view, maintains the PTY's file handles and communicates with the terminal
@@ -342,7 +342,7 @@ impl TerminalView {
             return Pixels::ZERO;
         };
 
-        let line_height = terminal.last_content().size.line_height;
+        let line_height = terminal.last_content().terminal_bounds.line_height;
         let mut terminal_lines = terminal.total_lines();
         let viewport_lines = terminal.viewport_lines();
         if terminal.total_lines() == terminal.viewport_lines() {
@@ -366,16 +366,11 @@ impl TerminalView {
         max_scroll_top_in_lines as f32 * line_height
     }
 
-    fn scroll_wheel(
-        &mut self,
-        event: &ScrollWheelEvent,
-        origin: gpui::Point<Pixels>,
-        cx: &mut Context<Self>,
-    ) {
+    fn scroll_wheel(&mut self, event: &ScrollWheelEvent, cx: &mut Context<Self>) {
         let terminal_content = self.terminal.read(cx).last_content();
 
         if self.block_below_cursor.is_some() && terminal_content.display_offset == 0 {
-            let line_height = terminal_content.size.line_height;
+            let line_height = terminal_content.terminal_bounds.line_height;
             let y_delta = event.delta.pixel_delta(line_height).y;
             if y_delta < Pixels::ZERO || self.scroll_top > Pixels::ZERO {
                 self.scroll_top = cmp::max(
@@ -387,8 +382,7 @@ impl TerminalView {
             }
         }
 
-        self.terminal
-            .update(cx, |term, _| term.scroll_wheel(event, origin));
+        self.terminal.update(cx, |term, _| term.scroll_wheel(event));
     }
 
     fn scroll_line_up(&mut self, _: &ScrollLineUp, _: &mut Window, cx: &mut Context<Self>) {
@@ -397,7 +391,7 @@ impl TerminalView {
             && terminal_content.display_offset == 0
             && self.scroll_top > Pixels::ZERO
         {
-            let line_height = terminal_content.size.line_height;
+            let line_height = terminal_content.terminal_bounds.line_height;
             self.scroll_top = cmp::max(self.scroll_top - line_height, Pixels::ZERO);
             return;
         }
@@ -411,7 +405,7 @@ impl TerminalView {
         if self.block_below_cursor.is_some() && terminal_content.display_offset == 0 {
             let max_scroll_top = self.max_scroll_top(cx);
             if self.scroll_top < max_scroll_top {
-                let line_height = terminal_content.size.line_height;
+                let line_height = terminal_content.terminal_bounds.line_height;
                 self.scroll_top = cmp::min(self.scroll_top + line_height, max_scroll_top);
             }
             return;
@@ -425,7 +419,12 @@ impl TerminalView {
         if self.scroll_top == Pixels::ZERO {
             self.terminal.update(cx, |term, _| term.scroll_page_up());
         } else {
-            let line_height = self.terminal.read(cx).last_content.size.line_height();
+            let line_height = self
+                .terminal
+                .read(cx)
+                .last_content
+                .terminal_bounds
+                .line_height();
             let visible_block_lines = (self.scroll_top / line_height) as usize;
             let viewport_lines = self.terminal.read(cx).viewport_lines();
             let visible_content_lines = viewport_lines - visible_block_lines;
@@ -866,7 +865,8 @@ fn subscribe_for_terminal_events(
                         }
                     }
                     None => false,
-                }
+                };
+                cx.notify()
             }
 
             Event::Open(maybe_navigation_target) => match maybe_navigation_target {