diff --git a/crates/gpui/src/elements/overlay.rs b/crates/gpui/src/elements/overlay.rs index 20b6c75c8f..d47a39e958 100644 --- a/crates/gpui/src/elements/overlay.rs +++ b/crates/gpui/src/elements/overlay.rs @@ -14,6 +14,7 @@ pub struct Overlay { anchor_position: Option, anchor_corner: AnchorCorner, fit_mode: OverlayFitMode, + position_mode: OverlayPositionMode, hoverable: bool, } @@ -24,6 +25,12 @@ pub enum OverlayFitMode { None, } +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum OverlayPositionMode { + Window, + Local, +} + #[derive(Clone, Copy, PartialEq, Eq)] pub enum AnchorCorner { TopLeft, @@ -73,6 +80,7 @@ impl Overlay { anchor_position: None, anchor_corner: AnchorCorner::TopLeft, fit_mode: OverlayFitMode::None, + position_mode: OverlayPositionMode::Window, hoverable: false, } } @@ -92,6 +100,11 @@ impl Overlay { self } + pub fn with_position_mode(mut self, position_mode: OverlayPositionMode) -> Self { + self.position_mode = position_mode; + self + } + pub fn with_hoverable(mut self, hoverable: bool) -> Self { self.hoverable = hoverable; self @@ -123,8 +136,20 @@ impl Element for Overlay { size: &mut Self::LayoutState, cx: &mut PaintContext, ) { - let anchor_position = self.anchor_position.unwrap_or_else(|| bounds.origin()); - let mut bounds = self.anchor_corner.get_bounds(anchor_position, *size); + let (anchor_position, mut bounds) = match self.position_mode { + OverlayPositionMode::Window => { + let anchor_position = self.anchor_position.unwrap_or_else(|| bounds.origin()); + let bounds = self.anchor_corner.get_bounds(anchor_position, *size); + (anchor_position, bounds) + } + OverlayPositionMode::Local => { + let anchor_position = self.anchor_position.unwrap_or_default(); + let bounds = self + .anchor_corner + .get_bounds(bounds.origin() + anchor_position, *size); + (anchor_position, bounds) + } + }; match self.fit_mode { OverlayFitMode::SnapToWindow => { diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 90671dab1a..56617001cf 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -385,6 +385,8 @@ impl TerminalBuilder { breadcrumb_text: String::new(), scroll_px: 0., last_mouse_position: None, + next_link_id: 0, + selection_phase: SelectionPhase::Ended, }; Ok(TerminalBuilder { @@ -471,7 +473,7 @@ pub struct TerminalContent { cursor: RenderableCursor, cursor_char: char, size: TerminalSize, - last_hovered_hyperlink: Option<(String, RangeInclusive)>, + last_hovered_hyperlink: Option<(String, RangeInclusive, usize)>, } impl Default for TerminalContent { @@ -493,6 +495,12 @@ impl Default for TerminalContent { } } +#[derive(PartialEq, Eq)] +pub enum SelectionPhase { + Selecting, + Ended, +} + pub struct Terminal { pty_tx: Notifier, term: Arc>>, @@ -511,6 +519,8 @@ pub struct Terminal { shell_fd: u32, foreground_process_info: Option, scroll_px: f32, + next_link_id: usize, + selection_phase: SelectionPhase, } impl Terminal { @@ -654,7 +664,7 @@ impl Terminal { } InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point), InternalEvent::Hyperlink(position, open) => { - self.last_content.last_hovered_hyperlink = None; + let prev_hyperlink = self.last_content.last_hovered_hyperlink.take(); let point = grid_point( *position, @@ -668,13 +678,37 @@ impl Terminal { if *open { open_uri(&url).log_err(); } else { - self.last_content.last_hovered_hyperlink = Some((url, url_match)); + self.update_hyperlink(prev_hyperlink, url, url_match); } } } } } + fn update_hyperlink( + &mut self, + prev_hyperlink: Option<(String, RangeInclusive, usize)>, + url: String, + url_match: RangeInclusive, + ) { + if let Some(prev_hyperlink) = prev_hyperlink { + if prev_hyperlink.0 == url && prev_hyperlink.1 == url_match { + self.last_content.last_hovered_hyperlink = Some((url, url_match, prev_hyperlink.2)); + } else { + self.last_content.last_hovered_hyperlink = + Some((url, url_match, self.next_link_id())); + } + } else { + self.last_content.last_hovered_hyperlink = Some((url, url_match, self.next_link_id())); + } + } + + fn next_link_id(&mut self) -> usize { + let res = self.next_link_id; + self.next_link_id = self.next_link_id.wrapping_add(1); + res + } + pub fn last_content(&self) -> &TerminalContent { &self.last_content } @@ -846,7 +880,8 @@ impl Terminal { } pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) { - self.last_content.last_hovered_hyperlink = None; + let prev_hyperlink = self.last_content.last_hovered_hyperlink.take(); + let position = e.position.sub(origin); self.last_mouse_position = Some(position); if self.mouse_mode(e.shift) { @@ -862,19 +897,26 @@ impl Terminal { self.pty_tx.notify(bytes); } } - } else if e.cmd { - self.fill_hyperlink(Some(position)); + } else { + self.fill_hyperlink(Some(position), prev_hyperlink); } } - fn fill_hyperlink(&mut self, position: Option) { - if let Some(position) = position { + fn fill_hyperlink( + &mut self, + position: Option, + prev_hyperlink: Option<(String, RangeInclusive, usize)>, + ) { + if self.selection_phase == SelectionPhase::Selecting { + self.last_content.last_hovered_hyperlink = None; + } else if let Some(position) = position { let content_index = content_index_for_mouse(position, &self.last_content); let link = self.last_content.cells[content_index].hyperlink(); if link.is_some() { let mut min_index = content_index; loop { - if self.last_content.cells[min_index - 1].hyperlink() == link { + if min_index >= 1 && self.last_content.cells[min_index - 1].hyperlink() == link + { min_index = min_index - 1; } else { break; @@ -882,21 +924,24 @@ impl Terminal { } let mut max_index = content_index; + let len = self.last_content.cells.len(); loop { - if self.last_content.cells[max_index + 1].hyperlink() == link { + if max_index < len - 1 + && self.last_content.cells[max_index + 1].hyperlink() == link + { max_index = max_index + 1; } else { break; } } - self.last_content.last_hovered_hyperlink = link.map(|link| { - ( - link.uri().to_owned(), - self.last_content.cells[min_index].point - ..=self.last_content.cells[max_index].point, - ) - }); + if let Some(link) = link { + let url = link.uri().to_owned(); + let url_match = self.last_content.cells[min_index].point + ..=self.last_content.cells[max_index].point; + + self.update_hyperlink(prev_hyperlink, url, url_match); + }; } else { self.events .push_back(InternalEvent::Hyperlink(position, false)); @@ -909,6 +954,7 @@ impl Terminal { self.last_mouse_position = Some(position); if !self.mouse_mode(e.shift) { + self.selection_phase = SelectionPhase::Selecting; // Alacritty has the same ordering, of first updating the selection // then scrolling 15ms later self.events @@ -969,7 +1015,9 @@ impl Terminal { pub fn left_click(&mut self, e: &ClickRegionEvent, origin: Vector2F) { let position = e.position.sub(origin); if !self.mouse_mode(e.shift) { - if e.cmd { + if self.last_content.last_hovered_hyperlink.is_some() + && self.last_content.selection.is_none() + { let mouse_cell_index = content_index_for_mouse(position, &self.last_content); if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() { open_uri(link.uri()).log_err(); @@ -1021,6 +1069,7 @@ impl Terminal { // so let's do that here self.copy(); } + self.selection_phase = SelectionPhase::Ended; self.last_mouse = None; } @@ -1061,10 +1110,10 @@ impl Terminal { } pub fn refresh_hyperlink(&mut self, cmd: bool) -> bool { - self.last_content.last_hovered_hyperlink = None; + let prev_hyperlink = self.last_content.last_hovered_hyperlink.take(); if cmd { - self.fill_hyperlink(self.last_mouse_position); + self.fill_hyperlink(self.last_mouse_position, prev_hyperlink); true } else { false diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 9c47e33da3..10d6d279c4 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -7,7 +7,7 @@ use alacritty_terminal::{ use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine}; use gpui::{ color::Color, - elements::{Overlay, Tooltip}, + elements::{Empty, Overlay}, fonts::{HighlightStyle, Properties, Style::Italic, TextStyle, Underline, Weight}, geometry::{ rect::RectF, @@ -15,7 +15,7 @@ use gpui::{ }, serde_json::json, text_layout::{Line, RunStyle}, - Axis, Element, ElementBox, Event, EventContext, FontCache, KeyDownEvent, ModelContext, + Element, ElementBox, Event, EventContext, FontCache, KeyDownEvent, ModelContext, ModifiersChangedEvent, MouseButton, MouseRegion, PaintContext, Quad, SizeConstraint, TextLayoutCache, WeakModelHandle, WeakViewHandle, }; @@ -324,7 +324,6 @@ impl TerminalElement { .unwrap_or_default(); if indexed.cell.hyperlink().is_some() { - underline.squiggly = true; if underline.thickness == OrderedFloat(0.) { underline.thickness = OrderedFloat(1.); } @@ -594,51 +593,34 @@ impl Element for TerminalElement { }; let terminal_handle = self.terminal.upgrade(cx).unwrap(); - let (last_hovered_hyperlink, last_mouse) = - terminal_handle.update(cx.app, |terminal, cx| { - terminal.set_size(dimensions); - terminal.try_sync(cx); - ( - terminal.last_content.last_hovered_hyperlink.clone(), - terminal.last_mouse_position, - ) - }); + let last_hovered_hyperlink = terminal_handle.update(cx.app, |terminal, cx| { + terminal.set_size(dimensions); + terminal.try_sync(cx); + terminal.last_content.last_hovered_hyperlink.clone() + }); let view_handle = self.view.clone(); - let hyperlink_tooltip = last_hovered_hyperlink.and_then(|(uri, _)| { - last_mouse.and_then(|last_mouse| { - view_handle.upgrade(cx).map(|handle| { - let mut tooltip = cx.render(&handle, |_, cx| { - // TODO: Use the correct dynamic line height - // let mut collapsed_tooltip = Tooltip::render_tooltip( - // uri.clone(), - // tooltip_style.clone(), - // None, - // false, - // ) - // .boxed(); + let hyperlink_tooltip = last_hovered_hyperlink.and_then(|(uri, _, id)| { + // last_mouse.and_then(|_last_mouse| { + view_handle.upgrade(cx).map(|handle| { + let mut tooltip = cx.render(&handle, |_, cx| { + Overlay::new( + Empty::new() + .contained() + .constrained() + .with_width(dimensions.width()) + .with_height(dimensions.height()) + .with_tooltip::(id, uri, None, tooltip_style, cx) + .boxed(), + ) + .with_position_mode(gpui::elements::OverlayPositionMode::Local) + .boxed() + }); - Overlay::new( - Tooltip::render_tooltip(uri, tooltip_style, None, false) - .constrained() - .with_height(text_style.line_height(cx.font_cache())) - // .dynamically(move |constraint, cx| { - // SizeConstraint::strict_along( - // Axis::Vertical, - // collapsed_tooltip.layout(constraint, cx).y(), - // ) - // }) - .boxed(), - ) - .with_fit_mode(gpui::elements::OverlayFitMode::SwitchAnchor) - .with_anchor_position(last_mouse) - .boxed() - }); - - tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx); - tooltip - }) + tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx); + tooltip }) + // }) }); let TerminalContent { @@ -672,7 +654,7 @@ impl Element for TerminalElement { self.modal, last_hovered_hyperlink .as_ref() - .map(|(_, range)| (link_style, range)), + .map(|(_, range, _)| (link_style, range)), ); //Layout cursor. Rectangle is used for IME, so we should lay it out even @@ -822,12 +804,7 @@ impl Element for TerminalElement { } if let Some(element) = &mut layout.hyperlink_tooltip { - element.paint( - visible_bounds.lower_left() - - vec2f(-layout.size.cell_width, layout.size.line_height), - visible_bounds, - cx, - ) + element.paint(origin, visible_bounds, cx) } }); }