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