use std::{any::TypeId, ops::DerefMut}; use collections::HashSet; use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle}; use crate::Workspace; pub fn init(cx: &mut AppContext) { cx.set_global(NotificationTracker::new()); simple_message_notification::init(cx); } pub trait Notification: View { fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool; } pub trait NotificationHandle { fn id(&self) -> usize; fn as_any(&self) -> &AnyViewHandle; } impl NotificationHandle for ViewHandle { fn id(&self) -> usize { self.id() } fn as_any(&self) -> &AnyViewHandle { self } } impl From<&dyn NotificationHandle> for AnyViewHandle { fn from(val: &dyn NotificationHandle) -> Self { val.as_any().clone() } } struct NotificationTracker { notifications_sent: HashSet, } impl std::ops::Deref for NotificationTracker { type Target = HashSet; fn deref(&self) -> &Self::Target { &self.notifications_sent } } impl DerefMut for NotificationTracker { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.notifications_sent } } impl NotificationTracker { fn new() -> Self { Self { notifications_sent: HashSet::default(), } } } impl Workspace { pub fn show_notification_once( &mut self, id: usize, cx: &mut ViewContext, build_notification: impl FnOnce(&mut ViewContext) -> ViewHandle, ) { if !cx .global::() .contains(&TypeId::of::()) { cx.update_global::(|tracker, _| { tracker.insert(TypeId::of::()) }); self.show_notification::(id, cx, build_notification) } } pub fn show_notification( &mut self, id: usize, cx: &mut ViewContext, build_notification: impl FnOnce(&mut ViewContext) -> ViewHandle, ) { let type_id = TypeId::of::(); if self .notifications .iter() .all(|(existing_type_id, existing_id, _)| { (*existing_type_id, *existing_id) != (type_id, id) }) { let notification = build_notification(cx); cx.subscribe(¬ification, move |this, handle, event, cx| { if handle.read(cx).should_dismiss_notification_on_event(event) { this.dismiss_notification_internal(type_id, id, cx); } }) .detach(); self.notifications .push((type_id, id, Box::new(notification))); cx.notify(); } } pub fn dismiss_notification(&mut self, id: usize, cx: &mut ViewContext) { let type_id = TypeId::of::(); self.dismiss_notification_internal(type_id, id, cx) } fn dismiss_notification_internal( &mut self, type_id: TypeId, id: usize, cx: &mut ViewContext, ) { self.notifications .retain(|(existing_type_id, existing_id, _)| { if (*existing_type_id, *existing_id) == (type_id, id) { cx.notify(); false } else { true } }); } } pub mod simple_message_notification { use std::borrow::Cow; use gpui::{ actions, elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text}, impl_actions, platform::{CursorStyle, MouseButton}, Action, AppContext, Element, Entity, View, ViewContext, }; use menu::Cancel; use serde::Deserialize; use settings::Settings; use crate::Workspace; use super::Notification; actions!(message_notifications, [CancelMessageNotification]); #[derive(Clone, Default, Deserialize, PartialEq)] pub struct OsOpen(pub Cow<'static, str>); impl OsOpen { pub fn new>>(url: I) -> Self { OsOpen(url.into()) } } impl_actions!(message_notifications, [OsOpen]); pub fn init(cx: &mut AppContext) { cx.add_action(MessageNotification::dismiss); cx.add_action( |_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext| { cx.platform().open_url(open_action.0.as_ref()); }, ) } pub struct MessageNotification { message: Cow<'static, str>, click_action: Option>, click_message: Option>, } pub enum MessageNotificationEvent { Dismiss, } impl Entity for MessageNotification { type Event = MessageNotificationEvent; } impl MessageNotification { pub fn new_message>>(message: S) -> MessageNotification { Self { message: message.into(), click_action: None, click_message: None, } } pub fn new_boxed_action>, S2: Into>>( message: S1, click_action: Box, click_message: S2, ) -> Self { Self { message: message.into(), click_action: Some(click_action), click_message: Some(click_message.into()), } } pub fn new>, A: Action, S2: Into>>( message: S1, click_action: A, click_message: S2, ) -> Self { Self { message: message.into(), click_action: Some(Box::new(click_action) as Box), click_message: Some(click_message.into()), } } pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext) { cx.emit(MessageNotificationEvent::Dismiss); } } impl View for MessageNotification { fn ui_name() -> &'static str { "MessageNotification" } fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { let theme = cx.global::().theme.clone(); let theme = &theme.simple_message_notification; enum MessageNotificationTag {} let click_action = self .click_action .as_ref() .map(|action| action.boxed_clone()); let click_message = self.click_message.as_ref().map(|message| message.clone()); let message = self.message.clone(); let has_click_action = click_action.is_some(); MouseEventHandler::::new(0, cx, |state, cx| { Flex::column() .with_child( Flex::row() .with_child( Text::new(message, theme.message.text.clone()) .contained() .with_style(theme.message.container) .aligned() .top() .left() .flex(1., true) .boxed(), ) .with_child( MouseEventHandler::::new(0, cx, |state, _| { let style = theme.dismiss_button.style_for(state, false); Svg::new("icons/x_mark_8.svg") .with_color(style.color) .constrained() .with_width(style.icon_width) .aligned() .contained() .with_style(style.container) .constrained() .with_width(style.button_width) .with_height(style.button_width) .boxed() }) .with_padding(Padding::uniform(5.)) .on_click(MouseButton::Left, move |_, cx| { cx.dispatch_action(CancelMessageNotification) }) .with_cursor_style(CursorStyle::PointingHand) .aligned() .constrained() .with_height( cx.font_cache().line_height(theme.message.text.font_size), ) .aligned() .top() .flex_float() .boxed(), ) .boxed(), ) .with_children({ let style = theme.action_message.style_for(state, false); if let Some(click_message) = click_message { Some( Flex::row() .with_child( Text::new(click_message, style.text.clone()) .contained() .with_style(style.container) .boxed(), ) .boxed(), ) } else { None } .into_iter() }) .contained() .boxed() }) // Since we're not using a proper overlay, we have to capture these extra events .on_down(MouseButton::Left, |_, _| {}) .on_up(MouseButton::Left, |_, _| {}) .on_click(MouseButton::Left, move |_, cx| { if let Some(click_action) = click_action.as_ref() { cx.dispatch_any_action(click_action.boxed_clone()); cx.dispatch_action(CancelMessageNotification) } }) .with_cursor_style(if has_click_action { CursorStyle::PointingHand } else { CursorStyle::Arrow }) .boxed() } } impl Notification for MessageNotification { fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool { match event { MessageNotificationEvent::Dismiss => true, } } } } pub trait NotifyResultExt { type Ok; fn notify_err( self, workspace: &mut Workspace, cx: &mut ViewContext, ) -> Option; } impl NotifyResultExt for Result where E: std::fmt::Debug, { type Ok = T; fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext) -> Option { match self { Ok(value) => Some(value), Err(err) => { workspace.show_notification(0, cx, |cx| { cx.add_view(|_cx| { simple_message_notification::MessageNotification::new_message(format!( "Error: {:?}", err, )) }) }); None } } } }