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
This commit is contained in:
parent
14af8998bf
commit
0da882349f
5 changed files with 68 additions and 13 deletions
19
crates/ui/src/animation.rs
Normal file
19
crates/ui/src/animation.rs
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ mod styled;
|
||||||
mod svg_img;
|
mod svg_img;
|
||||||
mod time;
|
mod time;
|
||||||
|
|
||||||
|
pub mod animation;
|
||||||
pub mod button;
|
pub mod button;
|
||||||
pub mod checkbox;
|
pub mod checkbox;
|
||||||
pub mod clipboard;
|
pub mod clipboard;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ use gpui::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
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)]
|
#[derive(IntoElement)]
|
||||||
|
|
@ -182,9 +183,10 @@ impl RenderOnce for Modal {
|
||||||
.children(self.footer)
|
.children(self.footer)
|
||||||
.with_animation(
|
.with_animation(
|
||||||
"slide-down",
|
"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| {
|
move |this, delta| {
|
||||||
let y_offset = px(-30.) + delta * px(30.);
|
let y_offset = px(0.) + delta * px(30.);
|
||||||
this.top(y + y_offset)
|
this.top(y + y_offset)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ use gpui::{
|
||||||
use smol::Timer;
|
use smol::Timer;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
button::Button, h_flex, theme::ActiveTheme as _, v_flex, Icon, IconName, Sizable as _,
|
animation::cubic_bezier, button::Button, h_flex, theme::ActiveTheme as _, v_flex, Icon,
|
||||||
StyledExt,
|
IconName, Sizable as _, StyledExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub enum NotificationType {
|
pub enum NotificationType {
|
||||||
|
|
@ -49,6 +49,7 @@ pub struct Notification {
|
||||||
icon: Option<Icon>,
|
icon: Option<Icon>,
|
||||||
autohide: bool,
|
autohide: bool,
|
||||||
on_click: Option<Arc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
|
on_click: Option<Arc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
|
||||||
|
closing: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<SharedString> for Notification {
|
impl From<SharedString> for Notification {
|
||||||
|
|
@ -92,6 +93,7 @@ impl Notification {
|
||||||
icon: None,
|
icon: None,
|
||||||
autohide: true,
|
autohide: true,
|
||||||
on_click: None,
|
on_click: None,
|
||||||
|
closing: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -166,13 +168,29 @@ impl Notification {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dismiss(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
|
fn dismiss(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
|
||||||
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<DismissEvent> for Notification {}
|
impl EventEmitter<DismissEvent> for Notification {}
|
||||||
impl FluentBuilder for Notification {}
|
impl FluentBuilder for Notification {}
|
||||||
impl Render for Notification {
|
impl Render for Notification {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
let closing = self.closing;
|
||||||
let icon = match self.icon.clone() {
|
let icon = match self.icon.clone() {
|
||||||
Some(icon) => icon,
|
Some(icon) => icon,
|
||||||
None => match self.type_ {
|
None => match self.type_ {
|
||||||
|
|
@ -216,8 +234,8 @@ impl Render for Notification {
|
||||||
)
|
)
|
||||||
.when_some(self.on_click.clone(), |this, on_click| {
|
.when_some(self.on_click.clone(), |this, on_click| {
|
||||||
this.cursor_pointer()
|
this.cursor_pointer()
|
||||||
.on_click(cx.listener(move |_, event, cx| {
|
.on_click(cx.listener(move |view, event, cx| {
|
||||||
cx.emit(DismissEvent);
|
view.dismiss(event, cx);
|
||||||
on_click(event, cx);
|
on_click(event, cx);
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
@ -239,11 +257,17 @@ impl Render for Notification {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.with_animation(
|
.with_animation(
|
||||||
"slide-left",
|
ElementId::NamedInteger("slide-down".into(), closing as usize),
|
||||||
Animation::new(Duration::from_secs_f64(0.1)),
|
Animation::new(Duration::from_secs_f64(0.15))
|
||||||
|
.with_easing(cubic_bezier(0.4, 0., 0.2, 1.)),
|
||||||
move |this, delta| {
|
move |this, delta| {
|
||||||
let x_offset = px(120.) + delta * px(-120.);
|
if closing {
|
||||||
this.left(px(0.) + x_offset)
|
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()
|
.iter()
|
||||||
.position(|note| note.read(cx).autohide)
|
.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()
|
cx.notify()
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,7 @@ pub trait StyledExt: Styled + Sized {
|
||||||
|
|
||||||
/// Set the opacity of the element.
|
/// Set the opacity of the element.
|
||||||
fn opacity(mut self, opacity: f32) -> Self {
|
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 bg_color = self.style().background.clone();
|
||||||
let border_color = self.style().border_color;
|
let border_color = self.style().border_color;
|
||||||
let box_shadow = self.style().box_shadow.clone();
|
let box_shadow = self.style().box_shadow.clone();
|
||||||
|
|
@ -151,6 +152,12 @@ pub trait StyledExt: Styled + Sized {
|
||||||
this
|
this
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let this = if let Some(text_color) = text_color {
|
||||||
|
this.text_color(text_color.opacity(opacity))
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
};
|
||||||
|
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue