notifications.rs

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