keymap_ui: Hover tooltip for context (#34290)

Ben Kunkle and Anthony created

Closes #ISSUE

Ideally the tooltip would only appear if the context was overflowing
it's column, but for now, we just unconditionally show a tooltip so that
long contexts can be seen.

This PR also includes a change to the tooltip element, allowing for
tooltips with non-text contents which is used here for syntax
highlighting

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Co-authored-by: Anthony <anthony@zed.dev>

Change summary

crates/settings_ui/src/keybindings.rs | 24 +++++++--
crates/ui/src/components/tooltip.rs   | 72 +++++++++++++++++++++++-----
2 files changed, 77 insertions(+), 19 deletions(-)

Detailed changes

crates/settings_ui/src/keybindings.rs 🔗

@@ -1015,12 +1015,24 @@ impl Render for KeymapEditor {
                                             }
                                         }
                                     };
-                                    let context = binding
-                                        .context
-                                        .clone()
-                                        .map_or(gpui::Empty.into_any_element(), |context| {
-                                            context.into_any_element()
-                                        });
+                                    let context = binding.context.clone().map_or(
+                                        gpui::Empty.into_any_element(),
+                                        |context| {
+                                            let is_local = context.local().is_some();
+
+                                            div()
+                                                .id(("keymap context", index))
+                                                .child(context.clone())
+                                                .when(is_local, |this| {
+                                                    this.tooltip(Tooltip::element({
+                                                        move |_, _| {
+                                                            context.clone().into_any_element()
+                                                        }
+                                                    }))
+                                                })
+                                                .into_any_element()
+                                        },
+                                    );
                                     let source = binding
                                         .source
                                         .clone()

crates/ui/src/components/tooltip.rs 🔗

@@ -1,3 +1,5 @@
+use std::rc::Rc;
+
 use gpui::{Action, AnyElement, AnyView, AppContext as _, FocusHandle, IntoElement, Render};
 use settings::Settings;
 use theme::ThemeSettings;
@@ -7,15 +9,36 @@ use crate::{Color, KeyBinding, Label, LabelSize, StyledExt, h_flex, v_flex};
 
 #[derive(RegisterComponent)]
 pub struct Tooltip {
-    title: SharedString,
+    title: Title,
     meta: Option<SharedString>,
     key_binding: Option<KeyBinding>,
 }
 
+#[derive(Clone, IntoElement)]
+enum Title {
+    Str(SharedString),
+    Callback(Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>),
+}
+
+impl From<SharedString> for Title {
+    fn from(value: SharedString) -> Self {
+        Title::Str(value)
+    }
+}
+
+impl RenderOnce for Title {
+    fn render(self, window: &mut Window, cx: &mut App) -> impl gpui::IntoElement {
+        match self {
+            Title::Str(title) => title.into_any_element(),
+            Title::Callback(element) => element(window, cx),
+        }
+    }
+}
+
 impl Tooltip {
     pub fn simple(title: impl Into<SharedString>, cx: &mut App) -> AnyView {
         cx.new(|_| Self {
-            title: title.into(),
+            title: Title::Str(title.into()),
             meta: None,
             key_binding: None,
         })
@@ -26,7 +49,7 @@ impl Tooltip {
         let title = title.into();
         move |_, cx| {
             cx.new(|_| Self {
-                title: title.clone(),
+                title: title.clone().into(),
                 meta: None,
                 key_binding: None,
             })
@@ -34,15 +57,15 @@ impl Tooltip {
         }
     }
 
-    pub fn for_action_title<Title: Into<SharedString>>(
-        title: Title,
+    pub fn for_action_title<T: Into<SharedString>>(
+        title: T,
         action: &dyn Action,
-    ) -> impl Fn(&mut Window, &mut App) -> AnyView + use<Title> {
+    ) -> impl Fn(&mut Window, &mut App) -> AnyView + use<T> {
         let title = title.into();
         let action = action.boxed_clone();
         move |window, cx| {
             cx.new(|cx| Self {
-                title: title.clone(),
+                title: Title::Str(title.clone()),
                 meta: None,
                 key_binding: KeyBinding::for_action(action.as_ref(), window, cx),
             })
@@ -60,7 +83,7 @@ impl Tooltip {
         let focus_handle = focus_handle.clone();
         move |window, cx| {
             cx.new(|cx| Self {
-                title: title.clone(),
+                title: Title::Str(title.clone()),
                 meta: None,
                 key_binding: KeyBinding::for_action_in(action.as_ref(), &focus_handle, window, cx),
             })
@@ -75,7 +98,7 @@ impl Tooltip {
         cx: &mut App,
     ) -> AnyView {
         cx.new(|cx| Self {
-            title: title.into(),
+            title: Title::Str(title.into()),
             meta: None,
             key_binding: KeyBinding::for_action(action, window, cx),
         })
@@ -90,7 +113,7 @@ impl Tooltip {
         cx: &mut App,
     ) -> AnyView {
         cx.new(|cx| Self {
-            title: title.into(),
+            title: title.into().into(),
             meta: None,
             key_binding: KeyBinding::for_action_in(action, focus_handle, window, cx),
         })
@@ -105,7 +128,7 @@ impl Tooltip {
         cx: &mut App,
     ) -> AnyView {
         cx.new(|cx| Self {
-            title: title.into(),
+            title: title.into().into(),
             meta: Some(meta.into()),
             key_binding: action.and_then(|action| KeyBinding::for_action(action, window, cx)),
         })
@@ -121,7 +144,7 @@ impl Tooltip {
         cx: &mut App,
     ) -> AnyView {
         cx.new(|cx| Self {
-            title: title.into(),
+            title: title.into().into(),
             meta: Some(meta.into()),
             key_binding: action
                 .and_then(|action| KeyBinding::for_action_in(action, focus_handle, window, cx)),
@@ -131,12 +154,35 @@ impl Tooltip {
 
     pub fn new(title: impl Into<SharedString>) -> Self {
         Self {
-            title: title.into(),
+            title: title.into().into(),
             meta: None,
             key_binding: None,
         }
     }
 
+    pub fn new_element(title: impl Fn(&mut Window, &mut App) -> AnyElement + 'static) -> Self {
+        Self {
+            title: Title::Callback(Rc::new(title)),
+            meta: None,
+            key_binding: None,
+        }
+    }
+
+    pub fn element(
+        title: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
+    ) -> impl Fn(&mut Window, &mut App) -> AnyView {
+        let title = Title::Callback(Rc::new(title));
+        move |_, cx| {
+            let title = title.clone();
+            cx.new(|_| Self {
+                title: title,
+                meta: None,
+                key_binding: None,
+            })
+            .into()
+        }
+    }
+
     pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
         self.meta = Some(meta.into());
         self