gpui2: Notifications (#3407)

Conrad Irwin created

Release Notes:

- N/A

Change summary

crates/auto_update2/src/update_notification.rs |   9 
crates/command_palette2/src/command_palette.rs |   9 
crates/file_finder2/src/file_finder.rs         |  10 
crates/go_to_line2/src/go_to_line.rs           |  13 +
crates/gpui2/src/app/async_context.rs          |   8 
crates/gpui2/src/app/test_context.rs           |   2 
crates/gpui2/src/window.rs                     |  10 
crates/ui2/src/components/context_menu.rs      |  16 +-
crates/workspace/src/workspace.rs              |  20 ++
crates/workspace2/src/notifications.rs         | 133 +++++++++++++------
crates/workspace2/src/workspace2.rs            |  71 ++++++----
11 files changed, 190 insertions(+), 111 deletions(-)

Detailed changes

crates/auto_update2/src/update_notification.rs 🔗

@@ -1,12 +1,13 @@
-use gpui::{div, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext};
+use gpui::{
+    div, DismissEvent, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext,
+};
 use menu::Cancel;
-use workspace::notifications::NotificationEvent;
 
 pub struct UpdateNotification {
     _version: SemanticVersion,
 }
 
-impl EventEmitter<NotificationEvent> for UpdateNotification {}
+impl EventEmitter<DismissEvent> for UpdateNotification {}
 
 impl Render for UpdateNotification {
     type Element = Div;
@@ -82,6 +83,6 @@ impl UpdateNotification {
     }
 
     pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(NotificationEvent::Dismiss);
+        cx.emit(DismissEvent::Dismiss);
     }
 }

crates/command_palette2/src/command_palette.rs 🔗

@@ -1,8 +1,9 @@
 use collections::{CommandPaletteFilter, HashMap};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, div, prelude::*, Action, AppContext, Div, EventEmitter, FocusHandle, FocusableView,
-    Keystroke, Manager, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
+    actions, div, prelude::*, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
+    FocusableView, Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext,
+    WeakView,
 };
 use picker::{Picker, PickerDelegate};
 use std::{
@@ -68,7 +69,7 @@ impl CommandPalette {
     }
 }
 
-impl EventEmitter<Manager> for CommandPalette {}
+impl EventEmitter<DismissEvent> for CommandPalette {}
 
 impl FocusableView for CommandPalette {
     fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
@@ -268,7 +269,7 @@ impl PickerDelegate for CommandPaletteDelegate {
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
         self.command_palette
-            .update(cx, |_, cx| cx.emit(Manager::Dismiss))
+            .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
             .log_err();
     }
 

crates/file_finder2/src/file_finder.rs 🔗

@@ -2,8 +2,8 @@ use collections::HashMap;
 use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
 use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
 use gpui::{
-    actions, div, AppContext, Div, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
-    IntoElement, Manager, Model, ParentElement, Render, Styled, Task, View, ViewContext,
+    actions, div, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
+    InteractiveElement, IntoElement, Model, ParentElement, Render, Styled, Task, View, ViewContext,
     VisualContext, WeakView,
 };
 use picker::{Picker, PickerDelegate};
@@ -111,7 +111,7 @@ impl FileFinder {
     }
 }
 
-impl EventEmitter<Manager> for FileFinder {}
+impl EventEmitter<DismissEvent> for FileFinder {}
 impl FocusableView for FileFinder {
     fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
         self.picker.focus_handle(cx)
@@ -690,7 +690,7 @@ impl PickerDelegate for FileFinderDelegate {
                         }
                     }
                     finder
-                        .update(&mut cx, |_, cx| cx.emit(Manager::Dismiss))
+                        .update(&mut cx, |_, cx| cx.emit(DismissEvent::Dismiss))
                         .ok()?;
 
                     Some(())
@@ -702,7 +702,7 @@ impl PickerDelegate for FileFinderDelegate {
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
         self.file_finder
-            .update(cx, |_, cx| cx.emit(Manager::Dismiss))
+            .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
             .log_err();
     }
 

crates/go_to_line2/src/go_to_line.rs 🔗

@@ -1,7 +1,8 @@
 use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
 use gpui::{
-    actions, div, prelude::*, AppContext, Div, EventEmitter, FocusHandle, FocusableView, Manager,
-    Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
+    actions, div, prelude::*, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
+    FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
+    WindowContext,
 };
 use text::{Bias, Point};
 use theme::ActiveTheme;
