Skip to main content

gpui/elements/
animation.rs

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