Simplify usage of tooltip

Antonio Scandurra created

Now you simply specify a text, an action and a style and GPUI will
take of rendering it properly. This is simpler compared to always
providing a custom element and should make tooltip more consistent
across the UI.

Change summary

crates/diagnostics/src/diagnostics.rs | 23 ------
crates/gpui/src/elements.rs           |  8 +
crates/gpui/src/elements/tooltip.rs   | 89 ++++++++++++++++++++++++++--
crates/theme/src/theme.rs             |  4 
styles/src/styleTree/tooltip.ts       | 10 ++
5 files changed, 99 insertions(+), 35 deletions(-)

Detailed changes

crates/diagnostics/src/diagnostics.rs 🔗

@@ -686,26 +686,9 @@ fn diagnostic_header_renderer(entry: DiagnosticEntry<Point>, path: ProjectPath)
                 })
                 .with_tooltip(
                     entry.diagnostic.group_id,
-                    Flex::row()
-                        .with_child(
-                            Label::new(
-                                "Jump to diagnostic (".to_string(),
-                                tooltip_style.text.clone(),
-                            )
-                            .boxed(),
-                        )
-                        .with_child(
-                            KeystrokeLabel::new(
-                                Box::new(editor::OpenExcerpts),
-                                Default::default(),
-                                tooltip_style.text.clone(),
-                            )
-                            .boxed(),
-                        )
-                        .with_child(Label::new(")".to_string(), tooltip_style.text).boxed())
-                        .contained()
-                        .with_style(tooltip_style.container)
-                        .boxed(),
+                    "Jump to diagnostic".to_string(),
+                    Some(Box::new(editor::OpenExcerpts)),
+                    tooltip_style,
                     cx,
                 )
                 .aligned()

crates/gpui/src/elements.rs 🔗

@@ -31,7 +31,7 @@ use crate::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
-    json, DebugContext, Event, EventContext, LayoutContext, PaintContext, RenderContext,
+    json, Action, DebugContext, Event, EventContext, LayoutContext, PaintContext, RenderContext,
     SizeConstraint, View,
 };
 use core::panic;
@@ -160,13 +160,15 @@ pub trait Element {
     fn with_tooltip<T: View>(
         self,
         id: usize,
-        tooltip: ElementBox,
+        text: String,
+        action: Option<Box<dyn Action>>,
+        style: TooltipStyle,
         cx: &mut RenderContext<T>,
     ) -> Tooltip
     where
         Self: 'static + Sized,
     {
-        Tooltip::new(id, self.boxed(), tooltip, cx)
+        Tooltip::new(id, text, action, style, self.boxed(), cx)
     }
 }
 

crates/gpui/src/elements/tooltip.rs 🔗

@@ -1,14 +1,19 @@
-use std::{
-    cell::{Cell, RefCell},
-    rc::Rc,
-    time::Duration,
+use super::{
+    ContainerStyle, Element, ElementBox, Flex, KeystrokeLabel, MouseEventHandler, ParentElement,
+    Text,
 };
-
-use super::{Element, ElementBox, MouseEventHandler};
 use crate::{
+    fonts::TextStyle,
     geometry::{rect::RectF, vector::Vector2F},
     json::json,
-    ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint, Task, View,
+    Action, Axis, ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint,
+    Task, View,
+};
+use serde::Deserialize;
+use std::{
+    cell::{Cell, RefCell},
+    rc::Rc,
+    time::Duration,
 };
 
 const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500);
@@ -26,17 +31,53 @@ struct TooltipState {
     debounce: RefCell<Option<Task<()>>>,
 }
 
+#[derive(Clone, Deserialize, Default)]
+pub struct TooltipStyle {
+    #[serde(flatten)]
+    container: ContainerStyle,
+    text: TextStyle,
+    keystroke: KeystrokeStyle,
+    max_text_width: f32,
+}
+
+#[derive(Clone, Deserialize, Default)]
+pub struct KeystrokeStyle {
+    #[serde(flatten)]
+    container: ContainerStyle,
+    #[serde(flatten)]
+    text: TextStyle,
+}
+
 impl Tooltip {
     pub fn new<T: View>(
         id: usize,
+        text: String,
+        action: Option<Box<dyn Action>>,
+        style: TooltipStyle,
         child: ElementBox,
-        tooltip: ElementBox,
         cx: &mut RenderContext<T>,
     ) -> Self {
         let state_handle = cx.element_state::<TooltipState, Rc<TooltipState>>(id);
         let state = state_handle.read(cx).clone();
         let tooltip = if state.visible.get() {
-            Some(tooltip)
+            let mut collapsed_tooltip = Self::render_tooltip(
+                text.clone(),
+                style.clone(),
+                action.as_ref().map(|a| a.boxed_clone()),
+                true,
+            )
+            .boxed();
+            Some(
+                Self::render_tooltip(text, style, action, false)
+                    .constrained()
+                    .dynamically(move |constraint, cx| {
+                        SizeConstraint::strict_along(
+                            Axis::Vertical,
+                            collapsed_tooltip.layout(constraint, cx).y(),
+                        )
+                    })
+                    .boxed(),
+            )
         } else {
             None
         };
@@ -73,6 +114,36 @@ impl Tooltip {
             state: state_handle,
         }
     }
+
+    fn render_tooltip(
+        text: String,
+        style: TooltipStyle,
+        action: Option<Box<dyn Action>>,
+        measure: bool,
+    ) -> impl Element {
+        Flex::row()
+            .with_child({
+                let text = Text::new(text, style.text)
+                    .constrained()
+                    .with_max_width(style.max_text_width);
+                if measure {
+                    text.flex(1., false).boxed()
+                } else {
+                    text.flex(1., false).aligned().boxed()
+                }
+            })
+            .with_children(action.map(|action| {
+                let keystroke_label =
+                    KeystrokeLabel::new(action, style.keystroke.container, style.keystroke.text);
+                if measure {
+                    keystroke_label.boxed()
+                } else {
+                    keystroke_label.aligned().boxed()
+                }
+            }))
+            .contained()
+            .with_style(style.container)
+    }
 }
 
 impl Element for Tooltip {

crates/theme/src/theme.rs 🔗

@@ -2,7 +2,7 @@ mod theme_registry;
 
 use gpui::{
     color::Color,
-    elements::{ContainerStyle, ImageStyle, LabelStyle},
+    elements::{ContainerStyle, ImageStyle, LabelStyle, TooltipStyle},
     fonts::{HighlightStyle, TextStyle},
     Border, MouseState,
 };
@@ -31,7 +31,7 @@ pub struct Theme {
     pub project_diagnostics: ProjectDiagnostics,
     pub breadcrumbs: ContainedText,
     pub contact_notification: ContactNotification,
-    pub tooltip: ContainedText,
+    pub tooltip: TooltipStyle,
 }
 
 #[derive(Deserialize, Default)]

styles/src/styleTree/tooltip.ts 🔗

@@ -9,6 +9,14 @@ export default function tooltip(theme: Theme) {
     margin: { top: 6, left: 6 },
     shadow: shadow(theme),
     cornerRadius: 6,
-    ...text(theme, "sans", "secondary", { size: "xs", weight: "bold" })
+    text: text(theme, "sans", "secondary", { size: "xs", weight: "bold" }),
+    keystroke: {
+      background: backgroundColor(theme, "on500"),
+      cornerRadius: 4,
+      margin: { left: 6 },
+      padding: { left: 3, right: 3 },
+      ...text(theme, "mono", "muted", { size: "xs", weight: "bold" })
+    },
+    maxTextWidth: 200,
   }
 }