notifications.rs

  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(&notification, 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 super::Notification;
153    use crate::Workspace;
154    use gpui::{
155        actions,
156        elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
157        impl_actions,
158        platform::{CursorStyle, MouseButton},
159        AppContext, Element, Entity, View, ViewContext,
160    };
161    use menu::Cancel;
162    use serde::Deserialize;
163    use std::{borrow::Cow, sync::Arc};
164
165    actions!(message_notifications, [CancelMessageNotification]);
166
167    #[derive(Clone, Default, Deserialize, PartialEq)]
168    pub struct OsOpen(pub Cow<'static, str>);
169
170    impl OsOpen {
171        pub fn new<I: Into<Cow<'static, str>>>(url: I) -> Self {
172            OsOpen(url.into())
173        }
174    }
175
176    impl_actions!(message_notifications, [OsOpen]);
177
178    pub fn init(cx: &mut AppContext) {
179        cx.add_action(MessageNotification::dismiss);
180        cx.add_action(
181            |_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext<Workspace>| {
182                cx.platform().open_url(open_action.0.as_ref());
183            },
184        )
185    }
186
187    pub struct MessageNotification {
188        message: Cow<'static, str>,
189        on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
190        click_message: Option<Cow<'static, str>>,
191    }
192
193    pub enum MessageNotificationEvent {
194        Dismiss,
195    }
196
197    impl Entity for MessageNotification {
198        type Event = MessageNotificationEvent;
199    }
200
201    impl MessageNotification {
202        pub fn new<S>(message: S) -> MessageNotification
203        where
204            S: Into<Cow<'static, str>>,
205        {
206            Self {
207                message: message.into(),
208                on_click: None,
209                click_message: None,
210            }
211        }
212
213        pub fn with_click_message<S>(mut self, message: S) -> Self
214        where
215            S: Into<Cow<'static, str>>,
216        {
217            self.click_message = Some(message.into());
218            self
219        }
220
221        pub fn on_click<F>(mut self, on_click: F) -> Self
222        where
223            F: 'static + Fn(&mut ViewContext<Self>),
224        {
225            self.on_click = Some(Arc::new(on_click));
226            self
227        }
228
229        pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext<Self>) {
230            cx.emit(MessageNotificationEvent::Dismiss);
231        }
232    }
233
234    impl View for MessageNotification {
235        fn ui_name() -> &'static str {
236            "MessageNotification"
237        }
238
239        fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
240            let theme = theme::current(cx).clone();
241            let theme = &theme.simple_message_notification;
242
243            enum MessageNotificationTag {}
244
245            let click_message = self.click_message.clone();
246            let message = self.message.clone();
247            let on_click = self.on_click.clone();
248            let has_click_action = on_click.is_some();
249
250            MouseEventHandler::<MessageNotificationTag, _>::new(0, cx, |state, cx| {
251                Flex::column()
252                    .with_child(
253                        Flex::row()
254                            .with_child(
255                                Text::new(message, theme.message.text.clone())
256                                    .contained()
257                                    .with_style(theme.message.container)
258                                    .aligned()
259                                    .top()
260                                    .left()
261                                    .flex(1., true),
262                            )
263                            .with_child(
264                                MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
265                                    let style = theme.dismiss_button.style_for(state, false);
266                                    Svg::new("icons/x_mark_8.svg")
267                                        .with_color(style.color)
268                                        .constrained()
269                                        .with_width(style.icon_width)
270                                        .aligned()
271                                        .contained()
272                                        .with_style(style.container)
273                                        .constrained()
274                                        .with_width(style.button_width)
275                                        .with_height(style.button_width)
276                                })
277                                .with_padding(Padding::uniform(5.))
278                                .on_click(MouseButton::Left, move |_, this, cx| {
279                                    this.dismiss(&Default::default(), cx);
280                                })
281                                .with_cursor_style(CursorStyle::PointingHand)
282                                .aligned()
283                                .constrained()
284                                .with_height(
285                                    cx.font_cache().line_height(theme.message.text.font_size),
286                                )
287                                .aligned()
288                                .top()
289                                .flex_float(),
290                            ),
291                    )
292                    .with_children({
293                        let style = theme.action_message.style_for(state, false);
294                        if let Some(click_message) = click_message {
295                            Some(
296                                Flex::row().with_child(
297                                    Text::new(click_message, style.text.clone())
298                                        .contained()
299                                        .with_style(style.container),
300                                ),
301                            )
302                        } else {
303                            None
304                        }
305                        .into_iter()
306                    })
307                    .contained()
308            })
309            // Since we're not using a proper overlay, we have to capture these extra events
310            .on_down(MouseButton::Left, |_, _, _| {})
311            .on_up(MouseButton::Left, |_, _, _| {})
312            .on_click(MouseButton::Left, move |_, this, cx| {
313                if let Some(on_click) = on_click.as_ref() {
314                    on_click(cx);
315                    this.dismiss(&Default::default(), cx);
316                }
317            })
318            .with_cursor_style(if has_click_action {
319                CursorStyle::PointingHand
320            } else {
321                CursorStyle::Arrow
322            })
323            .into_any()
324        }
325    }
326
327    impl Notification for MessageNotification {
328        fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
329            match event {
330                MessageNotificationEvent::Dismiss => true,
331            }
332        }
333    }
334}
335
336pub trait NotifyResultExt {
337    type Ok;
338
339    fn notify_err(
340        self,
341        workspace: &mut Workspace,
342        cx: &mut ViewContext<Workspace>,
343    ) -> Option<Self::Ok>;
344}
345
346impl<T, E> NotifyResultExt for Result<T, E>
347where
348    E: std::fmt::Debug,
349{
350    type Ok = T;
351
352    fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<T> {
353        match self {
354            Ok(value) => Some(value),
355            Err(err) => {
356                workspace.show_notification(0, cx, |cx| {
357                    cx.add_view(|_cx| {
358                        simple_message_notification::MessageNotification::new(format!(
359                            "Error: {:?}",
360                            err,
361                        ))
362                    })
363                });
364
365                None
366            }
367        }
368    }
369}