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