diff --git a/crates/story/src/notification_story.rs b/crates/story/src/notification_story.rs index 11ac92e5..5fd38ecb 100644 --- a/crates/story/src/notification_story.rs +++ b/crates/story/src/notification_story.rs @@ -6,7 +6,7 @@ use gpui::{ use gpui_component::{ button::{Button, ButtonVariants as _}, notification::{Notification, NotificationType}, - ContextModal as _, IconName, + ContextModal as _, }; use crate::section; @@ -54,14 +54,28 @@ impl Render for NotificationStory { .track_focus(&self.focus_handle) .size_full() .child( - section("Show Notification") + section("Simple Notification").child( + Button::new("show-notify-0") + .label("Show Notification") + .on_click(cx.listener(|_, _, window, cx| { + window.push_notification("This is a notification.", cx) + })), + ), + ) + .child( + section("Notification with Type") .child( Button::new("show-notify-info") .info() .label("Info") .on_click(cx.listener(|_, _, window, cx| { - window - .push_notification("You have been saved file successfully.", cx) + window.push_notification( + ( + NotificationType::Info, + "You have been saved file successfully.", + ), + cx, + ) })), ) .child( @@ -106,31 +120,36 @@ impl Render for NotificationStory { cx, ) })), - ) - .child( - Button::new("show-notify-with-title") - .label("Notification with Title") - .on_click(cx.listener(|_, _, window, cx| { - struct TestNotification; - - window.push_notification( - Notification::new( - "你已经成功保存了文件,但是有一些警告信息需要你注意。", - ) - .id::() - .title("保存成功") - .icon(IconName::Inbox) - .autohide(false) - .on_click(cx.listener( - |_, _, _, cx| { - println!("Notification clicked"); - cx.notify(); - }, - )), - cx, - ) - })), ), ) + .child( + section("With title and action").child( + Button::new("show-notify-with-title") + .label("Notification with Title") + .on_click(cx.listener(|_, _, window, cx| { + struct TestNotification; + + window.push_notification( + Notification::new("There was a problem with your request.") + .id::() + .title("Uh oh! Something went wrong.") + .autohide(false) + .action(|_, cx| { + Button::new("try-again").label("Try again").on_click( + cx.listener(|this, _, window, cx| { + println!("You have clicked the try again action."); + this.dismiss(window, cx); + }), + ) + }) + .on_click(cx.listener(|_, _, _, cx| { + println!("Notification clicked"); + cx.notify(); + })), + cx, + ) + })), + ), + ) } } diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index a51e389f..43f79731 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -212,9 +212,10 @@ impl Default for Icon { impl Clone for Icon { fn clone(&self) -> Self { let mut this = Self::default().path(self.path.clone()); - if let Some(size) = self.size { - this = this.with_size(size); - } + this.style = self.style.clone(); + this.rotation = self.rotation; + this.size = self.size; + this.text_color = self.text_color; this } } diff --git a/crates/ui/src/notification.rs b/crates/ui/src/notification.rs index 3d065099..1a4265c1 100644 --- a/crates/ui/src/notification.rs +++ b/crates/ui/src/notification.rs @@ -1,7 +1,7 @@ use std::{ any::TypeId, collections::{HashMap, VecDeque}, - sync::Arc, + rc::Rc, time::Duration, }; @@ -19,13 +19,26 @@ use crate::{ h_flex, v_flex, ActiveTheme as _, Icon, IconName, Sizable as _, StyledExt, }; +#[derive(Debug, Clone, Copy, Default)] pub enum NotificationType { + #[default] Info, Success, Warning, Error, } +impl NotificationType { + fn icon(&self, cx: &App) -> Icon { + match self { + Self::Info => Icon::new(IconName::Info).text_color(cx.theme().info), + Self::Success => Icon::new(IconName::CircleCheck).text_color(cx.theme().success), + Self::Warning => Icon::new(IconName::TriangleAlert).text_color(cx.theme().warning), + Self::Error => Icon::new(IconName::CircleX).text_color(cx.theme().danger), + } + } +} + #[derive(Debug, PartialEq, Clone, Hash, Eq)] pub(crate) enum NotificationId { Id(TypeId), @@ -51,12 +64,13 @@ pub struct Notification { /// /// None means the notification will be added to the end of the list. id: NotificationId, - type_: NotificationType, + type_: Option, title: Option, message: SharedString, icon: Option, autohide: bool, - on_click: Option>, + action_builder: Option) -> Button>>, + on_click: Option>, closing: bool, } @@ -91,6 +105,7 @@ impl From<(NotificationType, SharedString)> for Notification { } struct DefaultIdType; + impl Notification { /// Create a new notification with the given content. /// @@ -103,9 +118,10 @@ impl Notification { id: id.into(), title: None, message: message.into(), - type_: NotificationType::Info, + type_: None, icon: None, autohide: true, + action_builder: None, on_click: None, closing: false, } @@ -162,7 +178,7 @@ impl Notification { /// Set the type of the notification, default is NotificationType::Info. pub fn with_type(mut self, type_: NotificationType) -> Self { - self.type_ = type_; + self.type_ = Some(type_); self } @@ -177,11 +193,21 @@ impl Notification { mut self, on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, ) -> Self { - self.on_click = Some(Arc::new(on_click)); + self.on_click = Some(Rc::new(on_click)); self } - fn dismiss(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context) { + /// Set the action button of the notification. + pub fn action(mut self, action: F) -> Self + where + F: Fn(&mut Window, &mut Context) -> Button + 'static, + { + self.action_builder = Some(Rc::new(action)); + self + } + + /// Dismiss the notification. + pub fn dismiss(&mut self, _: &mut Window, cx: &mut Context) { self.closing = true; cx.notify(); @@ -203,25 +229,15 @@ impl Notification { impl EventEmitter for Notification {} impl FluentBuilder for Notification {} impl Render for Notification { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let closing = self.closing; - 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()) - } - }, + let icon = match self.type_ { + None => self.icon.clone(), + Some(type_) => Some(type_.icon(cx)), }; + let has_icon = icon.is_some(); - div() + h_flex() .id("notification") .group("") .occlude() @@ -235,20 +251,25 @@ impl Render for Notification { .py_2() .px_4() .gap_3() - .child(div().absolute().top_3().left_4().child(icon)) + .when_some(icon, |this, icon| { + this.child(div().absolute().top_3().left_4().child(icon)) + }) .child( v_flex() - .pl_6() - .gap_1() + .flex_1() + .when(has_icon, |this| this.pl_6()) .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.action_builder.clone(), |this, action_builder| { + this.child(action_builder(window, cx).small().outline().mr_1()) + }) .when_some(self.on_click.clone(), |this, on_click| { this.on_click(cx.listener(move |view, event, window, cx| { - view.dismiss(event, window, cx); + view.dismiss(window, cx); on_click(event, window, cx); })) }) @@ -265,7 +286,9 @@ impl Render for Notification { .icon(IconName::Close) .ghost() .xsmall() - .on_click(cx.listener(Self::dismiss)), + .on_click( + cx.listener(|this, _, window, cx| this.dismiss(window, cx)), + ), ), ) }) @@ -332,9 +355,9 @@ impl NotificationList { cx.spawn_in(window, async move |_, cx| { Timer::after(Duration::from_secs(5)).await; - if let Err(err) = notification.update_in(cx, |note, window, cx| { - note.dismiss(&ClickEvent::default(), window, cx) - }) { + if let Err(err) = + notification.update_in(cx, |note, window, cx| note.dismiss(window, cx)) + { println!("failed to auto hide notification: {:?}", err); } })