ZIm/crates/ui/src/styles/animation.rs
Daniel Sauble 5fa212183a
Fix animations in the component preview (#33673)
Fixes #33869

The Animation page in the Component Preview had a few issues.

* The animations only ran once, so you couldn't watch animations below
the fold.
* The offset math was wrong, so some animated elements were rendered
outside of their parent container.
* The "animate in from right" elements were defined with an initial
`.left()` offset, which overrode the animation behavior.

I made fixes to address these issues. In particular, every time you
click the active list item, it renders the preview again (which causes
the animations to run again).

Before:


https://github.com/user-attachments/assets/a1fa2e3f-653c-4b83-a6ed-c55ca9c78ad4

After:


https://github.com/user-attachments/assets/3623bbbc-9047-4443-b7f3-96bd92f582bf

Release Notes:

- N/A
2025-07-29 14:22:53 -07:00

285 lines
12 KiB
Rust

use crate::{ContentGroup, prelude::*};
use gpui::{AnimationElement, AnimationExt, Styled};
use std::time::Duration;
use gpui::ease_out_quint;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AnimationDuration {
Instant = 50,
Fast = 150,
Slow = 300,
}
impl AnimationDuration {
pub fn duration(&self) -> Duration {
Duration::from_millis(*self as u64)
}
}
impl Into<std::time::Duration> for AnimationDuration {
fn into(self) -> Duration {
self.duration()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AnimationDirection {
FromBottom,
FromLeft,
FromRight,
FromTop,
}
pub trait DefaultAnimations: Styled + Sized {
fn animate_in(
self,
animation_type: AnimationDirection,
fade_in: bool,
) -> AnimationElement<Self> {
let animation_name = match animation_type {
AnimationDirection::FromBottom => "animate_from_bottom",
AnimationDirection::FromLeft => "animate_from_left",
AnimationDirection::FromRight => "animate_from_right",
AnimationDirection::FromTop => "animate_from_top",
};
self.with_animation(
animation_name,
gpui::Animation::new(AnimationDuration::Fast.into()).with_easing(ease_out_quint()),
move |mut this, delta| {
let start_opacity = 0.4;
let start_pos = 0.0;
let end_pos = 40.0;
if fade_in {
this = this.opacity(start_opacity + delta * (1.0 - start_opacity));
}
match animation_type {
AnimationDirection::FromBottom => {
this.bottom(px(start_pos + delta * (end_pos - start_pos)))
}
AnimationDirection::FromLeft => {
this.left(px(start_pos + delta * (end_pos - start_pos)))
}
AnimationDirection::FromRight => {
this.right(px(start_pos + delta * (end_pos - start_pos)))
}
AnimationDirection::FromTop => {
this.top(px(start_pos + delta * (end_pos - start_pos)))
}
}
},
)
}
fn animate_in_from_bottom(self, fade: bool) -> AnimationElement<Self> {
self.animate_in(AnimationDirection::FromBottom, fade)
}
fn animate_in_from_left(self, fade: bool) -> AnimationElement<Self> {
self.animate_in(AnimationDirection::FromLeft, fade)
}
fn animate_in_from_right(self, fade: bool) -> AnimationElement<Self> {
self.animate_in(AnimationDirection::FromRight, fade)
}
fn animate_in_from_top(self, fade: bool) -> AnimationElement<Self> {
self.animate_in(AnimationDirection::FromTop, fade)
}
}
impl<E: Styled> DefaultAnimations for E {}
// Don't use this directly, it only exists to show animation previews
#[derive(RegisterComponent)]
struct Animation {}
impl Component for Animation {
fn scope() -> ComponentScope {
ComponentScope::None
}
fn description() -> Option<&'static str> {
Some("Demonstrates various animation patterns and transitions available in the UI system.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
let container_size = 128.0;
let element_size = 32.0;
let offset = container_size / 2.0 - element_size / 2.0;
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Animate In",
vec![
single_example(
"From Bottom",
ContentGroup::new()
.relative()
.items_center()
.justify_center()
.size(px(container_size))
.child(
div()
.id("animate-in-from-bottom")
.absolute()
.size(px(element_size))
.left(px(offset))
.rounded_md()
.bg(gpui::red())
.animate_in(AnimationDirection::FromBottom, false),
)
.into_any_element(),
),
single_example(
"From Top",
ContentGroup::new()
.relative()
.items_center()
.justify_center()
.size(px(container_size))
.child(
div()
.id("animate-in-from-top")
.absolute()
.size(px(element_size))
.left(px(offset))
.rounded_md()
.bg(gpui::blue())
.animate_in(AnimationDirection::FromTop, false),
)
.into_any_element(),
),
single_example(
"From Left",
ContentGroup::new()
.relative()
.items_center()
.justify_center()
.size(px(container_size))
.child(
div()
.id("animate-in-from-left")
.absolute()
.size(px(element_size))
.top(px(offset))
.rounded_md()
.bg(gpui::green())
.animate_in(AnimationDirection::FromLeft, false),
)
.into_any_element(),
),
single_example(
"From Right",
ContentGroup::new()
.relative()
.items_center()
.justify_center()
.size(px(container_size))
.child(
div()
.id("animate-in-from-right")
.absolute()
.size(px(element_size))
.top(px(offset))
.rounded_md()
.bg(gpui::yellow())
.animate_in(AnimationDirection::FromRight, false),
)
.into_any_element(),
),
],
)
.grow(),
example_group_with_title(
"Fade and Animate In",
vec![
single_example(
"From Bottom",
ContentGroup::new()
.relative()
.items_center()
.justify_center()
.size(px(container_size))
.child(
div()
.id("fade-animate-in-from-bottom")
.absolute()
.size(px(element_size))
.left(px(offset))
.rounded_md()
.bg(gpui::red())
.animate_in(AnimationDirection::FromBottom, true),
)
.into_any_element(),
),
single_example(
"From Top",
ContentGroup::new()
.relative()
.items_center()
.justify_center()
.size(px(container_size))
.child(
div()
.id("fade-animate-in-from-top")
.absolute()
.size(px(element_size))
.left(px(offset))
.rounded_md()
.bg(gpui::blue())
.animate_in(AnimationDirection::FromTop, true),
)
.into_any_element(),
),
single_example(
"From Left",
ContentGroup::new()
.relative()
.items_center()
.justify_center()
.size(px(container_size))
.child(
div()
.id("fade-animate-in-from-left")
.absolute()
.size(px(element_size))
.top(px(offset))
.rounded_md()
.bg(gpui::green())
.animate_in(AnimationDirection::FromLeft, true),
)
.into_any_element(),
),
single_example(
"From Right",
ContentGroup::new()
.relative()
.items_center()
.justify_center()
.size(px(container_size))
.child(
div()
.id("fade-animate-in-from-right")
.absolute()
.size(px(element_size))
.top(px(offset))
.rounded_md()
.bg(gpui::yellow())
.animate_in(AnimationDirection::FromRight, true),
)
.into_any_element(),
),
],
)
.grow(),
])
.into_any_element(),
)
}
}