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 time;
|
||||
|
||||
pub mod animation;
|
||||
pub mod button;
|
||||
pub mod checkbox;
|
||||
pub mod clipboard;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<Icon>,
|
||||
autohide: bool,
|
||||
on_click: Option<Arc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
|
||||
closing: bool,
|
||||
}
|
||||
|
||||
impl From<SharedString> 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<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 FluentBuilder for Notification {}
|
||||
impl Render for Notification {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> 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()
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue