animation.rs

  1use std::time::{Duration, Instant};
  2
  3use crate::{AnyElement, App, Element, ElementId, GlobalElementId, IntoElement, Window};
  4
  5pub use easing::*;
  6
  7/// An animation that can be applied to an element.
  8pub struct Animation {
  9    /// The amount of time for which this animation should run
 10    pub duration: Duration,
 11    /// Whether to repeat this animation when it finishes
 12    pub oneshot: bool,
 13    /// A function that takes a delta between 0 and 1 and returns a new delta
 14    /// between 0 and 1 based on the given easing function.
 15    pub easing: Box<dyn Fn(f32) -> f32>,
 16}
 17
 18impl Animation {
 19    /// Create a new animation with the given duration.
 20    /// By default the animation will only run once and will use a linear easing function.
 21    pub fn new(duration: Duration) -> Self {
 22        Self {
 23            duration,
 24            oneshot: true,
 25            easing: Box::new(linear),
 26        }
 27    }
 28
 29    /// Set the animation to loop when it finishes.
 30    pub fn repeat(mut self) -> Self {
 31        self.oneshot = false;
 32        self
 33    }
 34
 35    /// Set the easing function to use for this animation.
 36    /// The easing function will take a time delta between 0 and 1 and return a new delta
 37    /// between 0 and 1
 38    pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
 39        self.easing = Box::new(easing);
 40        self
 41    }
 42}
 43
 44/// An extension trait for adding the animation wrapper to both Elements and Components
 45pub trait AnimationExt {
 46    /// Render this component or element with an animation
 47    fn with_animation(
 48        self,
 49        id: impl Into<ElementId>,
 50        animation: Animation,
 51        animator: impl Fn(Self, f32) -> Self + 'static,
 52    ) -> AnimationElement<Self>
 53    where
 54        Self: Sized,
 55    {
 56        AnimationElement {
 57            id: id.into(),
 58            element: Some(self),
 59            animator: Box::new(animator),
 60            animation,
 61        }
 62    }
 63}
 64
 65impl<E> AnimationExt for E {}
 66
 67/// A GPUI element that applies an animation to another element
 68pub struct AnimationElement<E> {
 69    id: ElementId,
 70    element: Option<E>,
 71    animation: Animation,
 72    animator: Box<dyn Fn(E, f32) -> E + 'static>,
 73}
 74
 75impl<E> AnimationElement<E> {
 76    /// Returns a new [`AnimationElement<E>`] after applying the given function
 77    /// to the element being animated.
 78    pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement<E> {
 79        self.element = self.element.map(f);
 80        self
 81    }
 82}
 83
 84impl<E: IntoElement + 'static> IntoElement for AnimationElement<E> {
 85    type Element = AnimationElement<E>;
 86
 87    fn into_element(self) -> Self::Element {
 88        self
 89    }
 90}
 91
 92struct AnimationState {
 93    start: Instant,
 94}
 95
 96impl<E: IntoElement + 'static> Element for AnimationElement<E> {
 97    type RequestLayoutState = AnyElement;
 98    type PrepaintState = ();
 99
100    fn id(&self) -> Option<ElementId> {
101        Some(self.id.clone())
102    }
103
104    fn request_layout(
105        &mut self,
106        global_id: Option<&GlobalElementId>,
107        window: &mut Window,
108        cx: &mut App,
109    ) -> (crate::LayoutId, Self::RequestLayoutState) {
110        window.with_element_state(global_id.unwrap(), |state, window| {
111            let state = state.unwrap_or_else(|| AnimationState {
112                start: Instant::now(),
113            });
114            let mut delta =
115                state.start.elapsed().as_secs_f32() / self.animation.duration.as_secs_f32();
116
117            let mut done = false;
118            if delta > 1.0 {
119                if self.animation.oneshot {
120                    done = true;
121                    delta = 1.0;
122                } else {
123                    delta %= 1.0;
124                }
125            }
126            let delta = (self.animation.easing)(delta);
127
128            debug_assert!(
129                (0.0..=1.0).contains(&delta),
130                "delta should always be between 0 and 1"
131            );
132
133            let element = self.element.take().expect("should only be called once");
134            let mut element = (self.animator)(element, delta).into_any_element();
135
136            if !done {
137                window.request_animation_frame();
138            }
139
140            ((element.request_layout(window, cx), element), state)
141        })
142    }
143
144    fn prepaint(
145        &mut self,
146        _id: Option<&GlobalElementId>,
147        _bounds: crate::Bounds<crate::Pixels>,
148        element: &mut Self::RequestLayoutState,
149        window: &mut Window,
150        cx: &mut App,
151    ) -> Self::PrepaintState {
152        element.prepaint(window, cx);
153    }
154
155    fn paint(
156        &mut self,
157        _id: Option<&GlobalElementId>,
158        _bounds: crate::Bounds<crate::Pixels>,
159        element: &mut Self::RequestLayoutState,
160        _: &mut Self::PrepaintState,
161        window: &mut Window,
162        cx: &mut App,
163    ) {
164        element.paint(window, cx);
165    }
166}
167
168mod easing {
169    use std::f32::consts::PI;
170
171    /// The linear easing function, or delta itself
172    pub fn linear(delta: f32) -> f32 {
173        delta
174    }
175
176    /// The quadratic easing function, delta * delta
177    pub fn quadratic(delta: f32) -> f32 {
178        delta * delta
179    }
180
181    /// The quadratic ease-in-out function, which starts and ends slowly but speeds up in the middle
182    pub fn ease_in_out(delta: f32) -> f32 {
183        if delta < 0.5 {
184            2.0 * delta * delta
185        } else {
186            let x = -2.0 * delta + 2.0;
187            1.0 - x * x / 2.0
188        }
189    }
190
191    /// The Quint ease-out function, which starts quickly and decelerates to a stop
192    pub fn ease_out_quint() -> impl Fn(f32) -> f32 {
193        move |delta| 1.0 - (1.0 - delta).powi(5)
194    }
195
196    /// Apply the given easing function, first in the forward direction and then in the reverse direction
197    pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 {
198        move |delta| {
199            if delta < 0.5 {
200                easing(delta * 2.0)
201            } else {
202                easing((1.0 - delta) * 2.0)
203            }
204        }
205    }
206
207    /// A custom easing function for pulsating alpha that slows down as it approaches 0.1
208    pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 {
209        let range = max - min;
210
211        move |delta| {
212            // Use a combination of sine and cubic functions for a more natural breathing rhythm
213            let t = (delta * 2.0 * PI).sin();
214            let breath = (t * t * t + t) / 2.0;
215
216            // Map the breath to our desired alpha range
217            let normalized_alpha = (breath + 1.0) / 2.0;
218
219            min + (normalized_alpha * range)
220        }
221    }
222}