@@ -28,7 +29,7 @@ impl FocusableView for GoToLine {
         self.active_editor.focus_handle(cx)
     }
 }
-impl EventEmitter<Manager> for GoToLine {}
+impl EventEmitter<DismissEvent> for GoToLine {}
 
 impl GoToLine {
     fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
@@ -88,7 +89,7 @@ impl GoToLine {
     ) {
         match event {
             // todo!() this isn't working...
-            editor::EditorEvent::Blurred => cx.emit(Manager::Dismiss),
+            editor::EditorEvent::Blurred => cx.emit(DismissEvent::Dismiss),
             editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
             _ => {}
         }
@@ -123,7 +124,7 @@ impl GoToLine {
     }
 
     fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(Manager::Dismiss);
+        cx.emit(DismissEvent::Dismiss);
     }
 
     fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
@@ -140,7 +141,7 @@ impl GoToLine {
             self.prev_scroll_position.take();
         }
 
-        cx.emit(Manager::Dismiss);
+        cx.emit(DismissEvent::Dismiss);
     }
 }
 

crates/gpui2/src/app/async_context.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
-    AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView,
-    ForegroundExecutor, Manager, Model, ModelContext, Render, Result, Task, View, ViewContext,
-    VisualContext, WindowContext, WindowHandle,
+    AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, DismissEvent,
+    FocusableView, ForegroundExecutor, Model, ModelContext, Render, Result, Task, View,
+    ViewContext, VisualContext, WindowContext, WindowHandle,
 };
 use anyhow::{anyhow, Context as _};
 use derive_more::{Deref, DerefMut};
@@ -326,7 +326,7 @@ impl VisualContext for AsyncWindowContext {
         V: crate::ManagedView,
     {
         self.window.update(self, |_, cx| {
-            view.update(cx, |_, cx| cx.emit(Manager::Dismiss))
+            view.update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
         })
     }
 }

crates/gpui2/src/app/test_context.rs 🔗

