notifications.rs

  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(&notification, 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}