use std::{any::TypeId, collections::VecDeque, sync::Arc, time::Duration}; use gpui::{ div, prelude::FluentBuilder, px, Animation, AnimationExt, ClickEvent, DismissEvent, ElementId, EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, View, ViewContext, VisualContext, WindowContext, }; use smol::Timer; use crate::{ button::Button, h_flex, theme::ActiveTheme as _, v_flex, Icon, IconName, Sizable as _, StyledExt, }; pub enum NotificationType { Info, Success, Warning, Error, } #[derive(Debug, PartialEq, Clone)] pub(crate) enum NotificationId { Id(TypeId), IdAndElementId(TypeId, ElementId), } impl From for NotificationId { fn from(type_id: TypeId) -> Self { Self::Id(type_id) } } impl From<(TypeId, ElementId)> for NotificationId { fn from((type_id, id): (TypeId, ElementId)) -> Self { Self::IdAndElementId(type_id, id) } } pub struct Notification { /// The id is used make the notification unique. /// Then you push a notification with the same id, the previous notification will be replaced. /// /// None means the notification will be added to the end of the list. id: NotificationId, type_: NotificationType, title: Option, message: SharedString, icon: Option, autohide: bool, on_click: Option>, } impl From for Notification { fn from(s: SharedString) -> Self { Self::new(s) } } impl From<&'static str> for Notification { fn from(s: &'static str) -> Self { Self::new(s) } } impl From<(NotificationType, &'static str)> for Notification { fn from((type_, content): (NotificationType, &'static str)) -> Self { Self::new(content).with_type(type_) } } impl From<(NotificationType, SharedString)> for Notification { fn from((type_, content): (NotificationType, SharedString)) -> Self { Self::new(content).with_type(type_) } } struct DefaultIdType; impl Notification { /// Create a new notification with the given content. /// /// default width is 320px. pub fn new(message: impl Into) -> Self { let id: SharedString = uuid::Uuid::new_v4().to_string().into(); let id = (TypeId::of::(), id.into()); Self { id: id.into(), title: None, message: message.into(), type_: NotificationType::Info, icon: None, autohide: true, on_click: None, } } pub fn info(message: impl Into) -> Self { Self::new(message).with_type(NotificationType::Info) } pub fn success(message: impl Into) -> Self { Self::new(message).with_type(NotificationType::Success) } pub fn warning(message: impl Into) -> Self { Self::new(message).with_type(NotificationType::Warning) } pub fn error(message: impl Into) -> Self { Self::new(message).with_type(NotificationType::Error) } /// Set the type for unique identification of the notification. /// /// ```rs /// struct MyNotificationKind; /// let notification = Notification::new("Hello").id::(); /// ``` pub fn id(mut self) -> Self { self.id = TypeId::of::().into(); self } /// Set the type and id of the notification, used to uniquely identify the notification. pub fn id1(mut self, key: impl Into) -> Self { self.id = (TypeId::of::(), key.into()).into(); self } /// Set the title of the notification, default is None. /// /// If tilte is None, the notification will not have a title. pub fn title(mut self, title: impl Into) -> Self { self.title = Some(title.into()); self } /// Set the icon of the notification. /// /// If icon is None, the notification will use the default icon of the type. pub fn icon(mut self, icon: impl Into) -> Self { self.icon = Some(icon.into()); self } /// Set the type of the notification, default is NotificationType::Info. pub fn with_type(mut self, type_: NotificationType) -> Self { self.type_ = type_; self } /// Set the auto hide of the notification, default is true. pub fn autohide(mut self, autohide: bool) -> Self { self.autohide = autohide; self } /// Set the click callback of the notification. pub fn on_click( mut self, on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static, ) -> Self { self.on_click = Some(Arc::new(on_click)); self } fn dismiss(&mut self, _: &ClickEvent, cx: &mut ViewContext) { cx.emit(DismissEvent); } } impl EventEmitter for Notification {} impl FluentBuilder for Notification {} impl Render for Notification { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let icon = match self.icon.clone() { Some(icon) => icon, None => match self.type_ { NotificationType::Info => Icon::new(IconName::Info).text_color(crate::blue_500()), NotificationType::Success => { Icon::new(IconName::CircleCheck).text_color(crate::green_500()) } NotificationType::Warning => { Icon::new(IconName::TriangleAlert).text_color(crate::yellow_500()) } NotificationType::Error => { Icon::new(IconName::CircleX).text_color(crate::red_500()) } }, }; div() .id("notification") .group("") .occlude() .relative() .w_96() .border_1() .border_color(cx.theme().border) .bg(cx.theme().popover) .rounded_md() .shadow_md() .py_2() .px_4() .gap_3() .child(div().absolute().top_3().left_4().child(icon)) .child( v_flex() .pl_6() .gap_1() .when_some(self.title.clone(), |this, title| { this.child(div().text_sm().font_semibold().child(title)) }) .overflow_hidden() .child(div().text_sm().child(self.message.clone())), ) .when_some(self.on_click.clone(), |this, on_click| { this.cursor_pointer() .on_click(cx.listener(move |_, event, cx| { cx.emit(DismissEvent); on_click(event, cx); })) }) .when(!self.autohide, |this| { this.child( h_flex() .absolute() .top_1() .right_1() .invisible() .group_hover("", |this| this.visible()) .child( Button::new("close", cx) .icon(IconName::Close) .ghost() .xsmall() .on_click(cx.listener(Self::dismiss)), ), ) }) .with_animation( "slide-left", Animation::new(Duration::from_secs_f64(0.1)), move |this, delta| { let x_offset = px(120.) + delta * px(-120.); this.left(px(0.) + x_offset) }, ) } } /// A list of notifications. pub struct NotificationList { /// Notifications that will be auto hidden. pub(crate) notifications: VecDeque>, expanded: bool, } impl NotificationList { pub fn new(_cx: &mut ViewContext) -> Self { Self { notifications: VecDeque::new(), expanded: false, } } pub fn push(&mut self, notification: impl Into, cx: &mut ViewContext) { let notification = notification.into(); let id = notification.id.clone(); let autohide = notification.autohide; // Remove the notification by id, for keep unique. self.notifications.retain(|note| note.read(cx).id != id); let notification = cx.new_view(|_| notification); cx.subscribe(¬ification, move |view, _, _: &DismissEvent, cx| { view.notifications.retain(|note| id != note.read(cx).id); }) .detach(); self.notifications.push_back(notification); if autohide { // Sleep for 5 seconds to autohide the notification cx.spawn(|view, mut cx| async move { Timer::after(Duration::from_secs(5)).await; let _ = view.update(&mut cx, |view, cx| { if let Some(ix) = view .notifications .iter() .position(|note| note.read(cx).autohide) { view.notifications.remove(ix); } cx.notify() }); }) .detach(); } cx.notify(); } pub fn clear(&mut self, cx: &mut ViewContext) { self.notifications.clear(); cx.notify(); } pub fn notifications(&self) -> Vec> { self.notifications.iter().cloned().collect() } } impl Render for NotificationList { fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { let size = cx.viewport_size(); let items = self.notifications.iter().rev().take(10).rev().cloned(); div() .absolute() .flex() .top_4() .bottom_4() .right_4() .justify_end() .child( v_flex() .id("notification-list") .absolute() .relative() .right_0() .h(size.height - px(8.)) .on_hover(cx.listener(|view, hovered, cx| { view.expanded = *hovered; cx.notify() })) .gap_3() .children(items), ) } }