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