Skip to main content

guise/anim/
mod.rs

1//! Animation toolkit: easing curves, springs, and mount/unmount presence.
2//!
3//! gpui animates by replaying a render-time interpolation over a duration
4//! (`with_animation`); this module supplies the curves to drive it and the
5//! [`Presence`] entity that latches an element through its exit animation
6//! before unmounting. [`Transition`](crate::Transition) and
7//! [`Collapse`](crate::Collapse) build on it.
8
9pub mod ease;
10
11mod presence;
12mod spring;
13
14pub use presence::{Presence, PresenceEvent};
15pub use spring::Spring;
16
17use std::time::Duration;
18
19use gpui::Animation;
20
21/// A named easing curve, storable on builders (`Copy`). `apply` maps
22/// normalized time; `animation` builds a ready gpui [`Animation`].
23#[derive(Debug, Clone, Copy, PartialEq)]
24pub enum Easing {
25    Linear,
26    EaseIn,
27    EaseOut,
28    EaseInOut,
29    EaseInCubic,
30    EaseOutCubic,
31    EaseInOutCubic,
32    EaseOutQuint,
33    EaseOutExpo,
34    EaseOutBack,
35    EaseOutElastic,
36    EaseOutBounce,
37    /// CSS `cubic-bezier(x1, y1, x2, y2)`.
38    CubicBezier(f32, f32, f32, f32),
39    /// Physical spring; its duration comes from the spring itself.
40    Spring(Spring),
41}
42
43impl Default for Easing {
44    fn default() -> Self {
45        Easing::EaseOut
46    }
47}
48
49impl Easing {
50    pub fn apply(self, t: f32) -> f32 {
51        match self {
52            Easing::Linear => ease::linear(t),
53            Easing::EaseIn => ease::ease_in(t),
54            Easing::EaseOut => ease::ease_out(t),
55            Easing::EaseInOut => ease::ease_in_out(t),
56            Easing::EaseInCubic => ease::ease_in_cubic(t),
57            Easing::EaseOutCubic => ease::ease_out_cubic(t),
58            Easing::EaseInOutCubic => ease::ease_in_out_cubic(t),
59            Easing::EaseOutQuint => ease::ease_out_quint(t),
60            Easing::EaseOutExpo => ease::ease_out_expo(t),
61            Easing::EaseOutBack => ease::ease_out_back(t),
62            Easing::EaseOutElastic => ease::ease_out_elastic(t),
63            Easing::EaseOutBounce => ease::ease_out_bounce(t),
64            Easing::CubicBezier(x1, y1, x2, y2) => ease::cubic_bezier(x1, y1, x2, y2, t),
65            Easing::Spring(spring) => spring.easing()(t),
66        }
67    }
68
69    /// A gpui [`Animation`] running this curve, **clamped** into `0..=1`.
70    /// `duration_ms` is ignored for springs — they settle on their own clock.
71    ///
72    /// gpui debug-asserts that an animation's easing output stays within
73    /// `0..=1`, which overshooting curves (`Spring`, `EaseOutBack`,
74    /// `EaseOutElastic`, wide cubic-beziers) violate by design — unclamped
75    /// they abort any debug build. The clamp flattens the overshoot peaks;
76    /// to keep them, run [`clock`](Self::clock) and apply the curve inside
77    /// the animator closure, where any value is legal:
78    ///
79    /// ```ignore
80    /// el.with_animation(id, easing.clock(200), move |el, t| {
81    ///     let delta = easing.apply(t); // may overshoot past 1.0
82    ///     el.ml(px((1.0 - delta) * 8.0))
83    /// })
84    /// ```
85    pub fn animation(self, duration_ms: u64) -> Animation {
86        self.clock(duration_ms)
87            .with_easing(move |t| self.apply(t).clamp(0.0, 1.0))
88    }
89
90    /// The un-eased gpui [`Animation`] for this curve: a linear clock sized
91    /// for it (springs use their settle time). Pair with
92    /// [`apply`](Self::apply) in the animator closure — see
93    /// [`animation`](Self::animation) for why overshooting curves must run
94    /// animator-side.
95    pub fn clock(self, duration_ms: u64) -> Animation {
96        let duration = match self {
97            Easing::Spring(spring) => Duration::from_secs_f32(spring.settle_seconds()),
98            _ => Duration::from_millis(duration_ms),
99        };
100        Animation::new(duration)
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn every_variant_hits_the_endpoints() {
110        let variants = [
111            Easing::Linear,
112            Easing::EaseIn,
113            Easing::EaseOut,
114            Easing::EaseInOut,
115            Easing::EaseInCubic,
116            Easing::EaseOutCubic,
117            Easing::EaseInOutCubic,
118            Easing::EaseOutQuint,
119            Easing::EaseOutExpo,
120            Easing::EaseOutBack,
121            Easing::EaseOutElastic,
122            Easing::EaseOutBounce,
123            Easing::CubicBezier(0.25, 0.1, 0.25, 1.0),
124            Easing::Spring(Spring::default()),
125        ];
126        for easing in variants {
127            assert!(easing.apply(0.0).abs() < 1e-3, "{easing:?} at 0");
128            assert!((easing.apply(1.0) - 1.0).abs() < 1e-3, "{easing:?} at 1");
129        }
130    }
131
132    /// Overshoot is a feature of these curves — and exactly why the entity
133    /// animators run them via `clock()` + `apply()` instead of gpui's easing
134    /// slot, which debug-asserts its output into `0..=1`.
135    #[test]
136    fn overshooting_curves_really_overshoot() {
137        let overshooters = [
138            Easing::EaseOutBack,
139            Easing::EaseOutElastic,
140            Easing::Spring(Spring::default()),
141        ];
142        for easing in overshooters {
143            let peak = (1..100)
144                .map(|i| easing.apply(i as f32 / 100.0))
145                .fold(f32::MIN, f32::max);
146            assert!(peak > 1.0, "{easing:?} never exceeded 1.0 (peak {peak})");
147        }
148    }
149}