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/// How an [`Animation`] behaves once one pass finishes.
41#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
42pub enum AnimationRepeat {
43    /// Play once and hold the final value (the default).
44    #[default]
45    Once,
46    /// Restart forever, wrapping progress back to the start.
47    Forever,
48    /// Play a fixed number of passes in total, then hold the final value.
49    /// Zero is clamped to one pass.
50    Times(u32),
51}
52
53/// An animation that can be applied to an element.
54#[derive(Clone)]
55pub struct Animation {
56    /// The amount of time for which this animation should run. A zero
57    /// duration completes immediately after the delay, including for
58    /// [`AnimationRepeat::Forever`].
59    pub duration: Duration,
60    /// The amount of time to wait before the animation starts. The delay
61    /// applies once, when the animation begins; it is not repeated between
62    /// passes of a repeating animation.
63    pub delay: Duration,
64    /// Whether and how this animation repeats when a pass finishes
65    pub repeat: AnimationRepeat,
66    /// A function that maps normalized time to an animated value.
67    /// The result may exceed 0..1 for easing functions that overshoot.
68    pub easing: Rc<dyn Fn(f32) -> f32>,
69    /// The maximum number of times per second this animation re-renders.
70    /// When `None`, the animation re-renders on every frame.
71    pub max_fps: Option<f32>,
72}
73
74impl Animation {
75    /// Create a new animation with the given duration.
76    /// By default the animation will only run once and will use a linear easing function.
77    pub fn new(duration: Duration) -> Self {
78        Self {
79            duration,
80            delay: Duration::ZERO,
81            repeat: AnimationRepeat::Once,
82            easing: Rc::new(linear),
83            max_fps: None,
84        }
85    }
86
87    /// Set the animation to loop forever when it finishes.
88    pub fn repeat(mut self) -> Self {
89        self.repeat = AnimationRepeat::Forever;
90        self
91    }
92
93    /// Set the animation to run the given number of passes in total, then
94    /// hold its final value. Zero is treated as one pass.
95    pub fn repeat_n(mut self, times: u32) -> Self {
96        self.repeat = AnimationRepeat::Times(times);
97        self
98    }
99
100    /// Set how the animation repeats when a pass finishes.
101    pub fn with_repeat(mut self, repeat: AnimationRepeat) -> Self {
102        self.repeat = repeat;
103        self
104    }
105
106    /// Wait for the given duration before the animation starts. While the
107    /// delay elapses the animation holds its initial value (`easing(0)`) and
108    /// no animation frames are requested. In an animation chain (see
109    /// [`AnimationExt::with_animations`]) the delay of each animation counts
110    /// from the moment that animation begins.
111    pub fn with_delay(mut self, delay: Duration) -> Self {
112        self.delay = delay;
113        self
114    }
115
116    /// Sets the easing function used to map normalized time to an animated value.
117    ///
118    /// The output is not clamped, allowing physical easing functions such as
119    /// springs to overshoot.
120    pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
121        self.easing = Rc::new(easing);
122        self
123    }
124
125    /// Limit how often this animation re-renders. Instead of re-rendering on
126    /// every frame, the animation schedules its next render `1 / max_fps`
127    /// seconds after the current one. Values that are not finite and positive
128    /// are ignored.
129    pub fn with_max_fps(mut self, max_fps: f32) -> Self {
130        self.max_fps = Some(max_fps);
131        self
132    }
133}
134
135/// An extension trait for adding the animation wrapper to both Elements and Components
136pub trait AnimationExt {
137    /// Render this component or element with an animation
138    fn with_animation(
139        self,
140        id: impl Into<ElementId>,
141        animation: Animation,
142        animator: impl Fn(Self, f32) -> Self + 'static,
143    ) -> AnimationElement<Self>
144    where
145        Self: Sized,
146    {
147        AnimationElement {
148            id: id.into(),
149            element: Some(self),
150            animator: Box::new(move |this, _, value| animator(this, value)),
151            animations: smallvec::smallvec![animation],
152            cancel_handle: None,
153        }
154    }
155
156    /// Render this component or element with a chain of animations
157    fn with_animations(
158        self,
159        id: impl Into<ElementId>,
160        animations: Vec<Animation>,
161        animator: impl Fn(Self, usize, f32) -> Self + 'static,
162    ) -> AnimationElement<Self>
163    where
164        Self: Sized,
165    {
166        AnimationElement {
167            id: id.into(),
168            element: Some(self),
169            animator: Box::new(animator),
170            animations: animations.into(),
171            cancel_handle: None,
172        }
173    }
174
175    /// Render this component or element with a cancellable animation.
176    /// Returns the animated element and a handle that can be used to cancel the animation.
177    fn with_cancellable_animation(
178        self,
179        id: impl Into<ElementId>,
180        animation: Animation,
181        animator: impl Fn(Self, f32) -> Self + 'static,
182    ) -> (AnimationElement<Self>, AnimationHandle)
183    where
184        Self: Sized,
185    {
186        let handle = AnimationHandle::new();
187        let element = AnimationElement {
188            id: id.into(),
189            element: Some(self),
190            animator: Box::new(move |this, _, value| animator(this, value)),
191            animations: smallvec::smallvec![animation],
192            cancel_handle: Some(handle.cancelled.clone()),
193        };
194        (element, handle)
195    }
196
197    /// Renders this component or element at the value produced by a spring.
198    ///
199    /// The element ID preserves position and velocity across target changes.
200    /// A newly mounted spring starts at its target unless configured with
201    /// [`SpringAnimation::from`].
202    fn with_spring<T>(
203        self,
204        id: impl Into<ElementId>,
205        animation: SpringAnimation<T>,
206        animator: impl FnOnce(Self, T::Output) -> Self + 'static,
207    ) -> SpringAnimationElement<Self>
208    where
209        Self: Sized,
210        T: SpringTarget,
211        T::Output: 'static,
212    {
213        let SpringAnimation {
214            config,
215            target,
216            epsilon,
217            initial,
218            playback,
219        } = animation;
220        let scalar_target = target.target();
221        SpringAnimationElement {
222            id: id.into(),
223            element: Some(self),
224            config,
225            target: scalar_target,
226            epsilon,
227            initial,
228            playback,
229            animator: Some(Box::new(move |this, value| {
230                animator(this, target.resolve(value))
231            })),
232        }
233    }
234}
235
236impl<E: IntoElement + 'static> AnimationExt for E {}
237
238/// A GPUI element that applies an animation to another element
239pub struct AnimationElement<E> {
240    id: ElementId,
241    element: Option<E>,
242    animations: SmallVec<[Animation; 1]>,
243    animator: Box<dyn Fn(E, usize, f32) -> E + 'static>,
244    cancel_handle: Option<Rc<Cell<bool>>>,
245}
246
247/// A GPUI element driven by a stateful spring.
248pub struct SpringAnimationElement<E> {
249    id: ElementId,
250    element: Option<E>,
251    config: SpringConfig,
252    target: f32,
253    epsilon: f32,
254    initial: Option<f32>,
255    playback: SpringPlayback,
256    animator: Option<Box<dyn FnOnce(E, f32) -> E + 'static>>,
257}
258
259impl<E: ParentElement> ParentElement for SpringAnimationElement<E> {
260    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
261        let Some(element) = &mut self.element else {
262            return;
263        };
264
265        element.extend(elements);
266    }
267}
268
269impl<E> SpringAnimationElement<E> {
270    /// Returns a new [`SpringAnimationElement<E>`] after applying the given function
271    /// to the element being animated.
272    pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> SpringAnimationElement<E> {
273        self.element = self.element.map(f);
274        self
275    }
276}
277
278impl<E: IntoElement + 'static> IntoElement for SpringAnimationElement<E> {
279    type Element = SpringAnimationElement<E>;
280
281    fn into_element(self) -> Self::Element {
282        self
283    }
284}
285
286impl<E: ParentElement> ParentElement for AnimationElement<E> {
287    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
288        let Some(element) = &mut self.element else {
289            return;
290        };
291
292        element.extend(elements);
293    }
294}
295
296impl<E> AnimationElement<E> {
297    /// Returns a new [`AnimationElement<E>`] after applying the given function
298    /// to the element being animated.
299    pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement<E> {
300        self.element = self.element.map(f);
301        self
302    }
303}
304
305impl<E: IntoElement + 'static> IntoElement for AnimationElement<E> {
306    type Element = AnimationElement<E>;
307
308    fn into_element(self) -> Self::Element {
309        self
310    }
311}
312
313struct AnimationState {
314    start: Instant,
315    animation_ix: usize,
316    /// Whether a repaint timer is already scheduled, so overlapping renders
317    /// don't stack extra timers.
318    repaint_pending: Rc<Cell<bool>>,
319    /// Identifies the currently scheduled repaint timer. A newer animation
320    /// phase invalidates older timers without needing to cancel their tasks.
321    repaint_generation: Rc<Cell<u64>>,
322}
323
324/// Where a single animation is in its timeline for a given elapsed time,
325/// ignoring easing.
326#[derive(Clone, Copy, Debug, PartialEq)]
327enum AnimationPass {
328    /// The start delay has not elapsed yet; progress is zero.
329    Delayed,
330    /// The animation is running. `cycle_delta` is the normalized progress
331    /// within the current pass, in `0.0..=1.0`.
332    Running { cycle_delta: f32 },
333    /// All passes are finished; progress is one.
334    Finished,
335}
336
337fn animation_pass(animation: &Animation, elapsed: Duration) -> AnimationPass {
338    if elapsed < animation.delay {
339        return AnimationPass::Delayed;
340    }
341
342    let elapsed = elapsed - animation.delay;
343    if animation.duration.is_zero() {
344        return AnimationPass::Finished;
345    }
346
347    match animation.repeat {
348        AnimationRepeat::Once => {
349            if elapsed >= animation.duration {
350                AnimationPass::Finished
351            } else {
352                AnimationPass::Running {
353                    cycle_delta: elapsed.as_secs_f32() / animation.duration.as_secs_f32(),
354                }
355            }
356        }
357        AnimationRepeat::Forever => {
358            let delta = elapsed.as_secs_f32() / animation.duration.as_secs_f32();
359            AnimationPass::Running {
360                cycle_delta: delta % 1.0,
361            }
362        }
363        AnimationRepeat::Times(times) => {
364            let times = times.max(1);
365            let total_duration = animation
366                .duration
367                .checked_mul(times)
368                .unwrap_or(Duration::MAX);
369            if elapsed >= total_duration {
370                AnimationPass::Finished
371            } else {
372                let delta = elapsed.as_secs_f32() / animation.duration.as_secs_f32();
373                AnimationPass::Running {
374                    cycle_delta: delta % 1.0,
375                }
376            }
377        }
378    }
379}
380
381fn schedule_repaint(
382    window: &mut Window,
383    cx: &mut App,
384    delay: Duration,
385    repaint_pending: &Rc<Cell<bool>>,
386    repaint_generation: &Rc<Cell<u64>>,
387) {
388    if repaint_pending.get() {
389        return;
390    }
391
392    repaint_pending.set(true);
393    let generation = repaint_generation.get().wrapping_add(1);
394    repaint_generation.set(generation);
395    let repaint_pending = repaint_pending.clone();
396    let repaint_generation = repaint_generation.clone();
397    let view = window.current_view();
398    window
399        .spawn(cx, async move |cx| {
400            cx.background_executor().timer(delay).await;
401            if repaint_generation.get() != generation {
402                return;
403            }
404            repaint_pending.set(false);
405            cx.update(move |_, cx| cx.notify(view)).ok();
406        })
407        .detach();
408}
409
410fn invalidate_scheduled_repaint(state: &AnimationState) {
411    state
412        .repaint_generation
413        .set(state.repaint_generation.get().wrapping_add(1));
414    state.repaint_pending.set(false);
415}
416
417struct SpringElementState {
418    spring: SpringState,
419    target: f32,
420    config: SpringConfig,
421    initial: f32,
422    playback: SpringPlayback,
423    updated_at: Instant,
424}
425
426impl<E: IntoElement + 'static> Element for SpringAnimationElement<E> {
427    type RequestLayoutState = AnyElement;
428    type PrepaintState = ();
429
430    fn id(&self) -> Option<ElementId> {
431        Some(self.id.clone())
432    }
433
434    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
435        None
436    }
437
438    fn request_layout(
439        &mut self,
440        global_id: Option<&GlobalElementId>,
441        _inspector_id: Option<&InspectorElementId>,
442        window: &mut Window,
443        cx: &mut App,
444    ) -> (crate::LayoutId, Self::RequestLayoutState) {
445        window.with_element_state(global_id.unwrap(), |state, window| {
446            let now = cx.background_executor().now();
447            let initial = self.initial.unwrap_or(self.target);
448            let mut state = state.unwrap_or_else(|| SpringElementState {
449                spring: SpringState {
450                    position: initial,
451                    velocity: 0.0,
452                },
453                target: self.target,
454                config: self.config,
455                initial,
456                playback: self.playback,
457                updated_at: now,
458            });
459
460            let elapsed = now.duration_since(state.updated_at).as_secs_f32();
461            match state.playback {
462                SpringPlayback::Running => {
463                    state.spring = state.config.step(state.spring, state.target, elapsed);
464                }
465                SpringPlayback::Paused
466                | SpringPlayback::Stopped
467                | SpringPlayback::Completed
468                | SpringPlayback::Cancelled => {}
469            }
470
471            state.config = self.config;
472            state.target = self.target;
473
474            let done = match self.playback {
475                SpringPlayback::Running => {
476                    let done = state
477                        .config
478                        .is_settled(state.spring, state.target, self.epsilon);
479                    if done {
480                        state.spring = SpringState {
481                            position: state.target,
482                            velocity: 0.0,
483                        };
484                    }
485                    done
486                }
487                SpringPlayback::Paused => true,
488                SpringPlayback::Stopped => {
489                    state.spring.velocity = 0.0;
490                    true
491                }
492                SpringPlayback::Completed => {
493                    state.spring = SpringState {
494                        position: state.target,
495                        velocity: 0.0,
496                    };
497                    true
498                }
499                SpringPlayback::Cancelled => {
500                    state.spring = SpringState {
501                        position: state.initial,
502                        velocity: 0.0,
503                    };
504                    true
505                }
506            };
507            state.playback = self.playback;
508            state.updated_at = now;
509
510            let element = self.element.take().expect("should only be called once");
511            let animator = self.animator.take().expect("should only be called once");
512            let mut element = animator(element, state.spring.position).into_any_element();
513
514            if !done {
515                window.request_animation_frame();
516            }
517
518            ((element.request_layout(window, cx), element), state)
519        })
520    }
521
522    fn prepaint(
523        &mut self,
524        _id: Option<&GlobalElementId>,
525        _inspector_id: Option<&InspectorElementId>,
526        _bounds: crate::Bounds<crate::Pixels>,
527        element: &mut Self::RequestLayoutState,
528        window: &mut Window,
529        cx: &mut App,
530    ) -> Self::PrepaintState {
531        element.prepaint(window, cx);
532    }
533
534    fn paint(
535        &mut self,
536        _id: Option<&GlobalElementId>,
537        _inspector_id: Option<&InspectorElementId>,
538        _bounds: crate::Bounds<crate::Pixels>,
539        element: &mut Self::RequestLayoutState,
540        _: &mut Self::PrepaintState,
541        window: &mut Window,
542        cx: &mut App,
543    ) {
544        element.paint(window, cx);
545    }
546}
547
548impl<E: IntoElement + 'static> Element for AnimationElement<E> {
549    type RequestLayoutState = AnyElement;
550    type PrepaintState = ();
551
552    fn id(&self) -> Option<ElementId> {
553        Some(self.id.clone())
554    }
555
556    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
557        None
558    }
559
560    fn request_layout(
561        &mut self,
562        global_id: Option<&GlobalElementId>,
563        _inspector_id: Option<&InspectorElementId>,
564        window: &mut Window,
565        cx: &mut App,
566    ) -> (crate::LayoutId, Self::RequestLayoutState) {
567        window.with_element_state(global_id.unwrap(), |state, window| {
568            // One sample for this layout. Progress, chain handoff, and the
569            // delay/frame timer all have to agree, including under the test clock.
570            let now = cx.background_executor().now();
571            let mut state = state.unwrap_or_else(|| AnimationState {
572                start: now,
573                animation_ix: 0,
574                repaint_pending: Rc::new(Cell::new(false)),
575                repaint_generation: Rc::new(Cell::new(0)),
576            });
577
578            let cancelled = self.cancel_handle.as_ref().map_or(false, |h| h.get());
579
580            let animation_ix = state.animation_ix;
581            let animation = self.animations[animation_ix].clone();
582
583            let (delta, done) = if cancelled {
584                (1.0_f32, true)
585            } else {
586                let elapsed = now - state.start;
587                match animation_pass(&animation, elapsed) {
588                    AnimationPass::Delayed => ((animation.easing)(0.0), false),
589                    AnimationPass::Running { cycle_delta } => {
590                        ((animation.easing)(cycle_delta), false)
591                    }
592                    AnimationPass::Finished => {
593                        if animation_ix >= self.animations.len() - 1 {
594                            ((animation.easing)(1.0), true)
595                        } else {
596                            state.start = now;
597                            state.animation_ix += 1;
598                            ((animation.easing)(1.0), false)
599                        }
600                    }
601                }
602            };
603
604            debug_assert!(delta.is_finite(), "animated value should be finite");
605
606            let element = self.element.take().expect("should only be called once");
607            let mut element = (self.animator)(element, animation_ix, delta).into_any_element();
608
609            let animation_changed = state.animation_ix != animation_ix;
610            if animation_changed || done {
611                // A timer for the previous animation must not delay the next
612                // animation's delay or frame-rate schedule, or repaint after
613                // the animation has already completed.
614                invalidate_scheduled_repaint(&state);
615            }
616
617            if !done {
618                let animation = &self.animations[state.animation_ix];
619                let elapsed = now - state.start;
620                if elapsed < animation.delay {
621                    let remaining = animation.delay - elapsed;
622                    schedule_repaint(
623                        window,
624                        cx,
625                        remaining,
626                        &state.repaint_pending,
627                        &state.repaint_generation,
628                    );
629                } else {
630                    match animation.max_fps {
631                        Some(max_fps) if max_fps.is_finite() && max_fps > 0.0 => {
632                            let interval = Duration::from_secs_f32(1.0 / max_fps);
633                            schedule_repaint(
634                                window,
635                                cx,
636                                interval,
637                                &state.repaint_pending,
638                                &state.repaint_generation,
639                            );
640                        }
641                        _ => window.request_animation_frame(),
642                    }
643                }
644            }
645
646            ((element.request_layout(window, cx), element), state)
647        })
648    }
649
650    fn prepaint(
651        &mut self,
652        _id: Option<&GlobalElementId>,
653        _inspector_id: Option<&InspectorElementId>,
654        _bounds: crate::Bounds<crate::Pixels>,
655        element: &mut Self::RequestLayoutState,
656        window: &mut Window,
657        cx: &mut App,
658    ) -> Self::PrepaintState {
659        element.prepaint(window, cx);
660    }
661
662    fn paint(
663        &mut self,
664        _id: Option<&GlobalElementId>,
665        _inspector_id: Option<&InspectorElementId>,
666        _bounds: crate::Bounds<crate::Pixels>,
667        element: &mut Self::RequestLayoutState,
668        _: &mut Self::PrepaintState,
669        window: &mut Window,
670        cx: &mut App,
671    ) {
672        element.paint(window, cx);
673    }
674}
675
676mod easing {
677    use std::f32::consts::PI;
678
679    /// The linear easing function, or delta itself
680    pub fn linear(delta: f32) -> f32 {
681        delta
682    }
683
684    /// The quadratic easing function, delta * delta
685    pub fn quadratic(delta: f32) -> f32 {
686        delta * delta
687    }
688
689    /// The quadratic ease-in-out function, which starts and ends slowly but speeds up in the middle
690    pub fn ease_in_out(delta: f32) -> f32 {
691        if delta < 0.5 {
692            2.0 * delta * delta
693        } else {
694            let x = -2.0 * delta + 2.0;
695            1.0 - x * x / 2.0
696        }
697    }
698
699    /// The Quint ease-out function, which starts quickly and decelerates to a stop
700    pub fn ease_out_quint() -> impl Fn(f32) -> f32 {
701        move |delta| 1.0 - (1.0 - delta).powi(5)
702    }
703
704    /// Apply the given easing function, first in the forward direction and then in the reverse direction
705    pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 {
706        move |delta| {
707            if delta < 0.5 {
708                easing(delta * 2.0)
709            } else {
710                easing((1.0 - delta) * 2.0)
711            }
712        }
713    }
714
715    /// A custom easing function for pulsating alpha that slows down as it approaches 0.1
716    pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 {
717        let range = max - min;
718
719        move |delta| {
720            // Use a combination of sine and cubic functions for a more natural breathing rhythm
721            let t = (delta * 2.0 * PI).sin();
722            let breath = (t * t * t + t) / 2.0;
723
724            // Map the breath to our desired alpha range
725            let normalized_alpha = (breath + 1.0) / 2.0;
726
727            min + (normalized_alpha * range)
728        }
729    }
730}
731
732#[cfg(test)]
733mod tests {
734    use std::{cell::RefCell, rc::Rc};
735
736    use crate::{
737        Context, InteractiveElement, IntoElement, Pixels, Render, SpringPlayback, Styled,
738        TestAppContext, Window, WindowHandle, div, px,
739    };
740
741    use super::*;
742
743    #[test]
744    fn test_animation_parent() {
745        div()
746            .id("id")
747            .with_animation(
748                "animation",
749                Animation::new(Duration::from_secs(1)),
750                |el, _t| el,
751            )
752            .child(div());
753    }
754
755    #[test]
756    fn test_spring_animation_parent() {
757        div()
758            .id("id")
759            .with_spring(
760                "spring-animation",
761                SpringAnimation::new(SpringConfig::new(100.0, 10.0, 1.0))
762                    .to(px(10.0))
763                    .from(px(0.0)),
764                |element, value| element.left(value),
765            )
766            .child(div());
767    }
768
769    #[test]
770    fn test_with_max_fps_stores_positive_and_invalid_rates() {
771        let capped = Animation::new(Duration::from_secs(1)).with_max_fps(12.0);
772        assert_eq!(capped.max_fps, Some(12.0));
773
774        let invalid = Animation::new(Duration::from_secs(1)).with_max_fps(0.0);
775        assert_eq!(invalid.max_fps, Some(0.0));
776    }
777
778    #[test]
779    fn test_with_delay_stores_delay() {
780        let delayed = Animation::new(Duration::from_secs(1)).with_delay(Duration::from_millis(250));
781        assert_eq!(delayed.delay, Duration::from_millis(250));
782
783        let immediate = Animation::new(Duration::from_secs(1));
784        assert_eq!(immediate.delay, Duration::ZERO);
785    }
786
787    #[test]
788    fn test_repeat_builders() {
789        assert_eq!(
790            Animation::new(Duration::from_secs(1)).repeat,
791            AnimationRepeat::Once
792        );
793        assert_eq!(
794            Animation::new(Duration::from_secs(1)).repeat().repeat,
795            AnimationRepeat::Forever
796        );
797        assert_eq!(
798            Animation::new(Duration::from_secs(1)).repeat_n(3).repeat,
799            AnimationRepeat::Times(3)
800        );
801        assert_eq!(
802            Animation::new(Duration::from_secs(1))
803                .with_repeat(AnimationRepeat::Times(2))
804                .repeat,
805            AnimationRepeat::Times(2)
806        );
807    }
808
809    #[test]
810    fn test_animation_pass_once() {
811        let animation = Animation::new(Duration::from_secs(2));
812        assert_eq!(
813            animation_pass(&animation, Duration::ZERO),
814            AnimationPass::Running { cycle_delta: 0.0 }
815        );
816        assert_eq!(
817            animation_pass(&animation, Duration::from_secs(1)),
818            AnimationPass::Running { cycle_delta: 0.5 }
819        );
820        assert_eq!(
821            animation_pass(&animation, Duration::from_secs(2)),
822            AnimationPass::Finished
823        );
824        assert_eq!(
825            animation_pass(&animation, Duration::from_secs(3)),
826            AnimationPass::Finished
827        );
828    }
829
830    #[test]
831    fn test_animation_pass_delay_holds_zero_then_runs() {
832        let animation =
833            Animation::new(Duration::from_secs(2)).with_delay(Duration::from_millis(500));
834        assert_eq!(
835            animation_pass(&animation, Duration::from_millis(499)),
836            AnimationPass::Delayed
837        );
838        assert_eq!(
839            animation_pass(&animation, Duration::from_millis(500)),
840            AnimationPass::Running { cycle_delta: 0.0 }
841        );
842        assert_eq!(
843            animation_pass(&animation, Duration::from_millis(1500)),
844            AnimationPass::Running { cycle_delta: 0.5 }
845        );
846        assert_eq!(
847            animation_pass(&animation, Duration::from_millis(2600)),
848            AnimationPass::Finished
849        );
850    }
851
852    #[test]
853    fn test_animation_pass_forever_wraps() {
854        let animation = Animation::new(Duration::from_secs(1)).repeat();
855        assert_eq!(
856            animation_pass(&animation, Duration::from_millis(1500)),
857            AnimationPass::Running { cycle_delta: 0.5 }
858        );
859        assert_eq!(
860            animation_pass(&animation, Duration::from_secs(3)),
861            AnimationPass::Running { cycle_delta: 0.0 }
862        );
863    }
864
865    #[test]
866    fn test_animation_pass_finite_times() {
867        let animation = Animation::new(Duration::from_secs(1)).repeat_n(3);
868        assert_eq!(
869            animation_pass(&animation, Duration::from_millis(1500)),
870            AnimationPass::Running { cycle_delta: 0.5 }
871        );
872        // Third and final pass.
873        assert_eq!(
874            animation_pass(&animation, Duration::from_millis(2500)),
875            AnimationPass::Running { cycle_delta: 0.5 }
876        );
877        // Finish exactly when the final pass completes.
878        assert_eq!(
879            animation_pass(&animation, Duration::from_secs(3)),
880            AnimationPass::Finished
881        );
882        assert_eq!(
883            animation_pass(&animation, Duration::from_millis(3100)),
884            AnimationPass::Finished
885        );
886    }
887
888    #[test]
889    fn test_animation_pass_times_zero_clamps_to_once() {
890        let animation = Animation::new(Duration::from_secs(1)).repeat_n(0);
891        assert_eq!(
892            animation_pass(&animation, Duration::from_millis(500)),
893            AnimationPass::Running { cycle_delta: 0.5 }
894        );
895        assert_eq!(
896            animation_pass(&animation, Duration::from_millis(1500)),
897            AnimationPass::Finished
898        );
899    }
900
901    #[test]
902    fn test_animation_pass_zero_duration_finishes_immediately() {
903        let animation = Animation::new(Duration::ZERO);
904
905        assert_eq!(
906            animation_pass(&animation, Duration::ZERO),
907            AnimationPass::Finished
908        );
909        assert_eq!(
910            animation_pass(&animation.repeat(), Duration::from_secs(1)),
911            AnimationPass::Finished
912        );
913    }
914
915    struct AnimationTestView {
916        rendered_deltas: Rc<RefCell<Vec<f32>>>,
917        max_fps: Option<f32>,
918    }
919
920    struct SpringAnimationTestView {
921        target: Pixels,
922        initial: Option<Pixels>,
923        playback: SpringPlayback,
924        rendered_values: Rc<RefCell<Vec<Pixels>>>,
925    }
926
927    impl Render for SpringAnimationTestView {
928        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
929            let rendered_values = self.rendered_values.clone();
930            let mut animation = SpringAnimation::new(SpringConfig::new(100.0, 2.0, 1.0))
931                .to(self.target)
932                .with_epsilon(0.01)
933                .playback(self.playback);
934            if let Some(initial) = self.initial {
935                animation = animation.from(initial);
936            }
937            div().with_spring("spring-animation", animation, move |this, value| {
938                rendered_values.borrow_mut().push(value);
939                this.left(value)
940            })
941        }
942    }
943
944    impl Render for AnimationTestView {
945        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
946            let rendered_deltas = self.rendered_deltas.clone();
947            let mut animation = Animation::new(Duration::from_secs(1)).repeat();
948            if let Some(max_fps) = self.max_fps {
949                animation = animation.with_max_fps(max_fps);
950            }
951            div().size_full().child(div().with_animation(
952                "repeating-animation",
953                animation,
954                move |this, delta| {
955                    rendered_deltas.borrow_mut().push(delta);
956                    this
957                },
958            ))
959        }
960    }
961
962    fn open_test_window_with_max_fps(
963        cx: &mut TestAppContext,
964        max_fps: Option<f32>,
965    ) -> (Rc<RefCell<Vec<f32>>>, WindowHandle<AnimationTestView>) {
966        let rendered_deltas = Rc::new(RefCell::new(Vec::new()));
967        let window = cx.add_window({
968            let rendered_deltas = rendered_deltas.clone();
969            move |_, _| AnimationTestView {
970                rendered_deltas,
971                max_fps,
972            }
973        });
974        cx.run_until_parked();
975        (rendered_deltas, window)
976    }
977
978    /// This fork's test windows do not pump `request_animation_frame` callbacks.
979    /// Notifying the root view redraws it on the executor clock instead.
980    fn redraw<V: Render>(window: &WindowHandle<V>, cx: &mut TestAppContext) {
981        window.update(cx, |_, _, cx| cx.notify()).unwrap();
982        cx.run_until_parked();
983    }
984
985    /// Upstream `test_max_fps_schedules_timer_driven_frames`, without synced
986    /// animations. A capped repeating animation follows the test scheduler.
987    #[gpui::test]
988    fn test_max_fps_schedules_timer_driven_frames(cx: &mut TestAppContext) {
989        let (rendered_deltas, _window) = open_test_window_with_max_fps(cx, Some(10.0));
990
991        let assert_deltas_approx_eq = |expected: &[f32]| {
992            let actual = rendered_deltas.borrow();
993            assert_eq!(actual.len(), expected.len(), "deltas: {actual:?}");
994            for (actual, expected) in actual.iter().zip(expected) {
995                assert!(
996                    (actual - expected).abs() < 1e-2,
997                    "expected {expected}, got {actual}"
998                );
999            }
1000        };
1001
1002        assert_deltas_approx_eq(&[0.0]);
1003
1004        // The cap is a timer, so parking without moving the clock adds no frame.
1005        cx.run_until_parked();
1006        assert_deltas_approx_eq(&[0.0]);
1007
1008        // The timer is due at 100ms. This scheduler renders at that deadline,
1009        // then parks the clock at the requested 105ms.
1010        cx.executor().advance_clock(Duration::from_millis(105));
1011        cx.run_until_parked();
1012        assert_deltas_approx_eq(&[0.0, 0.1]);
1013
1014        cx.executor().advance_clock(Duration::from_millis(105));
1015        cx.run_until_parked();
1016        assert_deltas_approx_eq(&[0.0, 0.1, 0.2]);
1017    }
1018
1019    /// Upstream `test_spring_animation_preserves_velocity_when_retargeted`.
1020    /// Stepping uses the executor clock, so advancing it moves the spring
1021    /// instead of however much wall time passed between frames.
1022    #[gpui::test]
1023    fn test_spring_animation_preserves_velocity_when_retargeted(cx: &mut TestAppContext) {
1024        let rendered_values = Rc::new(RefCell::new(Vec::new()));
1025        let window = cx.add_window({
1026            let rendered_values = rendered_values.clone();
1027            move |_, _| SpringAnimationTestView {
1028                target: px(0.0),
1029                initial: None,
1030                playback: SpringPlayback::Running,
1031                rendered_values,
1032            }
1033        });
1034        cx.run_until_parked();
1035        assert_eq!(*rendered_values.borrow(), vec![px(0.0)]);
1036
1037        window
1038            .update(cx, |view, _, cx| {
1039                view.target = px(100.0);
1040                cx.notify();
1041            })
1042            .unwrap();
1043        cx.run_until_parked();
1044
1045        cx.executor().advance_clock(Duration::from_millis(50));
1046        redraw(&window, cx);
1047        let value_before_retargeting = *rendered_values.borrow().last().unwrap();
1048        assert!(value_before_retargeting > px(0.0));
1049        assert!(value_before_retargeting < px(100.0));
1050
1051        window
1052            .update(cx, |view, _, cx| {
1053                view.target = px(0.0);
1054                cx.notify();
1055            })
1056            .unwrap();
1057        cx.run_until_parked();
1058
1059        cx.executor().advance_clock(Duration::from_millis(5));
1060        redraw(&window, cx);
1061        let value_after_retargeting = *rendered_values.borrow().last().unwrap();
1062        assert!(value_after_retargeting > value_before_retargeting);
1063    }
1064}