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