1use std::rc::Rc;
2
3use gpui::{Action, AnyElement, AnyView, AppContext as _, FocusHandle, IntoElement, Render};
4use settings::Settings;
5use theme::ThemeSettings;
6
7use crate::prelude::*;
8use crate::{Color, KeyBinding, Label, LabelSize, StyledExt, h_flex, v_flex};
9
10#[derive(RegisterComponent)]
11pub struct Tooltip {
12 title: Title,
13 meta: Option<SharedString>,
14 key_binding: Option<KeyBinding>,
15}
16
17#[derive(Clone, IntoElement)]
18enum Title {
19 Str(SharedString),
20 Callback(Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>),
21}
22
23impl From<SharedString> for Title {
24 fn from(value: SharedString) -> Self {
25 Title::Str(value)
26 }
27}
28
29impl RenderOnce for Title {
30 fn render(self, window: &mut Window, cx: &mut App) -> impl gpui::IntoElement {
31 match self {
32 Title::Str(title) => title.into_any_element(),
33 Title::Callback(element) => element(window, cx),
34 }
35 }
36}
37
38impl Tooltip {
39 pub fn simple(title: impl Into<SharedString>, cx: &mut App) -> AnyView {
40 cx.new(|_| Self {
41 title: Title::Str(title.into()),
42 meta: None,
43 key_binding: None,
44 })
45 .into()
46 }
47
48 pub fn text(title: impl Into<SharedString>) -> impl Fn(&mut Window, &mut App) -> AnyView {
49 let title = title.into();
50 move |_, cx| {
51 cx.new(|_| Self {
52 title: title.clone().into(),
53 meta: None,
54 key_binding: None,
55 })
56 .into()
57 }
58 }
59
60 pub fn for_action_title<T: Into<SharedString>>(
61 title: T,
62 action: &dyn Action,
63 ) -> impl Fn(&mut Window, &mut App) -> AnyView + use<T> {
64 let title = title.into();
65 let action = action.boxed_clone();
66 move |window, cx| {
67 cx.new(|cx| Self {
68 title: Title::Str(title.clone()),
69 meta: None,
70 key_binding: KeyBinding::for_action(action.as_ref(), window, cx),
71 })
72 .into()
73 }
74 }
75
76 pub fn for_action_title_in<Str: Into<SharedString>>(
77 title: Str,
78 action: &dyn Action,
79 focus_handle: &FocusHandle,
80 ) -> impl Fn(&mut Window, &mut App) -> AnyView + use<Str> {
81 let title = title.into();
82 let action = action.boxed_clone();
83 let focus_handle = focus_handle.clone();
84 move |window, cx| {
85 cx.new(|cx| Self {
86 title: Title::Str(title.clone()),
87 meta: None,
88 key_binding: KeyBinding::for_action_in(action.as_ref(), &focus_handle, window, cx),
89 })
90 .into()
91 }
92 }
93
94 pub fn for_action(
95 title: impl Into<SharedString>,
96 action: &dyn Action,
97 window: &mut Window,
98 cx: &mut App,
99 ) -> AnyView {
100 cx.new(|cx| Self {
101 title: Title::Str(title.into()),
102 meta: None,
103 key_binding: KeyBinding::for_action(action, window, cx),
104 })
105 .into()
106 }
107
108 pub fn for_action_in(
109 title: impl Into<SharedString>,
110 action: &dyn Action,
111 focus_handle: &FocusHandle,
112 window: &mut Window,
113 cx: &mut App,
114 ) -> AnyView {
115 cx.new(|cx| Self {
116 title: title.into().into(),
117 meta: None,
118 key_binding: KeyBinding::for_action_in(action, focus_handle, window, cx),
119 })
120 .into()
121 }
122
123 pub fn with_meta(
124 title: impl Into<SharedString>,
125 action: Option<&dyn Action>,
126 meta: impl Into<SharedString>,
127 window: &mut Window,
128 cx: &mut App,
129 ) -> AnyView {
130 cx.new(|cx| Self {
131 title: title.into().into(),
132 meta: Some(meta.into()),
133 key_binding: action.and_then(|action| KeyBinding::for_action(action, window, cx)),
134 })
135 .into()
136 }
137
138 pub fn with_meta_in(
139 title: impl Into<SharedString>,
140 action: Option<&dyn Action>,
141 meta: impl Into<SharedString>,
142 focus_handle: &FocusHandle,
143 window: &mut Window,
144 cx: &mut App,
145 ) -> AnyView {
146 cx.new(|cx| Self {
147 title: title.into().into(),
148 meta: Some(meta.into()),
149 key_binding: action
150 .and_then(|action| KeyBinding::for_action_in(action, focus_handle, window, cx)),
151 })
152 .into()
153 }
154
155 pub fn new(title: impl Into<SharedString>) -> Self {
156 Self {
157 title: title.into().into(),
158 meta: None,
159 key_binding: None,
160 }
161 }
162
163 pub fn new_element(title: impl Fn(&mut Window, &mut App) -> AnyElement + 'static) -> Self {
164 Self {
165 title: Title::Callback(Rc::new(title)),
166 meta: None,
167 key_binding: None,
168 }
169 }
170
171 pub fn element(
172 title: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
173 ) -> impl Fn(&mut Window, &mut App) -> AnyView {
174 let title = Title::Callback(Rc::new(title));
175 move |_, cx| {
176 let title = title.clone();
177 cx.new(|_| Self {
178 title,
179 meta: None,
180 key_binding: None,
181 })
182 .into()
183 }
184 }
185
186 pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
187 self.meta = Some(meta.into());
188 self
189 }
190
191 pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
192 self.key_binding = key_binding.into();
193 self
194 }
195}
196
197impl Render for Tooltip {
198 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
199 tooltip_container(window, cx, |el, _, _| {
200 el.child(
201 h_flex()
202 .gap_4()
203 .child(div().max_w_72().child(self.title.clone()))
204 .when_some(self.key_binding.clone(), |this, key_binding| {
205 this.justify_between().child(key_binding)
206 }),
207 )
208 .when_some(self.meta.clone(), |this, meta| {
209 this.child(
210 div()
211 .max_w_72()
212 .child(Label::new(meta).size(LabelSize::Small).color(Color::Muted)),
213 )
214 })
215 })
216 }
217}
218
219pub fn tooltip_container<V, ContentsBuilder: FnOnce(Div, &mut Window, &mut Context<V>) -> Div>(
220 window: &mut Window,
221 cx: &mut Context<V>,
222 f: ContentsBuilder,
223) -> impl IntoElement + use<V, ContentsBuilder> {
224 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
225
226 // padding to avoid tooltip appearing right below the mouse cursor
227 div().pl_2().pt_2p5().child(
228 v_flex()
229 .elevation_2(cx)
230 .font(ui_font)
231 .text_ui(cx)
232 .text_color(cx.theme().colors().text)
233 .py_1()
234 .px_2()
235 .map(|el| f(el, window, cx)),
236 )
237}
238
239pub struct LinkPreview {
240 link: SharedString,
241}
242
243impl LinkPreview {
244 pub fn new(url: &str, cx: &mut App) -> AnyView {
245 let mut wrapped_url = String::new();
246 for (i, ch) in url.chars().enumerate() {
247 if i == 500 {
248 wrapped_url.push('…');
249 break;
250 }
251 if i % 100 == 0 && i != 0 {
252 wrapped_url.push('\n');
253 }
254 wrapped_url.push(ch);
255 }
256 cx.new(|_| LinkPreview {
257 link: wrapped_url.into(),
258 })
259 .into()
260 }
261}
262
263impl Render for LinkPreview {
264 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
265 tooltip_container(window, cx, |el, _, _| {
266 el.child(
267 Label::new(self.link.clone())
268 .size(LabelSize::XSmall)
269 .color(Color::Muted),
270 )
271 })
272 }
273}
274
275impl Component for Tooltip {
276 fn scope() -> ComponentScope {
277 ComponentScope::DataDisplay
278 }
279
280 fn description() -> Option<&'static str> {
281 Some(
282 "A tooltip that appears when hovering over an element, optionally showing a keybinding or additional metadata.",
283 )
284 }
285
286 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
287 Some(
288 example_group(vec![single_example(
289 "Text only",
290 Button::new("delete-example", "Delete")
291 .tooltip(Tooltip::text("This is a tooltip!"))
292 .into_any_element(),
293 )])
294 .into_any_element(),
295 )
296 }
297}