notifications.rs

  1use crate::{Toast, Workspace};
  2use collections::HashMap;
  3use gpui::{
  4    AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Render,
  5    View, ViewContext, VisualContext,
  6};
  7use std::{any::TypeId, ops::DerefMut};
  8
  9pub fn init(cx: &mut AppContext) {
 10    cx.set_global(NotificationTracker::new());
 11    // todo!()
 12    // simple_message_notification::init(cx);
 13}
 14
 15pub trait Notification: EventEmitter<DismissEvent> + Render {}
 16
 17impl<V: EventEmitter<DismissEvent> + Render> Notification for V {}
 18
 19pub trait NotificationHandle: Send {
 20    fn id(&self) -> EntityId;
 21    fn to_any(&self) -> AnyView;
 22}
 23
 24impl<T: Notification> NotificationHandle for View<T> {
 25    fn id(&self) -> EntityId {
 26        self.entity_id()
 27    }
 28
 29    fn to_any(&self) -> AnyView {
 30        self.clone().into()
 31    }
 32}
 33
 34impl From<&dyn NotificationHandle> for AnyView {
 35    fn from(val: &dyn NotificationHandle) -> Self {
 36        val.to_any()
 37    }
 38}
 39
 40pub(crate) struct NotificationTracker {
 41    notifications_sent: HashMap<TypeId, Vec<usize>>,
 42}
 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(
108                &notification,
109                move |this, handle, event: &DismissEvent, cx| match event {
110                    DismissEvent::Dismiss => {
111                        this.dismiss_notification_internal(type_id, id, cx);
112                    }
113                },
114            )
115            .detach();
116            self.notifications
117                .push((type_id, id, Box::new(notification)));
118            cx.notify();
119        }
120    }
121
122    pub fn show_error<E>(&mut self, err: &E, cx: &mut ViewContext<Self>)
123    where
124        E: std::fmt::Debug,
125    {
126        self.show_notification(0, cx, |cx| {
127            cx.build_view(|_cx| {
128                simple_message_notification::MessageNotification::new(format!("Error: {err:?}"))
129            })
130        });
131    }
132
133    pub fn dismiss_notification<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
134        let type_id = TypeId::of::<V>();
135
136        self.dismiss_notification_internal(type_id, id, cx)
137    }
138
139    pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
140        todo!()
141        // self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
142        // self.show_notification(toast.id, cx, |cx| {
143        //     cx.add_view(|_cx| match toast.on_click.as_ref() {
144        //         Some((click_msg, on_click)) => {
145        //             let on_click = on_click.clone();
146        //             simple_message_notification::MessageNotification::new(toast.msg.clone())
147        //                 .with_click_message(click_msg.clone())
148        //                 .on_click(move |cx| on_click(cx))
149        //         }
150        //         None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
151        //     })
152        // })
153    }
154
155    pub fn dismiss_toast(&mut self, id: usize, cx: &mut ViewContext<Self>) {
156        todo!()
157        // self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
158    }
159
160    fn dismiss_notification_internal(
161        &mut self,
162        type_id: TypeId,
163        id: usize,
164        cx: &mut ViewContext<Self>,
165    ) {
166        self.notifications
167            .retain(|(existing_type_id, existing_id, _)| {
168                if (*existing_type_id, *existing_id) == (type_id, id) {
169                    cx.notify();
170                    false
171                } else {
172                    true
173                }
174            });
175    }
176}
177
178pub mod simple_message_notification {
179    use gpui::{
180        div, AnyElement, AppContext, DismissEvent, Div, EventEmitter, InteractiveElement,
181        ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, TextStyle,
182        ViewContext,
183    };
184    use serde::Deserialize;
185    use std::{borrow::Cow, sync::Arc};
186    use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt};
187
188    #[derive(Clone, Default, Deserialize, PartialEq)]
189    pub struct OsOpen(pub Cow<'static, str>);
190
191    impl OsOpen {
192        pub fn new<I: Into<Cow<'static, str>>>(url: I) -> Self {
193            OsOpen(url.into())
194        }
195    }
196
197    // todo!()
198    //     impl_actions!(message_notifications, [OsOpen]);
199    //
200    // todo!()
201    //     pub fn init(cx: &mut AppContext) {
202    //         cx.add_action(MessageNotification::dismiss);
203    //         cx.add_action(
204    //             |_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext<Workspace>| {
205    //                 cx.platform().open_url(open_action.0.as_ref());
206    //             },
207    //         )
208    //     }
209
210    enum NotificationMessage {
211        Text(SharedString),
212        Element(fn(TextStyle, &AppContext) -> AnyElement),
213    }
214
215    pub struct MessageNotification {
216        message: NotificationMessage,
217        on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>) + Send + Sync>>,
218        click_message: Option<SharedString>,
219    }
220
221    impl EventEmitter<DismissEvent> for MessageNotification {}
222
223    impl MessageNotification {
224        pub fn new<S>(message: S) -> MessageNotification
225        where
226            S: Into<SharedString>,
227        {
228            Self {
229                message: NotificationMessage::Text(message.into()),
230                on_click: None,
231                click_message: None,
232            }
233        }
234
235        // not needed I think (only for the "new panel" toast, which is outdated now)
236        // pub fn new_element(
237        //     message: fn(TextStyle, &AppContext) -> AnyElement,
238        // ) -> MessageNotification {
239        //     Self {
240        //         message: NotificationMessage::Element(message),
241        //         on_click: None,
242        //         click_message: None,
243        //     }
244        // }
245
246        pub fn with_click_message<S>(mut self, message: S) -> Self
247        where
248            S: Into<SharedString>,
249        {
250            self.click_message = Some(message.into());
251            self
252        }
253
254        pub fn on_click<F>(mut self, on_click: F) -> Self
255        where
256            F: 'static + Send + Sync + Fn(&mut ViewContext<Self>),
257        {
258            self.on_click = Some(Arc::new(on_click));
259            self
260        }
261
262        pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
263            cx.emit(DismissEvent::Dismiss);
264        }
265    }
266
267    impl Render for MessageNotification {
268        type Element = Div;
269
270        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
271            v_stack()
272                .elevation_3(cx)
273                .p_4()
274                .child(
275                    h_stack()
276                        .justify_between()
277                        .child(div().max_w_80().child(match &self.message {
278                            NotificationMessage::Text(text) => Label::new(text.clone()),
279                            NotificationMessage::Element(element) => {
280                                todo!()
281                            }
282                        }))
283                        .child(
284                            div()
285                                .id("cancel")
286                                .child(IconElement::new(Icon::Close))
287                                .cursor_pointer()
288                                .on_click(cx.listener(|this, event, cx| this.dismiss(cx))),
289                        ),
290                )
291                .children(self.click_message.iter().map(|message| {
292                    Button::new(message.clone()).on_click(cx.listener(|this, _, cx| {
293                        if let Some(on_click) = this.on_click.as_ref() {
294                            (on_click)(cx)
295                        };
296                        this.dismiss(cx)
297                    }))
298                }))
299        }
300    }
301    // todo!()
302    //     impl View for MessageNotification {
303    //         fn ui_name() -> &'static str {
304    //             "MessageNotification"
305    //         }
306
307    //         fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
308    //             let theme = theme2::current(cx).clone();
309    //             let theme = &theme.simple_message_notification;
310
311    //             enum MessageNotificationTag {}
312
313    //             let click_message = self.click_message.clone();
314    //             let message = match &self.message {
315    //                 NotificationMessage::Text(text) => {
316    //                     Text::new(text.to_owned(), theme.message.text.clone()).into_any()
317    //                 }
318    //                 NotificationMessage::Element(e) => e(theme.message.text.clone(), cx),
319    //             };
320    //             let on_click = self.on_click.clone();
321    //             let has_click_action = on_click.is_some();
322
323    //             Flex::column()
324    //                 .with_child(
325    //                     Flex::row()
326    //                         .with_child(
327    //                             message
328    //                                 .contained()
329    //                                 .with_style(theme.message.container)
330    //                                 .aligned()
331    //                                 .top()
332    //                                 .left()
333    //                                 .flex(1., true),
334    //                         )
335    //                         .with_child(
336    //                             MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
337    //                                 let style = theme.dismiss_button.style_for(state);
338    //                                 Svg::new("icons/x.svg")
339    //                                     .with_color(style.color)
340    //                                     .constrained()
341    //                                     .with_width(style.icon_width)
342    //                                     .aligned()
343    //                                     .contained()
344    //                                     .with_style(style.container)
345    //                                     .constrained()
346    //                                     .with_width(style.button_width)
347    //                                     .with_height(style.button_width)
348    //                             })
349    //                             .with_padding(Padding::uniform(5.))
350    //                             .on_click(MouseButton::Left, move |_, this, cx| {
351    //                                 this.dismiss(&Default::default(), cx);
352    //                             })
353    //                             .with_cursor_style(CursorStyle::PointingHand)
354    //                             .aligned()
355    //                             .constrained()
356    //                             .with_height(cx.font_cache().line_height(theme.message.text.font_size))
357    //                             .aligned()
358    //                             .top()
359    //                             .flex_float(),
360    //                         ),
361    //                 )
362    //                 .with_children({
363    //                     click_message
364    //                         .map(|click_message| {
365    //                             MouseEventHandler::new::<MessageNotificationTag, _>(
366    //                                 0,
367    //                                 cx,
368    //                                 |state, _| {
369    //                                     let style = theme.action_message.style_for(state);
370
371    //                                     Flex::row()
372    //                                         .with_child(
373    //                                             Text::new(click_message, style.text.clone())
374    //                                                 .contained()
375    //                                                 .with_style(style.container),
376    //                                         )
377    //                                         .contained()
378    //                                 },
379    //                             )
380    //                             .on_click(MouseButton::Left, move |_, this, cx| {
381    //                                 if let Some(on_click) = on_click.as_ref() {
382    //                                     on_click(cx);
383    //                                     this.dismiss(&Default::default(), cx);
384    //                                 }
385    //                             })
386    //                             // Since we're not using a proper overlay, we have to capture these extra events
387    //                             .on_down(MouseButton::Left, |_, _, _| {})
388    //                             .on_up(MouseButton::Left, |_, _, _| {})
389    //                             .with_cursor_style(if has_click_action {
390    //                                 CursorStyle::PointingHand
391    //                             } else {
392    //                                 CursorStyle::Arrow
393    //                             })
394    //                         })
395    //                         .into_iter()
396    //                 })
397    //                 .into_any()
398    //         }
399    //     }
400}
401
402pub trait NotifyResultExt {
403    type Ok;
404
405    fn notify_err(
406        self,
407        workspace: &mut Workspace,
408        cx: &mut ViewContext<Workspace>,
409    ) -> Option<Self::Ok>;
410
411    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
412}
413
414impl<T, E> NotifyResultExt for Result<T, E>
415where
416    E: std::fmt::Debug,
417{
418    type Ok = T;
419
420    fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<T> {
421        match self {
422            Ok(value) => Some(value),
423            Err(err) => {
424                log::error!("TODO {err:?}");
425                workspace.show_error(&err, cx);
426                None
427            }
428        }
429    }
430
431    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
432        match self {
433            Ok(value) => Some(value),
434            Err(err) => {
435                log::error!("TODO {err:?}");
436                cx.update(|view, cx| {
437                    if let Ok(workspace) = view.downcast::<Workspace>() {
438                        workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
439                    }
440                })
441                .ok();
442                None
443            }
444        }
445    }
446}