notifications.rs

  1use std::{any::TypeId, ops::DerefMut};
  2
  3use collections::HashSet;
  4use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle};
  5
  6use crate::Workspace;
  7
  8pub fn init(cx: &mut AppContext) {
  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 as_any(&self) -> &AnyViewHandle;
 20}
 21
 22impl<T: Notification> NotificationHandle for ViewHandle<T> {
 23    fn id(&self) -> usize {
 24        self.id()
 25    }
 26
 27    fn as_any(&self) -> &AnyViewHandle {
 28        self
 29    }
 30}
 31
 32impl From<&dyn NotificationHandle> for AnyViewHandle {
 33    fn from(val: &dyn NotificationHandle) -> Self {
 34        val.as_any().clone()
 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,
142        platform::{CursorStyle, MouseButton},
143        Action, AppContext, Element, Entity, View, ViewContext,
144    };
145    use menu::Cancel;
146    use serde::Deserialize;
147    use settings::Settings;
148
149    use crate::Workspace;
150
151    use super::Notification;
152
153    actions!(message_notifications, [CancelMessageNotification]);
154
155    #[derive(Clone, Default, Deserialize, PartialEq)]
156    pub struct OsOpen(pub Cow<'static, str>);
157
158    impl OsOpen {
159        pub fn new<I: Into<Cow<'static, str>>>(url: I) -> Self {
160            OsOpen(url.into())
161        }
162    }
163
164    impl_actions!(message_notifications, [OsOpen]);
165
166    pub fn init(cx: &mut AppContext) {
167        cx.add_action(MessageNotification::dismiss);
168        cx.add_action(
169            |_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext<Workspace>| {
170                cx.platform().open_url(open_action.0.as_ref());
171            },
172        )
173    }
174
175    pub struct MessageNotification {
176        message: Cow<'static, str>,
177        click_action: Option<Box<dyn Action>>,
178        click_message: Option<Cow<'static, str>>,
179    }
180
181    pub enum MessageNotificationEvent {
182        Dismiss,
183    }
184
185    impl Entity for MessageNotification {
186        type Event = MessageNotificationEvent;
187    }
188
189    impl MessageNotification {
190        pub fn new_message<S: Into<Cow<'static, str>>>(message: S) -> MessageNotification {
191            Self {
192                message: message.into(),
193                click_action: None,
194                click_message: None,
195            }
196        }
197
198        pub fn new_boxed_action<S1: Into<Cow<'static, str>>, S2: Into<Cow<'static, str>>>(
199            message: S1,
200            click_action: Box<dyn Action>,
201            click_message: S2,
202        ) -> Self {
203            Self {
204                message: message.into(),
205                click_action: Some(click_action),
206                click_message: Some(click_message.into()),
207            }
208        }
209
210        pub fn new<S1: Into<Cow<'static, str>>, A: Action, S2: Into<Cow<'static, str>>>(
211            message: S1,
212            click_action: A,
213            click_message: S2,
214        ) -> Self {
215            Self {
216                message: message.into(),
217                click_action: Some(Box::new(click_action) as Box<dyn Action>),
218                click_message: Some(click_message.into()),
219            }
220        }
221
222        pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext<Self>) {
223            cx.emit(MessageNotificationEvent::Dismiss);
224        }
225    }
226
227    impl View for MessageNotification {
228        fn ui_name() -> &'static str {
229            "MessageNotification"
230        }
231
232        fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
233            let theme = cx.global::<Settings>().theme.clone();
234            let theme = &theme.simple_message_notification;
235
236            enum MessageNotificationTag {}
237
238            let click_action = self
239                .click_action
240                .as_ref()
241                .map(|action| action.boxed_clone());
242            let click_message = self.click_message.as_ref().map(|message| message.clone());
243            let message = self.message.clone();
244
245            let has_click_action = click_action.is_some();
246
247            MouseEventHandler::<MessageNotificationTag, _>::new(0, cx, |state, cx| {
248                Flex::column()
249                    .with_child(
250                        Flex::row()
251                            .with_child(
252                                Text::new(message, theme.message.text.clone())
253                                    .contained()
254                                    .with_style(theme.message.container)
255                                    .aligned()
256                                    .top()
257                                    .left()
258                                    .flex(1., true),
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                                })
274                                .with_padding(Padding::uniform(5.))
275                                .on_click(MouseButton::Left, move |_, _, cx| {
276                                    cx.dispatch_action(CancelMessageNotification)
277                                })
278                                .with_cursor_style(CursorStyle::PointingHand)
279                                .aligned()
280                                .constrained()
281                                .with_height(
282                                    cx.font_cache().line_height(theme.message.text.font_size),
283                                )
284                                .aligned()
285                                .top()
286                                .flex_float(),
287                            ),
288                    )
289                    .with_children({
290                        let style = theme.action_message.style_for(state, false);
291                        if let Some(click_message) = click_message {
292                            Some(
293                                Flex::row().with_child(
294                                    Text::new(click_message, style.text.clone())
295                                        .contained()
296                                        .with_style(style.container),
297                                ),
298                            )
299                        } else {
300                            None
301                        }
302                        .into_iter()
303                    })
304                    .contained()
305            })
306            // Since we're not using a proper overlay, we have to capture these extra events
307            .on_down(MouseButton::Left, |_, _, _| {})
308            .on_up(MouseButton::Left, |_, _, _| {})
309            .on_click(MouseButton::Left, move |_, _, cx| {
310                if let Some(click_action) = click_action.as_ref() {
311                    cx.dispatch_any_action(click_action.boxed_clone());
312                    cx.dispatch_action(CancelMessageNotification)
313                }
314            })
315            .with_cursor_style(if has_click_action {
316                CursorStyle::PointingHand
317            } else {
318                CursorStyle::Arrow
319            })
320            .into_any()
321        }
322    }
323
324    impl Notification for MessageNotification {
325        fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
326            match event {
327                MessageNotificationEvent::Dismiss => true,
328            }
329        }
330    }
331}
332
333pub trait NotifyResultExt {
334    type Ok;
335
336    fn notify_err(
337        self,
338        workspace: &mut Workspace,
339        cx: &mut ViewContext<Workspace>,
340    ) -> Option<Self::Ok>;
341}
342
343impl<T, E> NotifyResultExt for Result<T, E>
344where
345    E: std::fmt::Debug,
346{
347    type Ok = T;
348
349    fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<T> {
350        match self {
351            Ok(value) => Some(value),
352            Err(err) => {
353                workspace.show_notification(0, cx, |cx| {
354                    cx.add_view(|_cx| {
355                        simple_message_notification::MessageNotification::new_message(format!(
356                            "Error: {:?}",
357                            err,
358                        ))
359                    })
360                });
361
362                None
363            }
364        }
365    }
366}