@@ -611,7 +611,7 @@ impl<'a> VisualContext for VisualTestContext<'a> {
     {
         self.window
             .update(self.cx, |_, cx| {
-                view.update(cx, |_, cx| cx.emit(crate::Manager::Dismiss))
+                view.update(cx, |_, cx| cx.emit(crate::DismissEvent::Dismiss))
             })
             .unwrap()
     }

crates/gpui2/src/window.rs 🔗

@@ -193,11 +193,11 @@ pub trait FocusableView: 'static + Render {
 
 /// ManagedView is a view (like a Modal, Popover, Menu, etc.)
 /// where the lifecycle of the view is handled by another view.
-pub trait ManagedView: FocusableView + EventEmitter<Manager> {}
+pub trait ManagedView: FocusableView + EventEmitter<DismissEvent> {}
 
-impl<M: FocusableView + EventEmitter<Manager>> ManagedView for M {}
+impl<M: FocusableView + EventEmitter<DismissEvent>> ManagedView for M {}
 
-pub enum Manager {
+pub enum DismissEvent {
     Dismiss,
 }
 
@@ -1663,7 +1663,7 @@ impl VisualContext for WindowContext<'_> {
     where
         V: ManagedView,
     {
-        self.update_view(view, |_, cx| cx.emit(Manager::Dismiss))
+        self.update_view(view, |_, cx| cx.emit(DismissEvent::Dismiss))
     }
 }
 
@@ -2349,7 +2349,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     where
         V: ManagedView,
     {
-        self.defer(|_, cx| cx.emit(Manager::Dismiss))
+        self.defer(|_, cx| cx.emit(DismissEvent::Dismiss))
     }
 
     pub fn listener<E>(

crates/ui2/src/components/context_menu.rs 🔗

@@ -4,9 +4,9 @@ use std::rc::Rc;
 use crate::{prelude::*, v_stack, Label, List};
 use crate::{ListItem, ListSeparator, ListSubHeader};
 use gpui::{
-    overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DispatchPhase,
-    Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, ManagedView, Manager,
-    MouseButton, MouseDownEvent, Pixels, Point, Render, View, VisualContext,
+    overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DismissEvent,
+    DispatchPhase, Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId,
+    ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, VisualContext,
 };
 
 pub enum ContextMenuItem {
@@ -26,7 +26,7 @@ impl FocusableView for ContextMenu {
     }
 }
 
-impl EventEmitter<Manager> for ContextMenu {}
+impl EventEmitter<DismissEvent> for ContextMenu {}
 
 impl ContextMenu {
     pub fn build(
@@ -74,11 +74,11 @@ impl ContextMenu {
 
     pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
         // todo!()
-        cx.emit(Manager::Dismiss);
+        cx.emit(DismissEvent::Dismiss);
     }
 
     pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(Manager::Dismiss);
+        cx.emit(DismissEvent::Dismiss);
     }
 }
 
@@ -111,7 +111,7 @@ impl Render for ContextMenu {
                         }
                         ContextMenuItem::Entry(entry, callback) => {
                             let callback = callback.clone();
-                            let dismiss = cx.listener(|_, _, cx| cx.emit(Manager::Dismiss));
+                            let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent::Dismiss));
 
                             ListItem::new(entry.clone())
                                 .child(Label::new(entry.clone()))
@@ -265,7 +265,7 @@ impl<M: ManagedView> Element for MenuHandle<M> {
                 let new_menu = (builder)(cx);
                 let menu2 = menu.clone();
                 cx.subscribe(&new_menu, move |modal, e, cx| match e {
-                    &Manager::Dismiss => {
+                    &DismissEvent::Dismiss => {
                         *menu2.borrow_mut() = None;
                         cx.notify();
                     }

crates/workspace/src/workspace.rs 🔗

@@ -63,7 +63,7 @@ use crate::{
 };
 use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
 use lazy_static::lazy_static;
-use notifications::{NotificationHandle, NotifyResultExt};
+use notifications::{simple_message_notification, NotificationHandle, NotifyResultExt};
 pub use pane::*;
 pub use pane_group::*;
 use persistence::{model::SerializedItem, DB};
@@ -776,7 +776,23 @@ impl Workspace {
             }),
         ];
 
-        cx.defer(|this, cx| this.update_window_title(cx));
+        cx.defer(|this, cx| {
+            this.update_window_title(cx);
+
+            this.show_notification(0, cx, |cx| {
+                cx.add_view(|_cx| {
+                    simple_message_notification::MessageNotification::new(format!(
+                        "Error: what happens if this message is very very very very very long "
+                    ))
+                    .with_click_message("Click here because!")
+                })
+            });
+            this.show_notification(1, cx, |cx| {
+                cx.add_view(|_cx| {
+                    simple_message_notification::MessageNotification::new(format!("Nope"))
+                })
+            });
+        });
         Workspace {
             weak_self: weak_handle.clone(),
             modal: None,

crates/workspace2/src/notifications.rs 🔗

@@ -1,6 +1,9 @@
 use crate::{Toast, Workspace};
 use collections::HashMap;
-use gpui::{AnyView, AppContext, Entity, EntityId, EventEmitter, Render, View, ViewContext};
+use gpui::{
+    AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Render,
+    View, ViewContext, VisualContext,
+};
 use std::{any::TypeId, ops::DerefMut};
 
 pub fn init(cx: &mut AppContext) {
@@ -9,13 +12,9 @@ pub fn init(cx: &mut AppContext) {
     // simple_message_notification::init(cx);
 }
 
-pub enum NotificationEvent {
-    Dismiss,
-}
-
-pub trait Notification: EventEmitter<NotificationEvent> + Render {}
+pub trait Notification: EventEmitter<DismissEvent> + Render {}
 
-impl<V: EventEmitter<NotificationEvent> + Render> Notification for V {}
+impl<V: EventEmitter<DismissEvent> + Render> Notification for V {}
 
 pub trait NotificationHandle: Send {
     fn id(&self) -> EntityId;
@@ -107,8 +106,8 @@ impl Workspace {
             let notification = build_notification(cx);
             cx.subscribe(
                 &notification,
-                move |this, handle, event: &NotificationEvent, cx| match event {
-                    NotificationEvent::Dismiss => {
+                move |this, handle, event: &DismissEvent, cx| match event {
+                    DismissEvent::Dismiss => {
                         this.dismiss_notification_internal(type_id, id, cx);
                     }
                 },
@@ -120,6 +119,17 @@ impl Workspace {
         }
     }
 
+    pub fn show_error<E>(&mut self, err: &E, cx: &mut ViewContext<Self>)
+    where
+        E: std::fmt::Debug,
+    {
+        self.show_notification(0, cx, |cx| {
+            cx.build_view(|_cx| {
+                simple_message_notification::MessageNotification::new(format!("Error: {err:?}"))
+            })
+        });
+    }
+
     pub fn dismiss_notification<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
         let type_id = TypeId::of::<V>();
 
@@ -166,13 +176,14 @@ impl Workspace {
 }
 
 pub mod simple_message_notification {
-    use super::NotificationEvent;
-    use gpui::{AnyElement, AppContext, Div, EventEmitter, Render, TextStyle, ViewContext};
+    use gpui::{
+        div, AnyElement, AppContext, DismissEvent, Div, EventEmitter, InteractiveElement,
+        ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, TextStyle,
+        ViewContext,
+    };
     use serde::Deserialize;
     use std::{borrow::Cow, sync::Arc};
-
-    // todo!()
-    // actions!(message_notifications, [CancelMessageNotification]);
+    use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt};
 
     #[derive(Clone, Default, Deserialize, PartialEq)]
     pub struct OsOpen(pub Cow<'static, str>);
@@ -197,22 +208,22 @@ pub mod simple_message_notification {
     //     }
 
     enum NotificationMessage {
-        Text(Cow<'static, str>),
+        Text(SharedString),
         Element(fn(TextStyle, &AppContext) -> AnyElement),
     }
 
     pub struct MessageNotification {
         message: NotificationMessage,
         on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>) + Send + Sync>>,
-        click_message: Option<Cow<'static, str>>,
+        click_message: Option<SharedString>,
     }
 
-    impl EventEmitter<NotificationMessage> for MessageNotification {}
+    impl EventEmitter<DismissEvent> for MessageNotification {}
 
     impl MessageNotification {
         pub fn new<S>(message: S) -> MessageNotification
         where
-            S: Into<Cow<'static, str>>,
+            S: Into<SharedString>,
         {
             Self {
                 message: NotificationMessage::Text(message.into()),
@@ -221,19 +232,20 @@ pub mod simple_message_notification {
             }
         }
 
-        pub fn new_element(
-            message: fn(TextStyle, &AppContext) -> AnyElement,
-        ) -> MessageNotification {
-            Self {
-                message: NotificationMessage::Element(message),
-                on_click: None,
-                click_message: None,
-            }
-        }
+        // not needed I think (only for the "new panel" toast, which is outdated now)
+        // pub fn new_element(
+        //     message: fn(TextStyle, &AppContext) -> AnyElement,
+        // ) -> MessageNotification {
+        //     Self {
+        //         message: NotificationMessage::Element(message),
+        //         on_click: None,
+        //         click_message: None,
+        //     }
+        // }
 
         pub fn with_click_message<S>(mut self, message: S) -> Self
         where
-            S: Into<Cow<'static, str>>,
+            S: Into<SharedString>,
         {
             self.click_message = Some(message.into());
             self
@@ -247,17 +259,43 @@ pub mod simple_message_notification {
             self
         }
 
-        // todo!()
-        // pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext<Self>) {
-        //     cx.emit(MessageNotificationEvent::Dismiss);
-        // }
+        pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+            cx.emit(DismissEvent::Dismiss);
+        }
     }
 
     impl Render for MessageNotification {
         type Element = Div;
 
         fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-            todo!()
+            v_stack()
+                .elevation_3(cx)
+                .p_4()
+                .child(
+                    h_stack()
+                        .justify_between()
+                        .child(div().max_w_80().child(match &self.message {
+                            NotificationMessage::Text(text) => Label::new(text.clone()),
+                            NotificationMessage::Element(element) => {
+                                todo!()
+                            }
+                        }))
+                        .child(
+                            div()
+                                .id("cancel")
+                                .child(IconElement::new(Icon::Close))
+                                .cursor_pointer()
+                                .on_click(cx.listener(|this, event, cx| this.dismiss(cx))),
+                        ),
+                )
+                .children(self.click_message.iter().map(|message| {
+                    Button::new(message.clone()).on_click(cx.listener(|this, _, cx| {
+                        if let Some(on_click) = this.on_click.as_ref() {
+                            (on_click)(cx)
+                        };
+                        this.dismiss(cx)
+                    }))
+                }))
         }
     }
     // todo!()
@@ -359,8 +397,6 @@ pub mod simple_message_notification {
     //                 .into_any()
     //         }
     //     }
-
-    impl EventEmitter<NotificationEvent> for MessageNotification {}
 }
 
 pub trait NotifyResultExt {
@@ -371,6 +407,8 @@ pub trait NotifyResultExt {
         workspace: &mut Workspace,
         cx: &mut ViewContext<Workspace>,
     ) -> Option<Self::Ok>;
+
+    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
 }
 
 impl<T, E> NotifyResultExt for Result<T, E>
@@ -384,14 +422,23 @@ where
             Ok(value) => Some(value),
             Err(err) => {
                 log::error!("TODO {err:?}");
-                // todo!()
-                // workspace.show_notification(0, cx, |cx| {
-                //     cx.add_view(|_cx| {
-                //         simple_message_notification::MessageNotification::new(format!(
-                //             "Error: {err:?}",
-                //         ))
-                //     })
-                // });
+                workspace.show_error(&err, cx);
+                None
+            }
+        }
+    }
+
+    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
+        match self {
+            Ok(value) => Some(value),
+            Err(err) => {
+                log::error!("TODO {err:?}");
+                cx.update(|view, cx| {
+                    if let Ok(workspace) = view.downcast::<Workspace>() {
+                        workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
+                    }
+                })
+                .ok();
                 None
             }
         }

crates/workspace2/src/workspace2.rs 🔗

@@ -683,7 +683,21 @@ impl Workspace {
             }),
         ];
 
-        cx.defer(|this, cx| this.update_window_title(cx));
+        cx.defer(|this, cx| {
+            this.update_window_title(cx);
+            // todo! @nate - these are useful for testing notifications
+            // this.show_error(
+            //     &anyhow::anyhow!("what happens if this message is very very very very very long"),
+            //     cx,
+            // );
+
+            // this.show_notification(1, cx, |cx| {
+            //     cx.build_view(|_cx| {
+            //         simple_message_notification::MessageNotification::new(format!("Error:"))
+            //             .with_click_message("click here because!")
+            //     })
+            // });
+        });
         Workspace {
             window_self: window_handle,
             weak_self: weak_handle.clone(),
@@ -2566,32 +2580,31 @@ impl Workspace {
     //         }
     //     }
 
-    //     fn render_notifications(
-    //         &self,
-    //         theme: &theme::Workspace,
-    //         cx: &AppContext,
-    //     ) -> Option<AnyElement<Workspace>> {
-    //         if self.notifications.is_empty() {
-    //             None
-    //         } else {
-    //             Some(
-    //                 Flex::column()
-    //                     .with_children(self.notifications.iter().map(|(_, _, notification)| {
-    //                         ChildView::new(notification.as_any(), cx)
-    //                             .contained()
-    //                             .with_style(theme.notification)
-    //                     }))
-    //                     .constrained()
-    //                     .with_width(theme.notifications.width)
-    //                     .contained()
-    //                     .with_style(theme.notifications.container)
-    //                     .aligned()
-    //                     .bottom()
-    //                     .right()
-    //                     .into_any(),
-    //             )
-    //         }
-    //     }
+    fn render_notifications(&self, cx: &ViewContext<Self>) -> Option<Div> {
+        if self.notifications.is_empty() {
+            None
+        } else {
+            Some(
+                div()
+                    .absolute()
+                    .z_index(100)
+                    .right_3()
+                    .bottom_3()
+                    .w_96()
+                    .h_full()
+                    .flex()
+                    .flex_col()
+                    .justify_end()
+                    .gap_2()
+                    .children(self.notifications.iter().map(|(_, _, notification)| {
+                        div()
+                            .on_any_mouse_down(|_, cx| cx.stop_propagation())
+                            .on_any_mouse_up(|_, cx| cx.stop_propagation())
+                            .child(notification.to_any())
+                    })),
+            )
+        }
+    }
 
     //     // RPC handlers
 
@@ -3653,7 +3666,6 @@ impl Render for Workspace {
             .bg(cx.theme().colors().background)
             .children(self.titlebar_item.clone())
             .child(
-                // todo! should this be a component a view?
                 div()
                     .id("workspace")
                     .relative()
@@ -3703,7 +3715,8 @@ impl Render for Workspace {
                                     .overflow_hidden()
                                     .child(self.right_dock.clone()),
                             ),
-                    ),
+                    )
+                    .children(self.render_notifications(cx)),
             )
             .child(self.status_bar.clone())
     }