markdown preview: Allow clicking on image to navigate to source location (#21630)

Bennet Bo Fenner created

Follow up to #21082

Similar to checkboxes, you can now click on the image to navigate to the
source location, cmd-clicking opens the url in the browser.


https://github.com/user-attachments/assets/edaaa580-9d8f-490b-a4b3-d6ffb21f197c


Release Notes:

- N/A

Change summary

Cargo.lock                                       |  1 
crates/markdown_preview/Cargo.toml               |  1 
crates/markdown_preview/src/markdown_renderer.rs | 98 +++++++++++++----
3 files changed, 78 insertions(+), 22 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -7421,6 +7421,7 @@ dependencies = [
  "settings",
  "theme",
  "ui",
+ "util",
  "workspace",
 ]
 

crates/markdown_preview/Cargo.toml 🔗

@@ -28,6 +28,7 @@ pulldown-cmark.workspace = true
 settings.workspace = true
 theme.workspace = true
 ui.workspace = true
+util.workspace = true
 workspace.workspace = true
 
 [dev-dependencies]

crates/markdown_preview/src/markdown_renderer.rs 🔗

@@ -7,8 +7,8 @@ use crate::markdown_elements::{
 use gpui::{
     div, img, px, rems, AbsoluteLength, AnyElement, ClipboardItem, DefiniteLength, Div, Element,
     ElementId, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement, Keystroke, Length,
-    Modifiers, ParentElement, Resource, SharedString, Styled, StyledText, TextStyle, WeakView,
-    WindowContext,
+    Modifiers, ParentElement, Render, Resource, SharedString, Styled, StyledText, TextStyle, View,
+    WeakView, WindowContext,
 };
 use settings::Settings;
 use std::{
@@ -18,9 +18,10 @@ use std::{
 };
 use theme::{ActiveTheme, SyntaxTheme, ThemeSettings};
 use ui::{
-    h_flex, relative, v_flex, Checkbox, Clickable, FluentBuilder, IconButton, IconName, IconSize,
-    InteractiveElement, LinkPreview, Selection, StatefulInteractiveElement, StyledExt, StyledImage,
-    Tooltip, VisibleOnHover,
+    h_flex, relative, tooltip_container, v_flex, Checkbox, Clickable, Color, FluentBuilder,
+    IconButton, IconName, IconSize, InteractiveElement, Label, LabelCommon, LabelSize, LinkPreview,
+    Selection, StatefulInteractiveElement, StyledExt, StyledImage, ViewContext, VisibleOnHover,
+    VisualContext as _,
 };
 use workspace::Workspace;
 
@@ -206,15 +207,7 @@ fn render_markdown_list_item(
             )
             .hover(|s| s.cursor_pointer())
             .tooltip(|cx| {
-                let secondary_modifier = Keystroke {
-                    key: "".to_string(),
-                    modifiers: Modifiers::secondary_key(),
-                    key_char: None,
-                };
-                Tooltip::text(
-                    format!("{}-click to toggle the checkbox", secondary_modifier),
-                    cx,
-                )
+                InteractiveMarkdownElementTooltip::new(None, "toggle checkbox", cx).into()
             })
             .into_any_element(),
     };
@@ -513,6 +506,7 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext)
 
                 let image_element = div()
                     .id(element_id)
+                    .cursor_pointer()
                     .child(img(ImageSource::Resource(image_resource)).with_fallback({
                         let alt_text = image.alt_text.clone();
                         {
@@ -521,18 +515,31 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext)
                     }))
                     .tooltip({
                         let link = image.link.clone();
-                        move |cx| LinkPreview::new(&link.to_string(), cx)
+                        move |cx| {
+                            InteractiveMarkdownElementTooltip::new(
+                                Some(link.to_string()),
+                                "open image",
+                                cx,
+                            )
+                            .into()
+                        }
                     })
                     .on_click({
                         let workspace = workspace_clone.clone();
                         let link = image.link.clone();
-                        move |_event, window_cx| match &link {
-                            Link::Web { url } => window_cx.open_url(url),
-                            Link::Path { path, .. } => {
-                                if let Some(workspace) = &workspace {
-                                    _ = workspace.update(window_cx, |workspace, cx| {
-                                        workspace.open_abs_path(path.clone(), false, cx).detach();
-                                    });
+                        move |_, cx| {
+                            if cx.modifiers().secondary() {
+                                match &link {
+                                    Link::Web { url } => cx.open_url(url),
+                                    Link::Path { path, .. } => {
+                                        if let Some(workspace) = &workspace {
+                                            _ = workspace.update(cx, |workspace, cx| {
+                                                workspace
+                                                    .open_abs_path(path.clone(), false, cx)
+                                                    .detach();
+                                            });
+                                        }
+                                    }
                                 }
                             }
                         }
@@ -550,3 +557,50 @@ fn render_markdown_rule(cx: &mut RenderContext) -> AnyElement {
     let rule = div().w_full().h(px(2.)).bg(cx.border_color);
     div().pt_3().pb_3().child(rule).into_any()
 }
+
+struct InteractiveMarkdownElementTooltip {
+    tooltip_text: Option<SharedString>,
+    action_text: String,
+}
+
+impl InteractiveMarkdownElementTooltip {
+    pub fn new(
+        tooltip_text: Option<String>,
+        action_text: &str,
+        cx: &mut WindowContext,
+    ) -> View<Self> {
+        let tooltip_text = tooltip_text.map(|t| util::truncate_and_trailoff(&t, 50).into());
+
+        cx.new_view(|_| Self {
+            tooltip_text,
+            action_text: action_text.to_string(),
+        })
+    }
+}
+
+impl Render for InteractiveMarkdownElementTooltip {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        tooltip_container(cx, |el, _| {
+            let secondary_modifier = Keystroke {
+                modifiers: Modifiers::secondary_key(),
+                ..Default::default()
+            };
+
+            el.child(
+                v_flex()
+                    .gap_1()
+                    .when_some(self.tooltip_text.clone(), |this, text| {
+                        this.child(Label::new(text).size(LabelSize::Small))
+                    })
+                    .child(
+                        Label::new(format!(
+                            "{}-click to {}",
+                            secondary_modifier, self.action_text
+                        ))
+                        .size(LabelSize::Small)
+                        .color(Color::Muted),
+                    ),
+            )
+        })
+    }
+}