Skip to main content

gpui/elements/
animation.rs

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