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