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