Skip to main content

gpui_core/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,
9    ParentElement, SpringAnimation, SpringConfig, SpringPlayback, SpringState, SpringTarget,
10    Window,
11};
12
13pub use easing::*;
14use smallvec::SmallVec;
15
16/// A handle that can be used to cancel an in-flight animation.
17#[derive(Clone)]
18pub struct AnimationHandle {
19    cancelled: Rc<Cell<bool>>,
20}
21
22impl AnimationHandle {
23    fn new() -> Self {
24        Self {
25            cancelled: Rc::new(Cell::new(false)),
26        }
27    }
28
29    /// Cancel the animation, causing it to jump to its final state.
30    pub fn cancel(&self) {
31        self.cancelled.set(true);
32    }
33
34    /// Whether the animation has been cancelled.
35    pub fn is_cancelled(&self) -> bool {
36        self.cancelled.get()
37    }
38}
39
40/// An animation that can be applied to an element.
41#[derive(Clone)]
42pub struct Animation {
43    /// The amount of time for which this animation should run
44    pub duration: Duration,
45    /// Whether to repeat this animation when it finishes
46    pub oneshot: bool,
47    /// A function that maps normalized time to an animated value.
48    /// The result may exceed 0..1 for easing functions that overshoot.
49    pub easing: Rc<dyn Fn(f32) -> f32>,
50    /// The maximum number of times per second this animation re-renders.
51    /// When `None`, the animation re-renders on every frame.
52    pub max_fps: Option<f32>,
53}
54
55impl Animation {
56    /// Create a new animation with the given duration.
57    /// By default the animation will only run once and will use a linear easing function.
58    pub fn new(duration: Duration) -> Self {
59        Self {
60            duration,
61            oneshot: true,
62            easing: Rc::new(linear),
63            max_fps: None,
64        }
65    }
66
67    /// Set the animation to loop when it finishes.
68    pub fn repeat(mut self) -> Self {
69        self.oneshot = false;
70        self
71    }
72
73    /// Sets the easing function used to map normalized time to an animated value.
74    ///
75    /// The output is not clamped, allowing physical easing functions such as
76    /// springs to overshoot.
77    pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
78        self.easing = Rc::new(easing);
79        self
80    }
81
82    /// Limit how often this animation re-renders. Instead of re-rendering on
83    /// every frame, the animation schedules its next render `1 / max_fps`
84    /// seconds after the current one. Values that are not finite and positive
85    /// are ignored.
86    pub fn with_max_fps(mut self, max_fps: f32) -> Self {
87        self.max_fps = Some(max_fps);
88        self
89    }
90}
91
92/// An extension trait for adding the animation wrapper to both Elements and Components
93pub trait AnimationExt {
94    /// Render this component or element with an animation
95    fn with_animation(
96        self,
97        id: impl Into<ElementId>,
98        animation: Animation,
99        animator: impl Fn(Self, f32) -> Self + 'static,
100    ) -> AnimationElement<Self>
101    where
102        Self: Sized,
103    {
104        AnimationElement {
105            id: id.into(),
106            element: Some(self),
107            animator: Box::new(move |this, _, value| animator(this, value)),
108            animations: smallvec::smallvec![animation],
109            cancel_handle: None,
110        }
111    }
112
113    /// Render this component or element with a chain of animations
114    fn with_animations(
115        self,
116        id: impl Into<ElementId>,
117        animations: Vec<Animation>,
118        animator: impl Fn(Self, usize, f32) -> Self + 'static,
119    ) -> AnimationElement<Self>
120    where
121        Self: Sized,
122    {
123        AnimationElement {
124            id: id.into(),
125            element: Some(self),
126            animator: Box::new(animator),
127            animations: animations.into(),
128            cancel_handle: None,
129        }
130    }
131
132    /// Render this component or element with a cancellable animation.
133    /// Returns the animated element and a handle that can be used to cancel the animation.
134    fn with_cancellable_animation(
135        self,
136        id: impl Into<ElementId>,
137        animation: Animation,
138        animator: impl Fn(Self, f32) -> Self + 'static,
139    ) -> (AnimationElement<Self>, AnimationHandle)
140    where
141        Self: Sized,
142    {
143        let handle = AnimationHandle::new();
144        let element = AnimationElement {
145            id: id.into(),
146            element: Some(self),
147            animator: Box::new(move |this, _, value| animator(this, value)),
148            animations: smallvec::smallvec![animation],
149            cancel_handle: Some(handle.cancelled.clone()),
150        };
151        (element, handle)
152    }
153
154    /// Renders this component or element at the value produced by a spring.
155    ///
156    /// The element ID preserves position and velocity across target changes.
157    /// A newly mounted spring starts at its target unless configured with
158    /// [`SpringAnimation::from`].
159    fn with_spring<T>(
160        self,
161        id: impl Into<ElementId>,
162        animation: SpringAnimation<T>,
163        animator: impl FnOnce(Self, T::Output) -> Self + 'static,
164    ) -> SpringAnimationElement<Self>
165    where
166        Self: Sized,
167        T: SpringTarget,
168        T::Output: 'static,
169    {
170        let SpringAnimation {
171            config,
172            target,
173            epsilon,
174            initial,
175            playback,
176        } = animation;
177        let scalar_target = target.target();
178        SpringAnimationElement {
179            id: id.into(),
180            element: Some(self),
181            config,
182            target: scalar_target,
183            epsilon,
184            initial,
185            playback,
186            animator: Some(Box::new(move |this, value| {
187                animator(this, target.resolve(value))
188            })),
189        }
190    }
191}
192
193impl<E: IntoElement + 'static> AnimationExt for E {}
194
195/// A GPUI element that applies an animation to another element
196pub struct AnimationElement<E> {
197    id: ElementId,
198    element: Option<E>,
199    animations: SmallVec<[Animation; 1]>,
200    animator: Box<dyn Fn(E, usize, f32) -> E + 'static>,
201    cancel_handle: Option<Rc<Cell<bool>>>,
202}
203
204/// A GPUI element driven by a stateful spring.
205pub struct SpringAnimationElement<E> {
206    id: ElementId,
207    element: Option<E>,
208    config: SpringConfig,
209    target: f32,
210    epsilon: f32,
211    initial: Option<f32>,
212    playback: SpringPlayback,
213    animator: Option<Box<dyn FnOnce(E, f32) -> E + 'static>>,
214}
215
216impl<E: ParentElement> ParentElement for SpringAnimationElement<E> {
217    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
218        let Some(element) = &mut self.element else {
219            return;
220        };
221
222        element.extend(elements);
223    }
224}
225
226impl<E> SpringAnimationElement<E> {
227    /// Returns a new [`SpringAnimationElement<E>`] after applying the given function
228    /// to the element being animated.
229    pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> SpringAnimationElement<E> {
230        self.element = self.element.map(f);
231        self
232    }
233}
234
235impl<E: IntoElement + 'static> IntoElement for SpringAnimationElement<E> {
236    type Element = SpringAnimationElement<E>;
237
238    fn into_element(self) -> Self::Element {
239        self
240    }
241}
242
243impl<E: ParentElement> ParentElement for AnimationElement<E> {
244    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
245        let Some(element) = &mut self.element else {
246            return;
247        };
248
249        element.extend(elements);
250    }
251}
252
253impl<E> AnimationElement<E> {
254    /// Returns a new [`AnimationElement<E>`] after applying the given function
255    /// to the element being animated.
256    pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement<E> {
257        self.element = self.element.map(f);
258        self
259    }
260}
261
262impl<E: IntoElement + 'static> IntoElement for AnimationElement<E> {
263    type Element = AnimationElement<E>;
264
265    fn into_element(self) -> Self::Element {
266        self
267    }
268}
269
270struct AnimationState {
271    start: Instant,
272    animation_ix: usize,
273    /// Whether a throttled re-render (see [`Animation::with_max_fps`]) is
274    /// already scheduled, so overlapping renders don't stack extra timers.
275    delayed_frame_pending: Rc<Cell<bool>>,
276}
277
278struct SpringElementState {
279    spring: SpringState,
280    target: f32,
281    config: SpringConfig,
282    initial: f32,
283    playback: SpringPlayback,
284    updated_at: Instant,
285}
286
287impl<E: IntoElement + 'static> Element for SpringAnimationElement<E> {
288    type RequestLayoutState = AnyElement;
289    type PrepaintState = ();
290
291    fn id(&self) -> Option<ElementId> {
292        Some(self.id.clone())
293    }
294
295    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
296        None
297    }
298
299    fn request_layout(
300        &mut self,
301        global_id: Option<&GlobalElementId>,
302        _inspector_id: Option<&InspectorElementId>,
303        window: &mut Window,
304        cx: &mut App,
305    ) -> (crate::LayoutId, Self::RequestLayoutState) {
306        window.with_element_state(global_id.unwrap(), |state, window| {
307            let now = Instant::now();
308            let initial = self.initial.unwrap_or(self.target);
309            let mut state = state.unwrap_or_else(|| SpringElementState {
310                spring: SpringState {
311                    position: initial,
312                    velocity: 0.0,
313                },
314                target: self.target,
315                config: self.config,
316                initial,
317                playback: self.playback,
318                updated_at: now,
319            });
320
321            let elapsed = now.duration_since(state.updated_at).as_secs_f32();
322            match state.playback {
323                SpringPlayback::Running => {
324                    state.spring = state.config.step(state.spring, state.target, elapsed);
325                }
326                SpringPlayback::Paused
327                | SpringPlayback::Stopped
328                | SpringPlayback::Completed
329                | SpringPlayback::Cancelled => {}
330            }
331
332            state.config = self.config;
333            state.target = self.target;
334
335            let done = match self.playback {
336                SpringPlayback::Running => {
337                    let done = state
338                        .config
339                        .is_settled(state.spring, state.target, self.epsilon);
340                    if done {
341                        state.spring = SpringState {
342                            position: state.target,
343                            velocity: 0.0,
344                        };
345                    }
346                    done
347                }
348                SpringPlayback::Paused => true,
349                SpringPlayback::Stopped => {
350                    state.spring.velocity = 0.0;
351                    true
352                }
353                SpringPlayback::Completed => {
354                    state.spring = SpringState {
355                        position: state.target,
356                        velocity: 0.0,
357                    };
358                    true
359                }
360                SpringPlayback::Cancelled => {
361                    state.spring = SpringState {
362                        position: state.initial,
363                        velocity: 0.0,
364                    };
365                    true
366                }
367            };
368            state.playback = self.playback;
369            state.updated_at = now;
370
371            let element = self.element.take().expect("should only be called once");
372            let animator = self.animator.take().expect("should only be called once");
373            let mut element = animator(element, state.spring.position).into_any_element();
374
375            if !done {
376                window.request_animation_frame();
377            }
378
379            ((element.request_layout(window, cx), element), state)
380        })
381    }
382
383    fn prepaint(
384        &mut self,
385        _id: Option<&GlobalElementId>,
386        _inspector_id: Option<&InspectorElementId>,
387        _bounds: crate::Bounds<crate::Pixels>,
388        element: &mut Self::RequestLayoutState,
389        window: &mut Window,
390        cx: &mut App,
391    ) -> Self::PrepaintState {
392        element.prepaint(window, cx);
393    }
394
395    fn paint(
396        &mut self,
397        _id: Option<&GlobalElementId>,
398        _inspector_id: Option<&InspectorElementId>,
399        _bounds: crate::Bounds<crate::Pixels>,
400        element: &mut Self::RequestLayoutState,
401        _: &mut Self::PrepaintState,
402        window: &mut Window,
403        cx: &mut App,
404    ) {
405        element.paint(window, cx);
406    }
407}
408
409impl<E: IntoElement + 'static> Element for AnimationElement<E> {
410    type RequestLayoutState = AnyElement;
411    type PrepaintState = ();
412
413    fn id(&self) -> Option<ElementId> {
414        Some(self.id.clone())
415    }
416
417    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
418        None
419    }
420
421    fn request_layout(
422        &mut self,
423        global_id: Option<&GlobalElementId>,
424        _inspector_id: Option<&InspectorElementId>,
425        window: &mut Window,
426        cx: &mut App,
427    ) -> (crate::LayoutId, Self::RequestLayoutState) {
428        window.with_element_state(global_id.unwrap(), |state, window| {
429            let mut state = state.unwrap_or_else(|| AnimationState {
430                start: Instant::now(),
431                animation_ix: 0,
432                delayed_frame_pending: Rc::new(Cell::new(false)),
433            });
434
435            let cancelled = self.cancel_handle.as_ref().map_or(false, |h| h.get());
436
437            let animation_ix = state.animation_ix;
438
439            let (delta, done) = if cancelled {
440                (1.0_f32, true)
441            } else {
442                let mut delta = state.start.elapsed().as_secs_f32()
443                    / self.animations[animation_ix].duration.as_secs_f32();
444
445                let mut done = false;
446                if delta > 1.0 {
447                    if self.animations[animation_ix].oneshot {
448                        if animation_ix >= self.animations.len() - 1 {
449                            done = true;
450                        } else {
451                            state.start = Instant::now();
452                            state.animation_ix += 1;
453                        }
454                        delta = 1.0;
455                    } else {
456                        delta %= 1.0;
457                    }
458                }
459                let delta = (self.animations[animation_ix].easing)(delta);
460                (delta, done)
461            };
462
463            debug_assert!(delta.is_finite(), "animated value should be finite");
464
465            let element = self.element.take().expect("should only be called once");
466            let mut element = (self.animator)(element, animation_ix, delta).into_any_element();
467
468            if !done {
469                match self.animations[animation_ix].max_fps {
470                    Some(max_fps) if max_fps.is_finite() && max_fps > 0.0 => {
471                        if !state.delayed_frame_pending.get() {
472                            state.delayed_frame_pending.set(true);
473                            let delayed_frame_pending = state.delayed_frame_pending.clone();
474                            let view = window.current_view();
475                            let interval = Duration::from_secs_f32(1.0 / max_fps);
476                            window
477                                .spawn(cx, async move |cx| {
478                                    cx.background_executor().timer(interval).await;
479                                    delayed_frame_pending.set(false);
480                                    cx.update(move |_, cx| cx.notify(view)).ok();
481                                })
482                                .detach();
483                        }
484                    }
485                    _ => window.request_animation_frame(),
486                }
487            }
488
489            ((element.request_layout(window, cx), element), state)
490        })
491    }
492
493    fn prepaint(
494        &mut self,
495        _id: Option<&GlobalElementId>,
496        _inspector_id: Option<&InspectorElementId>,
497        _bounds: crate::Bounds<crate::Pixels>,
498        element: &mut Self::RequestLayoutState,
499        window: &mut Window,
500        cx: &mut App,
501    ) -> Self::PrepaintState {
502        element.prepaint(window, cx);
503    }
504
505    fn paint(
506        &mut self,
507        _id: Option<&GlobalElementId>,
508        _inspector_id: Option<&InspectorElementId>,
509        _bounds: crate::Bounds<crate::Pixels>,
510        element: &mut Self::RequestLayoutState,
511        _: &mut Self::PrepaintState,
512        window: &mut Window,
513        cx: &mut App,
514    ) {
515        element.paint(window, cx);
516    }
517}
518
519mod easing {
520    use std::f32::consts::PI;
521
522    /// The linear easing function, or delta itself
523    pub fn linear(delta: f32) -> f32 {
524        delta
525    }
526
527    /// The quadratic easing function, delta * delta
528    pub fn quadratic(delta: f32) -> f32 {
529        delta * delta
530    }
531
532    /// The quadratic ease-in-out function, which starts and ends slowly but speeds up in the middle
533    pub fn ease_in_out(delta: f32) -> f32 {
534        if delta < 0.5 {
535            2.0 * delta * delta
536        } else {
537            let x = -2.0 * delta + 2.0;
538            1.0 - x * x / 2.0
539        }
540    }
541
542    /// The Quint ease-out function, which starts quickly and decelerates to a stop
543    pub fn ease_out_quint() -> impl Fn(f32) -> f32 {
544        move |delta| 1.0 - (1.0 - delta).powi(5)
545    }
546
547    /// Apply the given easing function, first in the forward direction and then in the reverse direction
548    pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 {
549        move |delta| {
550            if delta < 0.5 {
551                easing(delta * 2.0)
552            } else {
553                easing((1.0 - delta) * 2.0)
554            }
555        }
556    }
557
558    /// A custom easing function for pulsating alpha that slows down as it approaches 0.1
559    pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 {
560        let range = max - min;
561
562        move |delta| {
563            // Use a combination of sine and cubic functions for a more natural breathing rhythm
564            let t = (delta * 2.0 * PI).sin();
565            let breath = (t * t * t + t) / 2.0;
566
567            // Map the breath to our desired alpha range
568            let normalized_alpha = (breath + 1.0) / 2.0;
569
570            min + (normalized_alpha * range)
571        }
572    }
573}
574
575#[cfg(test)]
576mod tests {
577    use crate::{InteractiveElement, Styled, div, px};
578
579    use super::*;
580
581    #[test]
582    fn test_animation_parent() {
583        div()
584            .id("id")
585            .with_animation(
586                "animation",
587                Animation::new(Duration::from_secs(1)),
588                |el, _t| el,
589            )
590            .child(div());
591    }
592
593    #[test]
594    fn test_spring_animation_parent() {
595        div()
596            .id("id")
597            .with_spring(
598                "spring-animation",
599                SpringAnimation::new(SpringConfig::new(100.0, 10.0, 1.0))
600                    .to(px(10.0))
601                    .from(px(0.0)),
602                |element, value| element.left(value),
603            )
604            .child(div());
605    }
606
607    #[test]
608    fn test_with_max_fps_stores_positive_and_invalid_rates() {
609        let capped = Animation::new(Duration::from_secs(1)).with_max_fps(12.0);
610        assert_eq!(capped.max_fps, Some(12.0));
611
612        let invalid = Animation::new(Duration::from_secs(1)).with_max_fps(0.0);
613        assert_eq!(invalid.max_fps, Some(0.0));
614    }
615}