diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 55ab7e44d2..c86230a5e1 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -36,10 +36,10 @@ struct TooltipState { #[derive(Clone, Deserialize, Default)] pub struct TooltipStyle { #[serde(flatten)] - container: ContainerStyle, - text: TextStyle, + pub container: ContainerStyle, + pub text: TextStyle, keystroke: KeystrokeStyle, - max_text_width: f32, + pub max_text_width: f32, } #[derive(Clone, Deserialize, Default)] @@ -126,7 +126,7 @@ impl Tooltip { } } - fn render_tooltip( + pub fn render_tooltip( text: String, style: TooltipStyle, action: Option>, diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 6dab9e60af..90671dab1a 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -384,6 +384,7 @@ impl TerminalBuilder { foreground_process_info: None, breadcrumb_text: String::new(), scroll_px: 0., + last_mouse_position: None, }; Ok(TerminalBuilder { @@ -496,7 +497,10 @@ pub struct Terminal { pty_tx: Notifier, term: Arc>>, events: VecDeque, + /// This is only used for mouse mode cell change detection last_mouse: Option<(Point, AlacDirection)>, + /// This is only used for terminal hyperlink checking + last_mouse_position: Option, pub matches: Vec>, last_content: TerminalContent, last_synced: Instant, @@ -813,7 +817,8 @@ impl Terminal { } } - pub fn focus_out(&self) { + 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()); } @@ -843,6 +848,7 @@ impl Terminal { pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) { self.last_content.last_hovered_hyperlink = None; let position = e.position.sub(origin); + self.last_mouse_position = Some(position); if self.mouse_mode(e.shift) { let point = grid_point( position, @@ -857,9 +863,14 @@ impl Terminal { } } } else if e.cmd { + self.fill_hyperlink(Some(position)); + } + } + + fn fill_hyperlink(&mut self, position: Option) { + 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 { @@ -895,6 +906,7 @@ impl Terminal { pub fn mouse_drag(&mut self, e: DragRegionEvent, origin: Vector2F) { let position = e.position.sub(origin); + self.last_mouse_position = Some(position); if !self.mouse_mode(e.shift) { // Alacritty has the same ordering, of first updating the selection @@ -1048,6 +1060,17 @@ impl Terminal { } } + pub fn refresh_hyperlink(&mut self, cmd: bool) -> bool { + self.last_content.last_hovered_hyperlink = None; + + if cmd { + self.fill_hyperlink(self.last_mouse_position); + true + } else { + false + } + } + fn determine_scroll_lines( &mut self, e: &ScrollWheelRegionEvent, diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index b66db8b440..9c47e33da3 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -7,15 +7,17 @@ use alacritty_terminal::{ use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine}; use gpui::{ color::Color, - fonts::{Properties, Style::Italic, TextStyle, Underline, Weight}, + elements::{Overlay, Tooltip}, + fonts::{HighlightStyle, Properties, Style::Italic, TextStyle, Underline, Weight}, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, }, serde_json::json, text_layout::{Line, RunStyle}, - Element, Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton, MouseRegion, - PaintContext, Quad, TextLayoutCache, WeakModelHandle, WeakViewHandle, + Axis, Element, ElementBox, Event, EventContext, FontCache, KeyDownEvent, ModelContext, + ModifiersChangedEvent, MouseButton, MouseRegion, PaintContext, Quad, SizeConstraint, + TextLayoutCache, WeakModelHandle, WeakViewHandle, }; use itertools::Itertools; use ordered_float::OrderedFloat; @@ -42,6 +44,7 @@ pub struct LayoutState { size: TerminalSize, mode: TermMode, display_offset: usize, + hyperlink_tooltip: Option, } ///Helper struct for converting data between alacritty's cursor points, and displayed cursor points @@ -180,6 +183,7 @@ impl TerminalElement { text_layout_cache: &TextLayoutCache, font_cache: &FontCache, modal: bool, + hyperlink: Option<(HighlightStyle, &RangeInclusive)>, ) -> (Vec, Vec) { let mut cells = vec![]; let mut rects = vec![]; @@ -245,6 +249,7 @@ impl TerminalElement { text_style, font_cache, modal, + hyperlink, ); let layout_cell = text_layout_cache.layout_str( @@ -304,6 +309,7 @@ impl TerminalElement { text_style: &TextStyle, font_cache: &FontCache, modal: bool, + hyperlink: Option<(HighlightStyle, &RangeInclusive)>, ) -> RunStyle { let flags = indexed.cell.flags; let fg = convert_color(&fg, &style.colors, modal); @@ -339,11 +345,25 @@ impl TerminalElement { .select_font(text_style.font_family_id, &properties) .unwrap_or(text_style.font_id); - RunStyle { + let mut result = RunStyle { color: fg, font_id, underline, + }; + + if let Some((style, range)) = hyperlink { + if range.contains(&indexed.point) { + if let Some(underline) = style.underline { + result.underline = underline; + } + + if let Some(color) = style.color { + result.color = color; + } + } } + + result } fn generic_button_handler( @@ -373,7 +393,7 @@ impl TerminalElement { ) { let connection = self.terminal; - let mut region = MouseRegion::new::(view_id, view_id, visible_bounds); + let mut region = MouseRegion::new::(view_id, 0, visible_bounds); // Terminal Emulator controlled behavior: region = region @@ -549,6 +569,9 @@ impl Element for TerminalElement { //Setup layout information let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone. + let link_style = settings.theme.editor.link_definition; + let tooltip_style = settings.theme.tooltip.clone(); + let text_style = TerminalElement::make_text_style(font_cache, settings); let selection_color = settings.theme.editor.selection.selection; let match_color = settings.theme.search.match_background; @@ -571,9 +594,51 @@ impl Element for TerminalElement { }; let terminal_handle = self.terminal.upgrade(cx).unwrap(); - terminal_handle.update(cx.app, |terminal, cx| { - terminal.set_size(dimensions); - terminal.try_sync(cx) + 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 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(); + + 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 + }) + }) }); let TerminalContent { @@ -585,7 +650,7 @@ impl Element for TerminalElement { cursor, last_hovered_hyperlink, .. - } = &terminal_handle.read(cx).last_content; + } = { &terminal_handle.read(cx).last_content }; // searches, highlights to a single range representations let mut relative_highlighted_ranges = Vec::new(); @@ -605,6 +670,9 @@ impl Element for TerminalElement { cx.text_layout_cache, cx.font_cache(), self.modal, + last_hovered_hyperlink + .as_ref() + .map(|(_, range)| (link_style, range)), ); //Layout cursor. Rectangle is used for IME, so we should lay it out even @@ -636,10 +704,11 @@ impl Element for TerminalElement { ) }; + let focused = self.focused; TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map( move |(cursor_position, block_width)| { let shape = match cursor.shape { - AlacCursorShape::Block if !self.focused => CursorShape::Hollow, + AlacCursorShape::Block if !focused => CursorShape::Hollow, AlacCursorShape::Block => CursorShape::Block, AlacCursorShape::Underline => CursorShape::Underscore, AlacCursorShape::Beam => CursorShape::Bar, @@ -672,6 +741,7 @@ impl Element for TerminalElement { relative_highlighted_ranges, mode: *mode, display_offset: *display_offset, + hyperlink_tooltip, }, ) } @@ -694,7 +764,11 @@ impl Element for TerminalElement { cx.scene.push_cursor_region(gpui::CursorRegion { bounds, - style: gpui::CursorStyle::IBeam, + style: if layout.hyperlink_tooltip.is_some() { + gpui::CursorStyle::PointingHand + } else { + gpui::CursorStyle::IBeam + }, }); cx.paint_layer(clip_bounds, |cx| { @@ -746,6 +820,15 @@ 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, + ) + } }); } @@ -784,6 +867,18 @@ impl Element for TerminalElement { }) }) .unwrap_or(false) + } else if let Event::ModifiersChanged(ModifiersChangedEvent { cmd, .. }) = event { + self.terminal + .upgrade(cx.app) + .map(|model_handle| { + if model_handle.update(cx.app, |term, _| term.refresh_hyperlink(*cmd)) { + cx.notify(); + true + } else { + false + } + }) + .unwrap_or(false) } else { false } diff --git a/crates/terminal/src/terminal_view.rs b/crates/terminal/src/terminal_view.rs index fc8bf20ca7..33d573a76a 100644 --- a/crates/terminal/src/terminal_view.rs +++ b/crates/terminal/src/terminal_view.rs @@ -362,7 +362,9 @@ impl View for TerminalView { } fn on_focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - self.terminal.read(cx).focus_out(); + self.terminal.update(cx, |terminal, _| { + terminal.focus_out(); + }); cx.notify(); }