notifications.rs

  1use crate::{Toast, Workspace};
  2use collections::HashMap;
  3use gpui::{
  4    AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Render,
  5    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 std::ops::Deref for NotificationTracker {
 43    type Target = HashMap<TypeId, Vec<usize>>;
 44
 45    fn deref(&self) -> &Self::Target {
 46        &self.notifications_sent
 47    }
 48}
 49
 50impl DerefMut for NotificationTracker {
 51    fn deref_mut(&mut self) -> &mut Self::Target {
 52        &mut self.notifications_sent
 53    }
 54}
 55
 56impl NotificationTracker {
 57    fn new() -> Self {
 58        Self {
 59            notifications_sent: Default::default(),
 60        }
 61    }
 62}
 63
 64impl Workspace {
 65    pub fn has_shown_notification_once<V: Notification>(
 66        &self,
 67        id: usize,
 68        cx: &ViewContext<Self>,
 69    ) -> bool {
 70        cx.global::<NotificationTracker>()
 71            .get(&TypeId::of::<V>())
 72            .map(|ids| ids.contains(&id))
 73            .unwrap_or(false)
 74    }
 75
 76    pub fn show_notification_once<V: Notification>(
 77        &mut self,
 78        id: usize,
 79        cx: &mut ViewContext<Self>,
 80        build_notification: impl FnOnce(&mut ViewContext<Self>) -> View<V>,
 81    ) {
 82        if !self.has_shown_notification_once::<V>(id, cx) {
 83            let tracker = cx.global_mut::<NotificationTracker>();
 84            let entry = tracker.entry(TypeId::of::<V>()).or_default();
 85            entry.push(id);
 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>) -> View<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, _, _: &DismissEvent, cx| {
106                this.dismiss_notification_internal(type_id, id, cx);
107            })
108            .detach();
109            self.notifications
110                .push((type_id, id, Box::new(notification)));
111            cx.notify();
112        }
113    }
114
115    pub fn show_error<E>(&mut self, err: &E, cx: &mut ViewContext<Self>)
116    where
117        E: std::fmt::Debug,
118    {
119        self.show_notification(0, cx, |cx| {
120            cx.new_view(|_cx| {
121                simple_message_notification::MessageNotification::new(format!("Error: {err:?}"))
122            })
123        });
124    }
125
126    pub fn dismiss_notification<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
127        let type_id = TypeId::of::<V>();
128
129        self.dismiss_notification_internal(type_id, id, cx)
130    }
131
132    pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
133        self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
134        self.show_notification(toast.id, cx, |cx| {
135            cx.new_view(|_cx| match toast.on_click.as_ref() {
136                Some((click_msg, on_click)) => {
137                    let on_click = on_click.clone();
138                    simple_message_notification::MessageNotification::new(toast.msg.clone())
139                        .with_click_message(click_msg.clone())
140                        .on_click(move |cx| on_click(cx))
141                }
142                None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
143            })
144        })
145    }
146
147    pub fn dismiss_toast(&mut self, id: usize, cx: &mut ViewContext<Self>) {
148        self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
149    }
150
151    fn dismiss_notification_internal(
152        &mut self,
153        type_id: TypeId,
154        id: usize,
155        cx: &mut ViewContext<Self>,
156    ) {
157        self.notifications
158            .retain(|(existing_type_id, existing_id, _)| {
159                if (*existing_type_id, *existing_id) == (type_id, id) {
160                    cx.notify();
161                    false
162                } else {
163                    true
164                }
165            });
166    }
167}
168
169pub mod simple_message_notification {
170    use gpui::{
171        div, DismissEvent, EventEmitter, InteractiveElement, ParentElement, Render, SharedString,
172        StatefulInteractiveElement, Styled, ViewContext,
173    };
174    use std::sync::Arc;
175    use ui::prelude::*;
176    use ui::{h_stack, v_stack, Button, Icon, IconName, Label, StyledExt};
177
178    pub struct MessageNotification {
179        message: SharedString,
180        on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
181        click_message: Option<SharedString>,
182    }
183
184    impl EventEmitter<DismissEvent> for MessageNotification {}
185
186    impl MessageNotification {
187        pub fn new<S>(message: S) -> MessageNotification
188        where
189            S: Into<SharedString>,
190        {
191            Self {
192                message: message.into(),
193                on_click: None,
194                click_message: None,
195            }
196        }
197
198        pub fn with_click_message<S>(mut self, message: S) -> Self
199        where
200            S: Into<SharedString>,
201        {
202            self.click_message = Some(message.into());
203            self
204        }
205
206        pub fn on_click<F>(mut self, on_click: F) -> Self
207        where
208            F: 'static + Fn(&mut ViewContext<Self>),
209        {
210            self.on_click = Some(Arc::new(on_click));
211            self
212        }
213
214        pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
215            cx.emit(DismissEvent);
216        }
217    }
218
219    impl Render for MessageNotification {
220        fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
221            v_stack()
222                .elevation_3(cx)
223                .p_4()
224                .child(
225                    h_stack()
226                        .justify_between()
227                        .child(div().max_w_80().child(Label::new(self.message.clone())))
228                        .child(
229                            div()
230                                .id("cancel")
231                                .child(Icon::new(IconName::Close))
232                                .cursor_pointer()
233                                .on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
234                        ),
235                )
236                .children(self.click_message.iter().map(|message| {
237                    Button::new(message.clone(), message.clone()).on_click(cx.listener(
238                        |this, _, cx| {
239                            if let Some(on_click) = this.on_click.as_ref() {
240                                (on_click)(cx)
241                            };
242                            this.dismiss(cx)
243                        },
244                    ))
245                }))
246        }
247    }
248}
249
250pub trait NotifyResultExt {
251    type Ok;
252
253    fn notify_err(
254        self,
255        workspace: &mut Workspace,
256        cx: &mut ViewContext<Workspace>,
257    ) -> Option<Self::Ok>;
258
259    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
260}
261
262impl<T, E> NotifyResultExt for Result<T, E>
263where
264    E: std::fmt::Debug,
265{
266    type Ok = T;
267
268    fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<T> {
269        match self {
270            Ok(value) => Some(value),
271            Err(err) => {
272                log::error!("TODO {err:?}");
273                workspace.show_error(&err, cx);
274                None
275            }
276        }
277    }
278
279    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
280        match self {
281            Ok(value) => Some(value),
282            Err(err) => {
283                log::error!("TODO {err:?}");
284                cx.update(|view, cx| {
285                    if let Ok(workspace) = view.downcast::<Workspace>() {
286                        workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
287                    }
288                })
289                .ok();
290                None
291            }
292        }
293    }
294}
295
296pub trait NotifyTaskExt {
297    fn detach_and_notify_err(self, cx: &mut WindowContext);
298}
299
300impl<R, E> NotifyTaskExt for Task<Result<R, E>>
301where
302    E: std::fmt::Debug + 'static,
303    R: 'static,
304{
305    fn detach_and_notify_err(self, cx: &mut WindowContext) {
306        cx.spawn(|mut cx| async move { self.await.notify_async_err(&mut cx) })
307            .detach();
308    }
309}