1use crate::{Toast, Workspace};
2use collections::HashSet;
3use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle};
4use std::{any::TypeId, ops::DerefMut};
5
6pub fn init(cx: &mut AppContext) {
7 cx.set_global(NotificationTracker::new());
8 simple_message_notification::init(cx);
9}
10
11pub trait Notification: View {
12 fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool;
13}
14
15pub trait NotificationHandle {
16 fn id(&self) -> usize;
17 fn as_any(&self) -> &AnyViewHandle;
18}
19
20impl<T: Notification> NotificationHandle for ViewHandle<T> {
21 fn id(&self) -> usize {
22 self.id()
23 }
24
25 fn as_any(&self) -> &AnyViewHandle {
26 self
27 }
28}
29
30impl From<&dyn NotificationHandle> for AnyViewHandle {
31 fn from(val: &dyn NotificationHandle) -> Self {
32 val.as_any().clone()
33 }
34}
35
36struct NotificationTracker {
37 notifications_sent: HashSet<TypeId>,
38}
39
40impl std::ops::Deref for NotificationTracker {
41 type Target = HashSet<TypeId>;
42
43 fn deref(&self) -> &Self::Target {
44 &self.notifications_sent
45 }
46}
47
48impl DerefMut for NotificationTracker {
49 fn deref_mut(&mut self) -> &mut Self::Target {
50 &mut self.notifications_sent
51 }
52}
53
54impl NotificationTracker {
55 fn new() -> Self {
56 Self {
57 notifications_sent: HashSet::default(),
58 }
59 }
60}
61
62impl Workspace {
63 pub fn show_notification_once<V: Notification>(
64 &mut self,
65 id: usize,
66 cx: &mut ViewContext<Self>,
67 build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
68 ) {
69 if !cx
70 .global::<NotificationTracker>()
71 .contains(&TypeId::of::<V>())
72 {
73 cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
74 tracker.insert(TypeId::of::<V>())
75 });
76
77 self.show_notification::<V>(id, cx, build_notification)
78 }
79 }
80
81 pub fn show_notification<V: Notification>(
82 &mut self,
83 id: usize,
84 cx: &mut ViewContext<Self>,
85 build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
86 ) {
87 let type_id = TypeId::of::<V>();
88 if self
89 .notifications
90 .iter()
91 .all(|(existing_type_id, existing_id, _)| {
92 (*existing_type_id, *existing_id) != (type_id, id)
93 })
94 {
95 let notification = build_notification(cx);
96 cx.subscribe(¬ification, move |this, handle, event, cx| {
97 if handle.read(cx).should_dismiss_notification_on_event(event) {
98 this.dismiss_notification_internal(type_id, id, cx);
99 }
100 })
101 .detach();
102 self.notifications
103 .push((type_id, id, Box::new(notification)));
104 cx.notify();
105 }
106 }
107
108 pub fn dismiss_notification<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
109 let type_id = TypeId::of::<V>();
110
111 self.dismiss_notification_internal(type_id, id, cx)
112 }
113
114 pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
115 self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
116 self.show_notification(toast.id, cx, |cx| {
117 cx.add_view(|_cx| match toast.on_click.as_ref() {
118 Some((click_msg, on_click)) => {
119 let on_click = on_click.clone();
120 simple_message_notification::MessageNotification::new(toast.msg.clone())
121 .with_click_message(click_msg.clone())
122 .on_click(move |cx| on_click(cx))
123 }
124 None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
125 })
126 })
127 }
128
129 pub fn dismiss_toast(&mut self, id: usize, cx: &mut ViewContext<Self>) {
130 self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
131 }
132
133 fn dismiss_notification_internal(
134 &mut self,
135 type_id: TypeId,
136 id: usize,
137 cx: &mut ViewContext<Self>,
138 ) {
139 self.notifications
140 .retain(|(existing_type_id, existing_id, _)| {
141 if (*existing_type_id, *existing_id) == (type_id, id) {
142 cx.notify();
143 false
144 } else {
145 true
146 }
147 });
148 }
149}
150
151pub mod simple_message_notification {
152 use gpui::{
153 actions,
154 elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
155 impl_actions,
156 platform::{CursorStyle, MouseButton},
157 AppContext, Element, Entity, View, ViewContext,
158 };
159 use menu::Cancel;
160 use serde::Deserialize;
161 use settings::Settings;
162 use std::{borrow::Cow, sync::Arc};
163
164 use crate::Workspace;
165
166 use super::Notification;
167
168 actions!(message_notifications, [CancelMessageNotification]);
169
170 #[derive(Clone, Default, Deserialize, PartialEq)]
171 pub struct OsOpen(pub Cow<'static, str>);
172
173 impl OsOpen {
174 pub fn new<I: Into<Cow<'static, str>>>(url: I) -> Self {
175 OsOpen(url.into())
176 }
177 }
178
179 impl_actions!(message_notifications, [OsOpen]);
180
181 pub fn init(cx: &mut AppContext) {
182 cx.add_action(MessageNotification::dismiss);
183 cx.add_action(
184 |_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext<Workspace>| {
185 cx.platform().open_url(open_action.0.as_ref());
186 },
187 )
188 }
189
190 pub struct MessageNotification {
191 message: Cow<'static, str>,
192 on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
193 click_message: Option<Cow<'static, str>>,
194 }
195
196 pub enum MessageNotificationEvent {
197 Dismiss,
198 }
199
200 impl Entity for MessageNotification {
201 type Event = MessageNotificationEvent;
202 }
203
204 impl MessageNotification {
205 pub fn new<S>(message: S) -> MessageNotification
206 where
207 S: Into<Cow<'static, str>>,
208 {
209 Self {
210 message: message.into(),
211 on_click: None,
212 click_message: None,
213 }
214 }
215
216 pub fn with_click_message<S>(mut self, message: S) -> Self
217 where
218 S: Into<Cow<'static, str>>,
219 {
220 self.click_message = Some(message.into());
221 self
222 }
223
224 pub fn on_click<F>(mut self, on_click: F) -> Self
225 where
226 F: 'static + Fn(&mut ViewContext<Self>),
227 {
228 self.on_click = Some(Arc::new(on_click));
229 self
230 }
231
232 pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext<Self>) {
233 cx.emit(MessageNotificationEvent::Dismiss);
234 }
235 }
236
237 impl View for MessageNotification {
238 fn ui_name() -> &'static str {
239 "MessageNotification"
240 }
241
242 fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
243 let theme = cx.global::<Settings>().theme.clone();
244 let theme = &theme.simple_message_notification;
245
246 enum MessageNotificationTag {}
247
248 let click_message = self.click_message.clone();
249 let message = self.message.clone();
250 let on_click = self.on_click.clone();
251 let has_click_action = on_click.is_some();
252
253 MouseEventHandler::<MessageNotificationTag, _>::new(0, cx, |state, cx| {
254 Flex::column()
255 .with_child(
256 Flex::row()
257 .with_child(
258 Text::new(message, theme.message.text.clone())
259 .contained()
260 .with_style(theme.message.container)
261 .aligned()
262 .top()
263 .left()
264 .flex(1., true),
265 )
266 .with_child(
267 MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
268 let style = theme.dismiss_button.style_for(state, false);
269 Svg::new("icons/x_mark_8.svg")
270 .with_color(style.color)
271 .constrained()
272 .with_width(style.icon_width)
273 .aligned()
274 .contained()
275 .with_style(style.container)
276 .constrained()
277 .with_width(style.button_width)
278 .with_height(style.button_width)
279 })
280 .with_padding(Padding::uniform(5.))
281 .on_click(MouseButton::Left, move |_, this, cx| {
282 this.dismiss(&Default::default(), cx);
283 })
284 .with_cursor_style(CursorStyle::PointingHand)
285 .aligned()
286 .constrained()
287 .with_height(
288 cx.font_cache().line_height(theme.message.text.font_size),
289 )
290 .aligned()
291 .top()
292 .flex_float(),
293 ),
294 )
295 .with_children({
296 let style = theme.action_message.style_for(state, false);
297 if let Some(click_message) = click_message {
298 Some(
299 Flex::row().with_child(
300 Text::new(click_message, style.text.clone())
301 .contained()
302 .with_style(style.container),
303 ),
304 )
305 } else {
306 None
307 }
308 .into_iter()
309 })
310 .contained()
311 })
312 // Since we're not using a proper overlay, we have to capture these extra events
313 .on_down(MouseButton::Left, |_, _, _| {})
314 .on_up(MouseButton::Left, |_, _, _| {})
315 .on_click(MouseButton::Left, move |_, this, cx| {
316 if let Some(on_click) = on_click.as_ref() {
317 on_click(cx);
318 this.dismiss(&Default::default(), cx);
319 }
320 })
321 .with_cursor_style(if has_click_action {
322 CursorStyle::PointingHand
323 } else {
324 CursorStyle::Arrow
325 })
326 .into_any()
327 }
328 }
329
330 impl Notification for MessageNotification {
331 fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
332 match event {
333 MessageNotificationEvent::Dismiss => true,
334 }
335 }
336 }
337}
338
339pub trait NotifyResultExt {
340 type Ok;
341
342 fn notify_err(
343 self,
344 workspace: &mut Workspace,
345 cx: &mut ViewContext<Workspace>,
346 ) -> Option<Self::Ok>;
347}
348
349impl<T, E> NotifyResultExt for Result<T, E>
350where
351 E: std::fmt::Debug,
352{
353 type Ok = T;
354
355 fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<T> {
356 match self {
357 Ok(value) => Some(value),
358 Err(err) => {
359 workspace.show_notification(0, cx, |cx| {
360 cx.add_view(|_cx| {
361 simple_message_notification::MessageNotification::new(format!(
362 "Error: {:?}",
363 err,
364 ))
365 })
366 });
367
368 None
369 }
370 }
371 }
372}