WIP, almost done with tooltips

Mikayla Maki created

Change summary

crates/gpui/src/elements/tooltip.rs     |   8 
crates/terminal/src/terminal.rs         |  27 +++++
crates/terminal/src/terminal_element.rs | 117 ++++++++++++++++++++++++--
crates/terminal/src/terminal_view.rs    |   4 
4 files changed, 138 insertions(+), 18 deletions(-)

Detailed changes

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<Box<dyn Action>>,

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<FairMutex<Term<ZedListener>>>,
     events: VecDeque<InternalEvent>,
+    /// 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<Vector2F>,
     pub matches: Vec<RangeInclusive<Point>>,
     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<Vector2F>) {
+        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,

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<ElementBox>,
 }
 
 ///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<Point>)>,
     ) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
         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<Point>)>,
     ) -> 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<E>(
@@ -373,7 +393,7 @@ impl TerminalElement {
     ) {
         let connection = self.terminal;
 
-        let mut region = MouseRegion::new::<Self>(view_id, view_id, visible_bounds);
+        let mut region = MouseRegion::new::<Self>(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
         }

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>) {
-        self.terminal.read(cx).focus_out();
+        self.terminal.update(cx, |terminal, _| {
+            terminal.focus_out();
+        });
         cx.notify();
     }