animation.rs

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