From 039c933d8e51df7cbeec87af8e373dfee9b196ac Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sun, 26 Nov 2023 22:27:33 -0700 Subject: [PATCH] gpui2: Notifications --- .../auto_update2/src/update_notification.rs | 9 +- .../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(-) diff --git a/crates/auto_update2/src/update_notification.rs b/crates/auto_update2/src/update_notification.rs index e6a22b7324..9cb1550bd4 100644 --- a/crates/auto_update2/src/update_notification.rs +++ b/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 for UpdateNotification {} +impl EventEmitter for UpdateNotification {} impl Render for UpdateNotification { type Element = Div; @@ -82,6 +83,6 @@ impl UpdateNotification { } pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext) { - cx.emit(NotificationEvent::Dismiss); + cx.emit(DismissEvent::Dismiss); } } diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 3c6f2fff92..07b819d3a1 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/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 for CommandPalette {} +impl EventEmitter 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>) { self.command_palette - .update(cx, |_, cx| cx.emit(Manager::Dismiss)) + .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss)) .log_err(); } diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 873054a68c..ea578fbb0e 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/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 for FileFinder {} +impl EventEmitter 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>) { self.file_finder - .update(cx, |_, cx| cx.emit(Manager::Dismiss)) + .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss)) .log_err(); } diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 61f5742750..d1119de9b4 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/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 for GoToLine {} +impl EventEmitter for GoToLine {} impl GoToLine { fn register(workspace: &mut Workspace, _: &mut ViewContext) { @@ -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) { - cx.emit(Manager::Dismiss); + cx.emit(DismissEvent::Dismiss); } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { @@ -140,7 +141,7 @@ impl GoToLine { self.prev_scroll_position.take(); } - cx.emit(Manager::Dismiss); + cx.emit(DismissEvent::Dismiss); } } diff --git a/crates/gpui2/src/app/async_context.rs b/crates/gpui2/src/app/async_context.rs index cc3b0ace57..11420bee69 100644 --- a/crates/gpui2/src/app/async_context.rs +++ b/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)) }) } } diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 2bd3a069ca..71bc8e3d81 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/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() } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 7b39089ae0..de86e42757 100644 --- a/crates/gpui2/src/window.rs +++ b/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 {} +pub trait ManagedView: FocusableView + EventEmitter {} -impl> ManagedView for M {} +impl> 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( diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index d5adaf586b..a92c08d82f 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/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 for ContextMenu {} +impl EventEmitter for ContextMenu {} impl ContextMenu { pub fn build( @@ -74,11 +74,11 @@ impl ContextMenu { pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { // todo!() - cx.emit(Manager::Dismiss); + cx.emit(DismissEvent::Dismiss); } pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - 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 Element for MenuHandle { 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(); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index a19d2c5b58..268c4f2ca0 100644 --- a/crates/workspace/src/workspace.rs +++ b/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, diff --git a/crates/workspace2/src/notifications.rs b/crates/workspace2/src/notifications.rs index 9b8557c62c..def13c518e 100644 --- a/crates/workspace2/src/notifications.rs +++ b/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 + Render {} -pub trait Notification: EventEmitter + Render {} - -impl + Render> Notification for V {} +impl + 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( ¬ification, - 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(&mut self, err: &E, cx: &mut ViewContext) + 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(&mut self, id: usize, cx: &mut ViewContext) { let type_id = TypeId::of::(); @@ -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) + Send + Sync>>, - click_message: Option>, + click_message: Option, } - impl EventEmitter for MessageNotification {} + impl EventEmitter for MessageNotification {} impl MessageNotification { pub fn new(message: S) -> MessageNotification where - S: Into>, + S: Into, { 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(mut self, message: S) -> Self where - S: Into>, + S: Into, { 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) { - // cx.emit(MessageNotificationEvent::Dismiss); - // } + pub fn dismiss(&mut self, cx: &mut ViewContext) { + cx.emit(DismissEvent::Dismiss); + } } impl Render for MessageNotification { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> 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 for MessageNotification {} } pub trait NotifyResultExt { @@ -371,6 +407,8 @@ pub trait NotifyResultExt { workspace: &mut Workspace, cx: &mut ViewContext, ) -> Option; + + fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option; } impl NotifyResultExt for Result @@ -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 { + match self { + Ok(value) => Some(value), + Err(err) => { + log::error!("TODO {err:?}"); + cx.update(|view, cx| { + if let Ok(workspace) = view.downcast::() { + workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx)) + } + }) + .ok(); None } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index b09b47d24c..5480ac4d3c 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/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> { - // 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) -> Option
{ + 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()) }