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(
56 title: impl Into<SharedString>,
57 action: &dyn Action,
58 window: &mut Window,
59 cx: &mut App,
60 ) -> AnyView {
61 cx.new(|cx| Self {
62 title: title.into(),
63 meta: None,
64 key_binding: KeyBinding::for_action(action, window, cx),
65 })
66 .into()
67 }
68
69 pub fn for_action_in(
70 title: impl Into<SharedString>,
71 action: &dyn Action,
72 focus_handle: &FocusHandle,
73 window: &mut Window,
74 cx: &mut App,
75 ) -> AnyView {
76 cx.new(|cx| Self {
77 title: title.into(),
78 meta: None,
79 key_binding: KeyBinding::for_action_in(action, focus_handle, window, cx),
80 })
81 .into()
82 }
83
84 pub fn with_meta(
85 title: impl Into<SharedString>,
86 action: Option<&dyn Action>,
87 meta: impl Into<SharedString>,
88 window: &mut Window,
89 cx: &mut App,
90 ) -> AnyView {
91 cx.new(|cx| Self {
92 title: title.into(),
93 meta: Some(meta.into()),
94 key_binding: action.and_then(|action| KeyBinding::for_action(action, window, cx)),
95 })
96 .into()
97 }
98
99 pub fn with_meta_in(
100 title: impl Into<SharedString>,
101 action: Option<&dyn Action>,
102 meta: impl Into<SharedString>,
103 focus_handle: &FocusHandle,
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
111 .and_then(|action| KeyBinding::for_action_in(action, focus_handle, window, cx)),
112 })
113 .into()
114 }
115
116 pub fn new(title: impl Into<SharedString>) -> Self {
117 Self {
118 title: title.into(),
119 meta: None,
120 key_binding: None,
121 }
122 }
123
124 pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
125 self.meta = Some(meta.into());
126 self
127 }
128
129 pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
130 self.key_binding = key_binding.into();
131 self
132 }
133}
134
135impl Render for Tooltip {
136 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
137 tooltip_container(window, cx, |el, _, _| {
138 el.child(
139 h_flex()
140 .gap_4()
141 .child(div().max_w_72().child(self.title.clone()))
142 .when_some(self.key_binding.clone(), |this, key_binding| {
143 this.justify_between().child(key_binding)
144 }),
145 )
146 .when_some(self.meta.clone(), |this, meta| {
147 this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
148 })
149 })
150 }
151}
152
153pub fn tooltip_container<V>(
154 window: &mut Window,
155 cx: &mut Context<V>,
156 f: impl FnOnce(Div, &mut Window, &mut Context<V>) -> Div,
157) -> impl IntoElement {
158 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
159
160 // padding to avoid tooltip appearing right below the mouse cursor
161 div().pl_2().pt_2p5().child(
162 v_flex()
163 .elevation_2(cx)
164 .font(ui_font)
165 .text_ui(cx)
166 .text_color(cx.theme().colors().text)
167 .py_1()
168 .px_2()
169 .map(|el| f(el, window, cx)),
170 )
171}
172
173pub struct LinkPreview {
174 link: SharedString,
175}
176
177impl LinkPreview {
178 pub fn new(url: &str, cx: &mut App) -> AnyView {
179 let mut wrapped_url = String::new();
180 for (i, ch) in url.chars().enumerate() {
181 if i == 500 {
182 wrapped_url.push('…');
183 break;
184 }
185 if i % 100 == 0 && i != 0 {
186 wrapped_url.push('\n');
187 }
188 wrapped_url.push(ch);
189 }
190 cx.new(|_| LinkPreview {
191 link: wrapped_url.into(),
192 })
193 .into()
194 }
195}
196
197impl Render for LinkPreview {
198 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
199 tooltip_container(window, cx, |el, _, _| {
200 el.child(
201 Label::new(self.link.clone())
202 .size(LabelSize::XSmall)
203 .color(Color::Muted),
204 )
205 })
206 }
207}
208
209// View this component preview using `workspace: open component-preview`
210impl ComponentPreview for Tooltip {
211 fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
212 example_group(vec![single_example(
213 "Text only",
214 Button::new("delete-example", "Delete")
215 .tooltip(Tooltip::text("This is a tooltip!"))
216 .into_any_element(),
217 )])
218 .into_any_element()
219 }
220}