Skip to main content

ui/
tooltip.rs

1//! [`Tooltip`] — the small hover label.
2//!
3//! An entity rather than a function because gpui's `.tooltip(..)` takes a
4//! builder returning an `AnyView`: the tooltip is mounted in its own layer,
5//! after the hover delay, so it cannot be an inline element.
6//!
7//! ```ignore
8//! use ui::tooltip::Tooltip;
9//!
10//! div()
11//!     .id("copy")
12//!     .tooltip(|window, cx| Tooltip::text("Copy path", window, cx))
13//!     .child("⌘C")
14//! ```
15
16use gpui::{Action, AnyView, App, Context, IntoElement, SharedString, Window, div, prelude::*, px};
17
18use theme::{TextStyle, Theme, Typeset};
19
20use crate::{keys, popover, surface::Surfaced as _};
21
22pub struct Tooltip {
23    text: SharedString,
24    /// Optional keystroke shown right-aligned, e.g. `⌘C`.
25    keystroke: Option<SharedString>,
26}
27
28impl Tooltip {
29    /// A plain text tooltip, built for `.tooltip(..)`.
30    pub fn text(text: impl Into<SharedString>, _window: &mut Window, cx: &mut App) -> AnyView {
31        let text = text.into();
32        cx.new(|_| Self {
33            text,
34            keystroke: None,
35        })
36        .into()
37    }
38
39    /// A tooltip that also names the shortcut — the pairing that keeps
40    /// keyboard affordances discoverable without a menu.
41    pub fn with_keystroke(
42        text: impl Into<SharedString>,
43        keystroke: impl Into<SharedString>,
44        _window: &mut Window,
45        cx: &mut App,
46    ) -> AnyView {
47        let (text, keystroke) = (text.into(), keystroke.into());
48        cx.new(|_| Self {
49            text,
50            keystroke: Some(keystroke),
51        })
52        .into()
53    }
54
55    /// The same pairing with the chord read off the keymap rather than typed
56    /// in, so rebinding the action moves the hint with it. Falls back to plain
57    /// text when nothing is bound.
58    ///
59    /// A tooltip is built on hover, while the surface it describes still holds
60    /// focus, so [`keys::shortcut`] is the right lookup — use
61    /// [`Tooltip::for_action_in`] where it is not, such as a button that moves
62    /// focus to itself.
63    pub fn for_action(
64        text: impl Into<SharedString>,
65        action: &dyn Action,
66        window: &mut Window,
67        cx: &mut App,
68    ) -> AnyView {
69        let text = text.into();
70        let keystroke = keys::shortcut(action, window);
71        cx.new(|_| Self { text, keystroke }).into()
72    }
73
74    /// [`Tooltip::for_action`] against a named key context instead of whatever
75    /// holds focus.
76    pub fn for_action_in(
77        text: impl Into<SharedString>,
78        action: &dyn Action,
79        context: &str,
80        window: &mut Window,
81        cx: &mut App,
82    ) -> AnyView {
83        let text = text.into();
84        let keystroke = keys::shortcut_in(action, context, window);
85        cx.new(|_| Self { text, keystroke }).into()
86    }
87}
88
89impl Render for Tooltip {
90    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
91        let theme = Theme::of(cx).clone();
92        // Tooltips are small and frequent, so this is a tighter card than
93        // `popover_card`: less padding, no menu rhythm.
94        popover::popover_card(&theme)
95            .px(px(8.0))
96            .py(px(5.0))
97            .flex()
98            .flex_row()
99            .items_center()
100            .gap(px(Theme::SPACE))
101            .text_style(TextStyle::Callout)
102            .text_color(theme.text)
103            .child(self.text.clone())
104            .when_some(self.keystroke.clone(), |card, keystroke| {
105                card.child(
106                    div()
107                        .text_style(TextStyle::Subheadline)
108                        .text_color(theme.text_faint)
109                        .child(keystroke),
110                )
111            })
112            .surface(&theme, theme.popover_surface)
113    }
114}