animation.rs

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