Skip to main content

ui/components/
tooltip.rs

1use std::borrow::Borrow;
2use std::rc::Rc;
3
4use crate::prelude::*;
5use crate::{Color, KeyBinding, Label, LabelSize, h_flex, v_flex};
6use gpui::{
7    Action, AnyElement, AnyView, AppContext, FocusHandle, IntoElement, KeybindingKeystroke,
8    Keystroke, Render, white,
9};
10
11#[derive(RegisterComponent)]
12pub struct Tooltip {
13    title: Title,
14    meta: Option<SharedString>,
15    key_binding: Option<KeyBinding>,
16}
17
18#[derive(Clone, IntoElement)]
19enum Title {
20    Str(SharedString),
21    Callback(Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>),
22}
23
24impl From<SharedString> for Title {
25    fn from(value: SharedString) -> Self {
26        Title::Str(value)
27    }
28}
29
30impl RenderOnce for Title {
31    fn render(self, window: &mut Window, cx: &mut App) -> impl gpui::IntoElement {
32        match self {
33            Title::Str(title) => title.into_any_element(),
34            Title::Callback(element) => element(window, cx),
35        }
36    }
37}
38
39impl Tooltip {
40    pub fn simple(title: impl Into<SharedString>, cx: &mut App) -> AnyView {
41        cx.new(|_| Self {
42            title: Title::Str(title.into()),
43            meta: None,
44            key_binding: None,
45        })
46        .into()
47    }
48
49    pub fn text(title: impl Into<SharedString>) -> impl Fn(&mut Window, &mut App) -> AnyView {
50        let title = title.into();
51        move |_, cx| {
52            cx.new(|_| Self {
53                title: title.clone().into(),
54                meta: None,
55                key_binding: None,
56            })
57            .into()
58        }
59    }
60
61    pub fn for_action_title<T: Into<SharedString>>(
62        title: T,
63        action: &dyn Action,
64    ) -> impl Fn(&mut Window, &mut App) -> AnyView + use<T> {
65        let title = title.into();
66        let action = action.boxed_clone();
67        move |_, cx| {
68            cx.new(|cx| Self {
69                title: Title::Str(title.clone()),
70                meta: None,
71                key_binding: Some(KeyBinding::for_action(action.as_ref(), cx)),
72            })
73            .into()
74        }
75    }
76
77    pub fn for_action_title_in<Str: Into<SharedString>>(
78        title: Str,
79        action: &dyn Action,
80        focus_handle: &FocusHandle,
81    ) -> impl Fn(&mut Window, &mut App) -> AnyView + use<Str> {
82        let title = title.into();
83        let action = action.boxed_clone();
84        let focus_handle = focus_handle.clone();
85        move |_, cx| {
86            cx.new(|cx| Self {
87                title: Title::Str(title.clone()),
88                meta: None,
89                key_binding: Some(KeyBinding::for_action_in(
90                    action.as_ref(),
91                    &focus_handle,
92                    cx,
93                )),
94            })
95            .into()
96        }
97    }
98
99    pub fn for_action(
100        title: impl Into<SharedString>,
101        action: &dyn Action,
102        cx: &mut App,
103    ) -> AnyView {
104        cx.new(|cx| Self {
105            title: Title::Str(title.into()),
106            meta: None,
107            key_binding: Some(KeyBinding::for_action(action, cx)),
108        })
109        .into()
110    }
111
112    pub fn for_action_in(
113        title: impl Into<SharedString>,
114        action: &dyn Action,
115        focus_handle: &FocusHandle,
116        cx: &mut App,
117    ) -> AnyView {
118        cx.new(|cx| Self {
119            title: title.into().into(),
120            meta: None,
121            key_binding: Some(KeyBinding::for_action_in(action, focus_handle, cx)),
122        })
123        .into()
124    }
125
126    pub fn with_meta(
127        title: impl Into<SharedString>,
128        action: Option<&dyn Action>,
129        meta: impl Into<SharedString>,
130        cx: &mut App,
131    ) -> AnyView {
132        cx.new(|cx| Self {
133            title: title.into().into(),
134            meta: Some(meta.into()),
135            key_binding: action.map(|action| KeyBinding::for_action(action, cx)),
136        })
137        .into()
138    }
139
140    pub fn with_meta_in(
141        title: impl Into<SharedString>,
142        action: Option<&dyn Action>,
143        meta: impl Into<SharedString>,
144        focus_handle: &FocusHandle,
145        cx: &mut App,
146    ) -> AnyView {
147        cx.new(|cx| Self {
148            title: title.into().into(),
149            meta: Some(meta.into()),
150            key_binding: action.map(|action| KeyBinding::for_action_in(action, focus_handle, 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, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
199        tooltip_container(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<C>(cx: &mut C, f: impl FnOnce(Div, &mut C) -> Div) -> impl IntoElement
220where
221    C: AppContext + Borrow<App>,
222{
223    let app = (*cx).borrow();
224    let ui_font = theme::theme_settings(app).ui_font(app).clone();
225
226    // padding to avoid tooltip appearing right below the mouse cursor
227    div().pl_2().pt_2p5().child(
228        v_flex()
229            .bg(palette::neutral(900))
230            .rounded_md()
231            .shadow_level(Shadow::Md)
232            .font(ui_font)
233            .text_ui(app)
234            .text_color(white())
235            .py_1()
236            .px_2()
237            .map(|el| f(el, cx)),
238    )
239}
240
241pub struct LinkPreview {
242    link: SharedString,
243}
244
245impl LinkPreview {
246    pub fn new(url: &str, cx: &mut App) -> AnyView {
247        let mut wrapped_url = String::new();
248        for (i, ch) in url.chars().enumerate() {
249            if i == 500 {
250                wrapped_url.push('…');
251                break;
252            }
253            if i % 100 == 0 && i != 0 {
254                wrapped_url.push('\n');
255            }
256            wrapped_url.push(ch);
257        }
258        cx.new(|_| LinkPreview {
259            link: wrapped_url.into(),
260        })
261        .into()
262    }
263}
264
265impl Render for LinkPreview {
266    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
267        tooltip_container(cx, |el, _| {
268            el.child(
269                Label::new(self.link.clone())
270                    .size(LabelSize::XSmall)
271                    .color(Color::Muted),
272            )
273        })
274    }
275}
276
277impl Component for Tooltip {
278    fn scope() -> ComponentScope {
279        ComponentScope::DataDisplay
280    }
281
282    fn description() -> Option<&'static str> {
283        Some(
284            "A tooltip that appears when hovering over an element, optionally showing a keybinding or additional metadata.",
285        )
286    }
287
288    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
289        Some(
290            example_group(vec![
291                single_example(
292                    "Text only",
293                    Button::new("delete-example", "Delete")
294                        .tooltip(Tooltip::text("This is a tooltip!"))
295                        .into_any_element(),
296                ),
297                single_example(
298                    "With Meta",
299                    Button::new("archive-example", "Archive")
300                        .tooltip(move |_, cx| {
301                            Tooltip::with_meta(
302                                "Archive conversation",
303                                None,
304                                "Moves this to your archive; you can restore it later.",
305                                cx,
306                            )
307                        })
308                        .into_any_element(),
309                ),
310                single_example(
311                    "With Keybinding",
312                    Button::new("save-example", "Save")
313                        .tooltip(move |_, cx| {
314                            let key_binding = KeyBinding::from_keystrokes(
315                                vec![KeybindingKeystroke::from_keystroke(
316                                    Keystroke::parse("cmd-s").unwrap(),
317                                )]
318                                .into(),
319                                false,
320                            );
321                            cx.new(|_| Tooltip {
322                                title: SharedString::from("Save").into(),
323                                meta: None,
324                                key_binding: Some(key_binding),
325                            })
326                            .into()
327                        })
328                        .into_any_element(),
329                ),
330            ])
331            .into_any_element(),
332        )
333    }
334}