1use std::borrow::Borrow;
2use std::rc::Rc;
3
4use gpui::{Action, AnyElement, AnyView, AppContext, FocusHandle, IntoElement, Render};
5use settings::Settings;
6use theme::ThemeSettings;
7
8use crate::prelude::*;
9use crate::{Color, KeyBinding, Label, LabelSize, StyledExt, h_flex, v_flex};
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 |window, cx| {
68 cx.new(|cx| Self {
69 title: Title::Str(title.clone()),
70 meta: None,
71 key_binding: KeyBinding::for_action(action.as_ref(), window, 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 |window, cx| {
86 cx.new(|cx| Self {
87 title: Title::Str(title.clone()),
88 meta: None,
89 key_binding: KeyBinding::for_action_in(action.as_ref(), &focus_handle, window, cx),
90 })
91 .into()
92 }
93 }
94
95 pub fn for_action(
96 title: impl Into<SharedString>,
97 action: &dyn Action,
98 window: &mut Window,
99 cx: &mut App,
100 ) -> AnyView {
101 cx.new(|cx| Self {
102 title: Title::Str(title.into()),
103 meta: None,
104 key_binding: KeyBinding::for_action(action, window, 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 window: &mut Window,
114 cx: &mut App,
115 ) -> AnyView {
116 cx.new(|cx| Self {
117 title: title.into().into(),
118 meta: None,
119 key_binding: KeyBinding::for_action_in(action, focus_handle, window, cx),
120 })
121 .into()
122 }
123
124 pub fn with_meta(
125 title: impl Into<SharedString>,
126 action: Option<&dyn Action>,
127 meta: impl Into<SharedString>,
128 window: &mut Window,
129 cx: &mut App,
130 ) -> AnyView {
131 cx.new(|cx| Self {
132 title: title.into().into(),
133 meta: Some(meta.into()),
134 key_binding: action.and_then(|action| KeyBinding::for_action(action, window, cx)),
135 })
136 .into()
137 }
138
139 pub fn with_meta_in(
140 title: impl Into<SharedString>,
141 action: Option<&dyn Action>,
142 meta: impl Into<SharedString>,
143 focus_handle: &FocusHandle,
144 window: &mut Window,
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
151 .and_then(|action| KeyBinding::for_action_in(action, focus_handle, window, cx)),
152 })
153 .into()
154 }
155
156 pub fn new(title: impl Into<SharedString>) -> Self {
157 Self {
158 title: title.into().into(),
159 meta: None,
160 key_binding: None,
161 }
162 }
163
164 pub fn new_element(title: impl Fn(&mut Window, &mut App) -> AnyElement + 'static) -> Self {
165 Self {
166 title: Title::Callback(Rc::new(title)),
167 meta: None,
168 key_binding: None,
169 }
170 }
171
172 pub fn element(
173 title: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
174 ) -> impl Fn(&mut Window, &mut App) -> AnyView {
175 let title = Title::Callback(Rc::new(title));
176 move |_, cx| {
177 let title = title.clone();
178 cx.new(|_| Self {
179 title,
180 meta: None,
181 key_binding: None,
182 })
183 .into()
184 }
185 }
186
187 pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
188 self.meta = Some(meta.into());
189 self
190 }
191
192 pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
193 self.key_binding = key_binding.into();
194 self
195 }
196}
197
198impl Render for Tooltip {
199 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
200 tooltip_container(cx, |el, _| {
201 el.child(
202 h_flex()
203 .gap_4()
204 .child(div().max_w_72().child(self.title.clone()))
205 .when_some(self.key_binding.clone(), |this, key_binding| {
206 this.justify_between().child(key_binding)
207 }),
208 )
209 .when_some(self.meta.clone(), |this, meta| {
210 this.child(
211 div()
212 .max_w_72()
213 .child(Label::new(meta).size(LabelSize::Small).color(Color::Muted)),
214 )
215 })
216 })
217 }
218}
219
220pub fn tooltip_container<C>(cx: &mut C, f: impl FnOnce(Div, &mut C) -> Div) -> impl IntoElement
221where
222 C: AppContext + Borrow<App>,
223{
224 let app = (*cx).borrow();
225 let ui_font = ThemeSettings::get_global(app).ui_font.clone();
226
227 // padding to avoid tooltip appearing right below the mouse cursor
228 div().pl_2().pt_2p5().child(
229 v_flex()
230 .elevation_2(app)
231 .font(ui_font)
232 .text_ui(app)
233 .text_color(app.theme().colors().text)
234 .py_1()
235 .px_2()
236 .map(|el| f(el, cx)),
237 )
238}
239
240pub struct LinkPreview {
241 link: SharedString,
242}
243
244impl LinkPreview {
245 pub fn new(url: &str, cx: &mut App) -> AnyView {
246 let mut wrapped_url = String::new();
247 for (i, ch) in url.chars().enumerate() {
248 if i == 500 {
249 wrapped_url.push('…');
250 break;
251 }
252 if i % 100 == 0 && i != 0 {
253 wrapped_url.push('\n');
254 }
255 wrapped_url.push(ch);
256 }
257 cx.new(|_| LinkPreview {
258 link: wrapped_url.into(),
259 })
260 .into()
261 }
262}
263
264impl Render for LinkPreview {
265 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
266 tooltip_container(cx, |el, _| {
267 el.child(
268 Label::new(self.link.clone())
269 .size(LabelSize::XSmall)
270 .color(Color::Muted),
271 )
272 })
273 }
274}
275
276impl Component for Tooltip {
277 fn scope() -> ComponentScope {
278 ComponentScope::DataDisplay
279 }
280
281 fn description() -> Option<&'static str> {
282 Some(
283 "A tooltip that appears when hovering over an element, optionally showing a keybinding or additional metadata.",
284 )
285 }
286
287 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
288 Some(
289 example_group(vec![single_example(
290 "Text only",
291 Button::new("delete-example", "Delete")
292 .tooltip(Tooltip::text("This is a tooltip!"))
293 .into_any_element(),
294 )])
295 .into_any_element(),
296 )
297 }
298}