notifications.rs

  1use crate::{Toast, Workspace};
  2use collections::HashMap;
  3use gpui::{AnyView, AppContext, Entity, EntityId, EventEmitter, Render, View, ViewContext};
  4use std::{any::TypeId, ops::DerefMut};
  5
  6pub fn init(cx: &mut AppContext) {
  7    cx.set_global(NotificationTracker::new());
  8    // todo!()
  9    // simple_message_notification::init(cx);
 10}
 11
 12pub trait Notification: EventEmitter + Render {
 13    fn should_dismiss_notification_on_event(&self, event: &Self::Event) -> bool;
 14}
 15
 16pub trait NotificationHandle: Send {
 17    fn id(&self) -> EntityId;
 18    fn to_any(&self) -> AnyView;
 19}
 20
 21impl<T: Notification> NotificationHandle for View<T> {
 22    fn id(&self) -> EntityId {
 23        self.entity_id()
 24    }
 25
 26    fn to_any(&self) -> AnyView {
 27        self.clone().into()
 28    }
 29}
 30
 31impl From<&dyn NotificationHandle> for AnyView {
 32    fn from(val: &dyn NotificationHandle) -> Self {
 33        val.to_any()
 34    }
 35}
 36
 37pub(crate) struct NotificationTracker {
 38    notifications_sent: HashMap<TypeId, Vec<usize>>,
 39}
 40
 41impl std::ops::Deref for NotificationTracker {
 42    type Target = HashMap<TypeId, Vec<usize>>;
 43
 44    fn deref(&self) -> &Self::Target {
 45        &self.notifications_sent
 46    }
 47}
 48
 49impl DerefMut for NotificationTracker {
 50    fn deref_mut(&mut self) -> &mut Self::Target {
 51        &mut self.notifications_sent
 52    }
 53}
 54
 55impl NotificationTracker {
 56    fn new() -> Self {
 57        Self {
 58            notifications_sent: Default::default(),
 59        }
 60    }
 61}
 62
 63impl Workspace {
 64    pub fn has_shown_notification_once<V: Notification>(
 65        &self,
 66        id: usize,
 67        cx: &ViewContext<Self>,
 68    ) -> bool {
 69        cx.global::<NotificationTracker>()
 70            .get(&TypeId::of::<V>())
 71            .map(|ids| ids.contains(&id))
 72            .unwrap_or(false)
 73    }
 74
 75    pub fn show_notification_once<V: Notification>(
 76        &mut self,
 77        id: usize,
 78        cx: &mut ViewContext<Self>,
 79        build_notification: impl FnOnce(&mut ViewContext<Self>) -> View<V>,
 80    ) {
 81        if !self.has_shown_notification_once::<V>(id, cx) {
 82            let tracker = cx.global_mut::<NotificationTracker>();
 83            let entry = tracker.entry(TypeId::of::<V>()).or_default();
 84            entry.push(id);
 85            self.show_notification::<V>(id, cx, build_notification)
 86        }
 87    }
 88
 89    pub fn show_notification<V: Notification>(
 90        &mut self,
 91        id: usize,
 92        cx: &mut ViewContext<Self>,
 93        build_notification: impl FnOnce(&mut ViewContext<Self>) -> View<V>,
 94    ) {
 95        let type_id = TypeId::of::<V>();
 96        if self
 97            .notifications
 98            .iter()
 99            .all(|(existing_type_id, existing_id, _)| {
100                (*existing_type_id, *existing_id) != (type_id, id)
101            })
102        {
103            let notification = build_notification(cx);
104            cx.subscribe(&notification, move |this, handle, event, cx| {
105                if handle.read(cx).should_dismiss_notification_on_event(event) {
106                    this.dismiss_notification_internal(type_id, id, cx);
107                }
108            })
109            .detach();
110            self.notifications
111                .push((type_id, id, Box::new(notification)));
112            cx.notify();
113        }
114    }
115
116    pub fn dismiss_notification<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
117        let type_id = TypeId::of::<V>();
118
119        self.dismiss_notification_internal(type_id, id, cx)
120    }
121
122    pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
123        todo!()
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        todo!()
140        // self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
141    }
142
143    fn dismiss_notification_internal(
144        &mut self,
145        type_id: TypeId,
146        id: usize,
147        cx: &mut ViewContext<Self>,
148    ) {
149        self.notifications
150            .retain(|(existing_type_id, existing_id, _)| {
151                if (*existing_type_id, *existing_id) == (type_id, id) {
152                    cx.notify();
153                    false
154                } else {
155                    true
156                }
157            });
158    }
159}
160
161pub mod simple_message_notification {
162    use super::Notification;
163    use gpui::{AnyElement, AppContext, Div, EventEmitter, Render, TextStyle, ViewContext};
164    use serde::Deserialize;
165    use std::{borrow::Cow, sync::Arc};
166
167    // todo!()
168    // actions!(message_notifications, [CancelMessageNotification]);
169
170    #[derive(Clone, Default, Deserialize, PartialEq)]
171    pub struct OsOpen(pub Cow<'static, str>);
172
173    impl OsOpen {
174        pub fn new<I: Into<Cow<'static, str>>>(url: I) -> Self {
175            OsOpen(url.into())
176        }
177    }
178
179    // todo!()
180    //     impl_actions!(message_notifications, [OsOpen]);
181    //
182    // todo!()
183    //     pub fn init(cx: &mut AppContext) {
184    //         cx.add_action(MessageNotification::dismiss);
185    //         cx.add_action(
186    //             |_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext<Workspace>| {
187    //                 cx.platform().open_url(open_action.0.as_ref());
188    //             },
189    //         )
190    //     }
191
192    enum NotificationMessage {
193        Text(Cow<'static, str>),
194        Element(fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>),
195    }
196
197    pub struct MessageNotification {
198        message: NotificationMessage,
199        on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>) + Send + Sync>>,
200        click_message: Option<Cow<'static, str>>,
201    }
202
203    pub enum MessageNotificationEvent {
204        Dismiss,
205    }
206
207    impl EventEmitter for MessageNotification {
208        type Event = MessageNotificationEvent;
209    }
210
211    impl MessageNotification {
212        pub fn new<S>(message: S) -> MessageNotification
213        where
214            S: Into<Cow<'static, str>>,
215        {
216            Self {
217                message: NotificationMessage::Text(message.into()),
218                on_click: None,
219                click_message: None,
220            }
221        }
222
223        pub fn new_element(
224            message: fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>,
225        ) -> MessageNotification {
226            Self {
227                message: NotificationMessage::Element(message),
228                on_click: None,
229                click_message: None,
230            }
231        }
232
233        pub fn with_click_message<S>(mut self, message: S) -> Self
234        where
235            S: Into<Cow<'static, str>>,
236        {
237            self.click_message = Some(message.into());
238            self
239        }
240
241        pub fn on_click<F>(mut self, on_click: F) -> Self
242        where
243            F: 'static + Send + Sync + Fn(&mut ViewContext<Self>),
244        {
245            self.on_click = Some(Arc::new(on_click));
246            self
247        }
248
249        // todo!()
250        // pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext<Self>) {
251        //     cx.emit(MessageNotificationEvent::Dismiss);
252        // }
253    }
254
255    impl Render for MessageNotification {
256        type Element = Div<Self>;
257
258        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
259            todo!()
260        }
261    }
262    // todo!()
263    //     impl View for MessageNotification {
264    //         fn ui_name() -> &'static str {
265    //             "MessageNotification"
266    //         }
267
268    //         fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
269    //             let theme = theme2::current(cx).clone();
270    //             let theme = &theme.simple_message_notification;
271
272    //             enum MessageNotificationTag {}
273
274    //             let click_message = self.click_message.clone();
275    //             let message = match &self.message {
276    //                 NotificationMessage::Text(text) => {
277    //                     Text::new(text.to_owned(), theme.message.text.clone()).into_any()
278    //                 }
279    //                 NotificationMessage::Element(e) => e(theme.message.text.clone(), cx),
280    //             };
281    //             let on_click = self.on_click.clone();
282    //             let has_click_action = on_click.is_some();
283
284    //             Flex::column()
285    //                 .with_child(
286    //                     Flex::row()
287    //                         .with_child(
288    //                             message
289    //                                 .contained()
290    //                                 .with_style(theme.message.container)
291    //                                 .aligned()
292    //                                 .top()
293    //                                 .left()
294    //                                 .flex(1., true),
295    //                         )
296    //                         .with_child(
297    //                             MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
298    //                                 let style = theme.dismiss_button.style_for(state);
299    //                                 Svg::new("icons/x.svg")
300    //                                     .with_color(style.color)
301    //                                     .constrained()
302    //                                     .with_width(style.icon_width)
303    //                                     .aligned()
304    //                                     .contained()
305    //                                     .with_style(style.container)
306    //                                     .constrained()
307    //                                     .with_width(style.button_width)
308    //                                     .with_height(style.button_width)
309    //                             })
310    //                             .with_padding(Padding::uniform(5.))
311    //                             .on_click(MouseButton::Left, move |_, this, cx| {
312    //                                 this.dismiss(&Default::default(), cx);
313    //                             })
314    //                             .with_cursor_style(CursorStyle::PointingHand)
315    //                             .aligned()
316    //                             .constrained()
317    //                             .with_height(cx.font_cache().line_height(theme.message.text.font_size))
318    //                             .aligned()
319    //                             .top()
320    //                             .flex_float(),
321    //                         ),
322    //                 )
323    //                 .with_children({
324    //                     click_message
325    //                         .map(|click_message| {
326    //                             MouseEventHandler::new::<MessageNotificationTag, _>(
327    //                                 0,
328    //                                 cx,
329    //                                 |state, _| {
330    //                                     let style = theme.action_message.style_for(state);
331
332    //                                     Flex::row()
333    //                                         .with_child(
334    //                                             Text::new(click_message, style.text.clone())
335    //                                                 .contained()
336    //                                                 .with_style(style.container),
337    //                                         )
338    //                                         .contained()
339    //                                 },
340    //                             )
341    //                             .on_click(MouseButton::Left, move |_, this, cx| {
342    //                                 if let Some(on_click) = on_click.as_ref() {
343    //                                     on_click(cx);
344    //                                     this.dismiss(&Default::default(), cx);
345    //                                 }
346    //                             })
347    //                             // Since we're not using a proper overlay, we have to capture these extra events
348    //                             .on_down(MouseButton::Left, |_, _, _| {})
349    //                             .on_up(MouseButton::Left, |_, _, _| {})
350    //                             .with_cursor_style(if has_click_action {
351    //                                 CursorStyle::PointingHand
352    //                             } else {
353    //                                 CursorStyle::Arrow
354    //                             })
355    //                         })
356    //                         .into_iter()
357    //                 })
358    //                 .into_any()
359    //         }
360    //     }
361
362    impl Notification for MessageNotification {
363        fn should_dismiss_notification_on_event(&self, event: &Self::Event) -> bool {
364            match event {
365                MessageNotificationEvent::Dismiss => true,
366            }
367        }
368    }
369}
370
371pub trait NotifyResultExt {
372    type Ok;
373
374    fn notify_err(
375        self,
376        workspace: &mut Workspace,
377        cx: &mut ViewContext<Workspace>,
378    ) -> Option<Self::Ok>;
379}
380
381impl<T, E> NotifyResultExt for Result<T, E>
382where
383    E: std::fmt::Debug,
384{
385    type Ok = T;
386
387    fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<T> {
388        match self {
389            Ok(value) => Some(value),
390            Err(err) => {
391                log::error!("TODO {err:?}");
392                // todo!()
393                // workspace.show_notification(0, cx, |cx| {
394                //     cx.add_view(|_cx| {
395                //         simple_message_notification::MessageNotification::new(format!(
396                //             "Error: {err:?}",
397                //         ))
398                //     })
399                // });
400                None
401            }
402        }
403    }
404}