use gpui::{AnimationElement, AnimationExt, App, ElementId, IntoElement, Styled, px};
use gpui_kit_theme::{ActiveTheme, SpringPreset, Theme};
use super::{MotionSpec, Spring, Stagger, dialog_arrival, entrance, menu, spec::state_change};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Entrance {
Fade,
#[default]
Rise,
Menu,
Dialog,
}
impl Entrance {
fn spec(self, theme: &Theme) -> MotionSpec {
match self {
Self::Fade => entrance(theme),
Self::Rise => entrance(theme),
Self::Menu => menu(theme),
Self::Dialog => dialog_arrival(theme),
}
}
fn travel(self) -> f32 {
match self {
Self::Fade => 0.0,
Self::Rise => 6.0,
Self::Menu => -2.0,
Self::Dialog => 8.0,
}
}
fn opening_opacity(self) -> f32 {
match self {
Self::Menu => 0.3,
_ => 0.0,
}
}
}
pub trait Animated: Styled + IntoElement + Sized + 'static {
fn animate_in(
self,
id: impl Into<ElementId>,
cx: &App,
entrance: Entrance,
) -> AnimationElement<Self> {
self.animate_with(id, entrance, entrance.spec(cx.theme()))
}
fn animate_in_staggered(
self,
id: impl Into<ElementId>,
cx: &App,
entrance: Entrance,
index: usize,
count: usize,
) -> AnimationElement<Self> {
let spec = Stagger::rows().spec(index, count, entrance.spec(cx.theme()));
self.animate_with(id, entrance, spec)
}
fn animate_with(
self,
id: impl Into<ElementId>,
entrance: Entrance,
spec: MotionSpec,
) -> AnimationElement<Self> {
let travel = entrance.travel();
let from = entrance.opening_opacity();
self.with_animation(id, spec.animation(), move |element, progress| {
let element = element.opacity(from + (1.0 - from) * progress);
if travel == 0.0 {
element
} else {
element.relative().top(px(travel * (1.0 - progress)))
}
})
}
fn animate_change(self, id: impl Into<ElementId>, cx: &App) -> AnimationElement<Self> {
self.animate_with(id, Entrance::Fade, state_change(cx.theme()))
}
fn animate_sprung(
self,
id: impl Into<ElementId>,
cx: &App,
entrance: Entrance,
preset: SpringPreset,
) -> AnimationElement<Self> {
let spec = MotionSpec::sprung(Spring::preset(cx.theme(), preset));
self.animate_with(id, entrance, spec)
}
}
impl<T: Styled + IntoElement + Sized + 'static> Animated for T {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_faded_arrival_never_moves_the_element() {
assert_eq!(Entrance::Fade.travel(), 0.0);
}
#[test]
fn a_menu_drops_from_its_anchor_and_the_rest_rise_to_theirs() {
assert!(Entrance::Menu.travel() < 0.0);
assert!(Entrance::Rise.travel() > 0.0);
assert!(Entrance::Dialog.travel() > 0.0);
}
#[test]
fn a_dialog_arrives_on_a_spring_and_a_menu_on_a_curve() {
let theme = Theme::studio_dark();
assert!(Entrance::Dialog.spec(&theme).is_sprung());
assert!(!Entrance::Menu.spec(&theme).is_sprung());
}
#[test]
fn every_arrival_ends_fully_opaque() {
let theme = Theme::studio_dark();
for entrance in [
Entrance::Fade,
Entrance::Rise,
Entrance::Menu,
Entrance::Dialog,
] {
let spec = entrance.spec(&theme);
let from = entrance.opening_opacity();
let settled = from + (1.0 - from) * spec.progress(1.0);
assert!(
(settled - 1.0).abs() < f32::EPSILON,
"{entrance:?} settled at {settled}"
);
}
}
#[test]
fn only_a_menu_starts_partly_visible() {
assert!(Entrance::Menu.opening_opacity() > 0.0);
for entrance in [Entrance::Fade, Entrance::Rise, Entrance::Dialog] {
assert_eq!(entrance.opening_opacity(), 0.0);
}
}
#[test]
fn a_staggered_group_finishes_within_the_row_cap() {
let theme = Theme::studio_dark();
let spec = Entrance::Fade.spec(&theme);
let stagger = Stagger::rows();
for count in [2, 8, 50, 500] {
let last = stagger.spec(count - 1, count, spec);
assert!(
last.delay_ms <= super::super::ROW_STAGGER_CAP.as_millis() as u64,
"{count} rows waited {}ms",
last.delay_ms
);
}
}
}