notifications.rs

  1use crate::{Toast, Workspace};
  2use collections::HashMap;
  3use gpui::{
  4    AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Global,
  5    PromptLevel, Render, Task, View, ViewContext, VisualContext, WindowContext,
  6};
  7use std::{any::TypeId, ops::DerefMut};
  8
  9pub fn init(cx: &mut AppContext) {
 10    cx.set_global(NotificationTracker::new());
 11}
 12
 13pub trait Notification: EventEmitter<DismissEvent> + Render {}
 14
 15impl<V: EventEmitter<DismissEvent> + Render> Notification for V {}
 16
 17pub trait NotificationHandle: Send {
 18    fn id(&self) -> EntityId;
 19    fn to_any(&self) -> AnyView;
 20}
 21
 22impl<T: Notification> NotificationHandle for View<T> {
 23    fn id(&self) -> EntityId {
 24        self.entity_id()
 25    }
 26
 27    fn to_any(&self) -> AnyView {
 28        self.clone().into()
 29    }
 30}
 31
 32impl From<&dyn NotificationHandle> for AnyView {
 33    fn from(val: &dyn NotificationHandle) -> Self {
 34        val.to_any()
 35    }
 36}
 37
 38pub(crate) struct NotificationTracker {
 39    notifications_sent: HashMap<TypeId, Vec<usize>>,
 40}
 41
 42impl Global for NotificationTracker {}
 43
 44impl std::ops::Deref for NotificationTracker {
 45    type Target = HashMap<TypeId, Vec<usize>>;
 46
 47    fn deref(&self) -> &Self::Target {
 48        &self.notifications_sent
 49    }
 50}
 51
 52impl DerefMut for NotificationTracker {
 53    fn deref_mut(&mut self) -> &mut Self::Target {
 54        &mut self.notifications_sent
 55    }
 56}
 57
 58impl NotificationTracker {
 59    fn new() -> Self {
 60        Self {
 61            notifications_sent: Default::default(),
 62        }
 63    }
 64}
 65
 66impl Workspace {
 67    pub fn has_shown_notification_once<V: Notification>(
 68        &self,
 69        id: usize,
 70        cx: &ViewContext<Self>,
 71    ) -> bool {
 72        cx.global::<NotificationTracker>()
 73            .get(&TypeId::of::<V>())
 74            .map(|ids| ids.contains(&id))
 75            .unwrap_or(false)
 76    }
 77
 78    pub fn show_notification_once<V: Notification>(
 79        &mut self,
 80        id: usize,
 81        cx: &mut ViewContext<Self>,
 82        build_notification: impl FnOnce(&mut ViewContext<Self>) -> View<V>,
 83    ) {
 84        if !self.has_shown_notification_once::<V>(id, cx) {
 85            let tracker = cx.global_mut::<NotificationTracker>();
 86            let entry = tracker.entry(TypeId::of::<V>()).or_default();
 87            entry.push(id);
 88            self.show_notification::<V>(id, cx, build_notification)
 89        }
 90    }
 91
 92    pub fn show_notification<V: Notification>(
 93        &mut self,
 94        id: usize,
 95        cx: &mut ViewContext<Self>,
 96        build_notification: impl FnOnce(&mut ViewContext<Self>) -> View<V>,
 97    ) {
 98        let type_id = TypeId::of::<V>();
 99        if self
100            .notifications
101            .iter()
102            .all(|(existing_type_id, existing_id, _)| {
103                (*existing_type_id, *existing_id) != (type_id, id)
104            })
105        {
106            let notification = build_notification(cx);
107            cx.subscribe(&notification, move |this, _, _: &DismissEvent, cx| {
108                this.dismiss_notification_internal(type_id, id, cx);
109            })
110            .detach();
111            self.notifications
112                .push((type_id, id, Box::new(notification)));
113            cx.notify();
114        }
115    }
116
117    pub fn show_error<E>(&mut self, err: &E, cx: &mut ViewContext<Self>)
118    where
119        E: std::fmt::Debug,
120    {
121        self.show_notification(0, cx, |cx| {
122            cx.new_view(|_cx| {
123                simple_message_notification::MessageNotification::new(format!("Error: {err:?}"))
124            })
125        });
126    }
127
128    pub fn dismiss_notification<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
129        let type_id = TypeId::of::<V>();
130
131        self.dismiss_notification_internal(type_id, id, cx)
132    }
133
134    pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
135        self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
136        self.show_notification(toast.id, cx, |cx| {
137            cx.new_view(|_cx| match toast.on_click.as_ref() {
138                Some((click_msg, on_click)) => {
139                    let on_click = on_click.clone();
140                    simple_message_notification::MessageNotification::new(toast.msg.clone())
141                        .with_click_message(click_msg.clone())
142                        .on_click(move |cx| on_click(cx))
143                }
144                None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
145            })
146        })
147    }
148
149    pub fn dismiss_toast(&mut self, id: usize, cx: &mut ViewContext<Self>) {
150        self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
151    }
152
153    fn dismiss_notification_internal(
154        &mut self,
155        type_id: TypeId,
156        id: usize,
157        cx: &mut ViewContext<Self>,
158    ) {
159        self.notifications
160            .retain(|(existing_type_id, existing_id, _)| {
161                if (*existing_type_id, *existing_id) == (type_id, id) {
162                    cx.notify();
163                    false
164                } else {
165                    true
166                }
167            });
168    }
169}
170
171pub mod simple_message_notification {
172    use gpui::{
173        div, DismissEvent, EventEmitter, InteractiveElement, ParentElement, Render, SharedString,
174        StatefulInteractiveElement, Styled, ViewContext,
175    };
176    use std::sync::Arc;
177    use ui::prelude::*;
178    use ui::{h_flex, v_flex, Button, Icon, IconName, Label, StyledExt};
179
180    pub struct MessageNotification {
181        message: SharedString,
182        on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
183        click_message: Option<SharedString>,
184    }
185
186    impl EventEmitter<DismissEvent> for MessageNotification {}
187
188    impl MessageNotification {
189        pub fn new<S>(message: S) -> MessageNotification
190        where
191            S: Into<SharedString>,
192        {
193            Self {
194                message: message.into(),
195                on_click: None,
196                click_message: None,
197            }
198        }
199
200        pub fn with_click_message<S>(mut self, message: S) -> Self
201        where
202            S: Into<SharedString>,
203        {
204            self.click_message = Some(message.into());
205            self
206        }
207
208        pub fn on_click<F>(mut self, on_click: F) -> Self
209        where
210            F: 'static + Fn(&mut ViewContext<Self>),
211        {
212            self.on_click = Some(Arc::new(on_click));
213            self
214        }
215
216        pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
217            cx.emit(DismissEvent);
218        }
219    }
220
221    impl Render for MessageNotification {
222        fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
223            v_flex()
224                .elevation_3(cx)
225                .p_4()
226                .child(
227                    h_flex()
228                        .justify_between()
229                        .child(div().max_w_80().child(Label::new(self.message.clone())))
230                        .child(
231                            div()
232                                .id("cancel")
233                                .child(Icon::new(IconName::Close))
234                                .cursor_pointer()
235                                .on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
236                        ),
237                )
238                .children(self.click_message.iter().map(|message| {
239                    Button::new(message.clone(), message.clone()).on_click(cx.listener(
240                        |this, _, cx| {
241                            if let Some(on_click) = this.on_click.as_ref() {
242                                (on_click)(cx)
243                            };
244                            this.dismiss(cx)
245                        },
246                    ))
247                }))
248        }
249    }
250}
251
252pub trait NotifyResultExt {
253    type Ok;
254
255    fn notify_err(
256        self,
257        workspace: &mut Workspace,
258        cx: &mut ViewContext<Workspace>,
259    ) -> Option<Self::Ok>;
260
261    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
262}
263
264impl<T, E> NotifyResultExt for Result<T, E>
265where
266    E: std::fmt::Debug,
267{
268    type Ok = T;
269
270    fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<T> {
271        match self {
272            Ok(value) => Some(value),
273            Err(err) => {
274                log::error!("TODO {err:?}");
275                workspace.show_error(&err, cx);
276                None
277            }
278        }
279    }
280
281    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
282        match self {
283            Ok(value) => Some(value),
284            Err(err) => {
285                log::error!("TODO {err:?}");
286                cx.update_root(|view, cx| {
287                    if let Ok(workspace) = view.downcast::<Workspace>() {
288                        workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
289                    }
290                })
291                .ok();
292                None
293            }
294        }
295    }
296}
297
298pub trait NotifyTaskExt {
299    fn detach_and_notify_err(self, cx: &mut WindowContext);
300}
301
302impl<R, E> NotifyTaskExt for Task<Result<R, E>>
303where
304    E: std::fmt::Debug + Sized + 'static,
305    R: 'static,
306{
307    fn detach_and_notify_err(self, cx: &mut WindowContext) {
308        cx.spawn(|mut cx| async move { self.await.notify_async_err(&mut cx) })
309            .detach();
310    }
311}
312
313pub trait DetachAndPromptErr {
314    fn detach_and_prompt_err(
315        self,
316        msg: &str,
317        cx: &mut WindowContext,
318        f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
319    );
320}
321
322impl<R> DetachAndPromptErr for Task<anyhow::Result<R>>
323where
324    R: 'static,
325{
326    fn detach_and_prompt_err(
327        self,
328        msg: &str,
329        cx: &mut WindowContext,
330        f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
331    ) {
332        let msg = msg.to_owned();
333        cx.spawn(|mut cx| async move {
334            if let Err(err) = self.await {
335                log::error!("{err:?}");
336                if let Ok(prompt) = cx.update(|cx| {
337                    let detail = f(&err, cx)
338                        .unwrap_or_else(|| format!("{err:?}. Please try again.", err = err));
339                    cx.prompt(PromptLevel::Critical, &msg, Some(&detail), &["Ok"])
340                }) {
341                    prompt.await.ok();
342                }
343            }
344        })
345        .detach();
346    }
347}