From 0da882349f64ef4524a5fef63c362edb336f13ab Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Fri, 30 Aug 2024 11:24:55 +0800 Subject: [PATCH] Add exit animation to Notification. - Add `cubic_bezier` method and change modal to use this. Ref #148 https://github.com/user-attachments/assets/c09adc18-554c-4300-8109-98f3ab183910 --- crates/ui/src/animation.rs | 19 +++++++++++++++ crates/ui/src/lib.rs | 1 + crates/ui/src/modal.rs | 8 +++--- crates/ui/src/notification.rs | 46 +++++++++++++++++++++++++++-------- crates/ui/src/styled.rs | 7 ++++++ 5 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 crates/ui/src/animation.rs diff --git a/crates/ui/src/animation.rs b/crates/ui/src/animation.rs new file mode 100644 index 00000000..be3539ea --- /dev/null +++ b/crates/ui/src/animation.rs @@ -0,0 +1,19 @@ +/// A cubic bezier function like CSS `cubic-bezier`. +/// +/// Builder: +/// +/// https://cubic-bezier.com +pub fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> impl Fn(f32) -> f32 { + move |t: f32| { + let one_t = 1.0 - t; + let one_t2 = one_t * one_t; + let t2 = t * t; + let t3 = t2 * t; + + // The Bezier curve function for x and y, where x0 = 0, y0 = 0, x3 = 1, y3 = 1 + let _x = 3.0 * x1 * one_t2 * t + 3.0 * x2 * one_t * t2 + t3; + let y = 3.0 * y1 * one_t2 * t + 3.0 * y2 * one_t * t2 + t3; + + y + } +} diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 84648918..9b0422b8 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -7,6 +7,7 @@ mod styled; mod svg_img; mod time; +pub mod animation; pub mod button; pub mod checkbox; pub mod clipboard; diff --git a/crates/ui/src/modal.rs b/crates/ui/src/modal.rs index 53a00aec..acc9a0f4 100644 --- a/crates/ui/src/modal.rs +++ b/crates/ui/src/modal.rs @@ -7,7 +7,8 @@ use gpui::{ }; use crate::{ - button::Button, theme::ActiveTheme as _, v_flex, ContextModal, IconName, Sizable as _, + animation::cubic_bezier, button::Button, theme::ActiveTheme as _, v_flex, ContextModal, + IconName, Sizable as _, }; #[derive(IntoElement)] @@ -182,9 +183,10 @@ impl RenderOnce for Modal { .children(self.footer) .with_animation( "slide-down", - Animation::new(Duration::from_secs_f64(0.1)), + Animation::new(Duration::from_secs_f64(0.25)) + .with_easing(cubic_bezier(0.32, 0.72, 0., 1.)), move |this, delta| { - let y_offset = px(-30.) + delta * px(30.); + let y_offset = px(0.) + delta * px(30.); this.top(y + y_offset) }, ), diff --git a/crates/ui/src/notification.rs b/crates/ui/src/notification.rs index 4e1a1ce9..00b8a3b3 100644 --- a/crates/ui/src/notification.rs +++ b/crates/ui/src/notification.rs @@ -8,8 +8,8 @@ use gpui::{ use smol::Timer; use crate::{ - button::Button, h_flex, theme::ActiveTheme as _, v_flex, Icon, IconName, Sizable as _, - StyledExt, + animation::cubic_bezier, button::Button, h_flex, theme::ActiveTheme as _, v_flex, Icon, + IconName, Sizable as _, StyledExt, }; pub enum NotificationType { @@ -49,6 +49,7 @@ pub struct Notification { icon: Option, autohide: bool, on_click: Option>, + closing: bool, } impl From for Notification { @@ -92,6 +93,7 @@ impl Notification { icon: None, autohide: true, on_click: None, + closing: false, } } @@ -166,13 +168,29 @@ impl Notification { } fn dismiss(&mut self, _: &ClickEvent, cx: &mut ViewContext) { - cx.emit(DismissEvent); + self.closing = true; + cx.notify(); + + // Dismiss the notification after 0.15s to show the animation. + cx.spawn(|view, mut cx| async move { + Timer::after(Duration::from_secs_f32(0.15)).await; + cx.update(|cx| { + if let Some(view) = view.upgrade() { + view.update(cx, |view, cx| { + view.closing = false; + cx.emit(DismissEvent); + }); + } + }) + }) + .detach() } } impl EventEmitter for Notification {} impl FluentBuilder for Notification {} impl Render for Notification { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let closing = self.closing; let icon = match self.icon.clone() { Some(icon) => icon, None => match self.type_ { @@ -216,8 +234,8 @@ impl Render for Notification { ) .when_some(self.on_click.clone(), |this, on_click| { this.cursor_pointer() - .on_click(cx.listener(move |_, event, cx| { - cx.emit(DismissEvent); + .on_click(cx.listener(move |view, event, cx| { + view.dismiss(event, cx); on_click(event, cx); })) }) @@ -239,11 +257,17 @@ impl Render for Notification { ) }) .with_animation( - "slide-left", - Animation::new(Duration::from_secs_f64(0.1)), + ElementId::NamedInteger("slide-down".into(), closing as usize), + Animation::new(Duration::from_secs_f64(0.15)) + .with_easing(cubic_bezier(0.4, 0., 0.2, 1.)), move |this, delta| { - let x_offset = px(120.) + delta * px(-120.); - this.left(px(0.) + x_offset) + if closing { + let x_offset = px(0.) + delta * px(45.); + this.left(px(0.) + x_offset).opacity(1. - delta) + } else { + let y_offset = px(-45.) + delta * px(45.); + this.top(px(0.) + y_offset) + } }, ) } @@ -289,7 +313,9 @@ impl NotificationList { .iter() .position(|note| note.read(cx).autohide) { - view.notifications.remove(ix); + if let Some(note) = view.notifications.get(ix) { + note.update(cx, |note, cx| note.dismiss(&ClickEvent::default(), cx)); + } } cx.notify() }); diff --git a/crates/ui/src/styled.rs b/crates/ui/src/styled.rs index 21ef7e12..71b0f1f6 100644 --- a/crates/ui/src/styled.rs +++ b/crates/ui/src/styled.rs @@ -124,6 +124,7 @@ pub trait StyledExt: Styled + Sized { /// Set the opacity of the element. fn opacity(mut self, opacity: f32) -> Self { + let text_color = self.style().text.clone().and_then(|t| t.color); let bg_color = self.style().background.clone(); let border_color = self.style().border_color; let box_shadow = self.style().box_shadow.clone(); @@ -151,6 +152,12 @@ pub trait StyledExt: Styled + Sized { this }; + let this = if let Some(text_color) = text_color { + this.text_color(text_color.opacity(opacity)) + } else { + this + }; + this }