notifications.rs

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