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 = Instant::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            let mut state = state.unwrap_or_else(|| AnimationState {
569                start: Instant::now(),
570                animation_ix: 0,
571                repaint_pending: Rc::new(Cell::new(false)),
572                repaint_generation: Rc::new(Cell::new(0)),
573            });
574
575            let cancelled = self.cancel_handle.as_ref().map_or(false, |h| h.get());
576
577            let animation_ix = state.animation_ix;
578            let animation = self.animations[animation_ix].clone();
579
580            let (delta, done) = if cancelled {
581                (1.0_f32, true)
582            } else {
583                let elapsed = state.start.elapsed();
584                match animation_pass(&animation, elapsed) {
585                    AnimationPass::Delayed => ((animation.easing)(0.0), false),
586                    AnimationPass::Running { cycle_delta } => {
587                        ((animation.easing)(cycle_delta), false)
588                    }
589                    AnimationPass::Finished => {
590                        if animation_ix >= self.animations.len() - 1 {
591                            ((animation.easing)(1.0), true)
592                        } else {
593                            state.start = Instant::now();
594                            state.animation_ix += 1;
595                            ((animation.easing)(1.0), false)
596                        }
597                    }
598                }
599            };
600
601            debug_assert!(delta.is_finite(), "animated value should be finite");
602
603            let element = self.element.take().expect("should only be called once");
604            let mut element = (self.animator)(element, animation_ix, delta).into_any_element();
605
606            let animation_changed = state.animation_ix != animation_ix;
607            if animation_changed || done {
608                // A timer for the previous animation must not delay the next
609                // animation's delay or frame-rate schedule, or repaint after
610                // the animation has already completed.
611                invalidate_scheduled_repaint(&state);
612            }
613
614            if !done {
615                let animation = &self.animations[state.animation_ix];
616                let elapsed = state.start.elapsed();
617                if elapsed < animation.delay {
618                    let remaining = animation.delay - elapsed;
619                    schedule_repaint(
620                        window,
621                        cx,
622                        remaining,
623                        &state.repaint_pending,
624                        &state.repaint_generation,
625                    );
626                } else {
627                    match animation.max_fps {
628                        Some(max_fps) if max_fps.is_finite() && max_fps > 0.0 => {
629                            let interval = Duration::from_secs_f32(1.0 / max_fps);
630                            schedule_repaint(
631                                window,
632                                cx,
633                                interval,
634                                &state.repaint_pending,
635                                &state.repaint_generation,
636                            );
637                        }
638                        _ => window.request_animation_frame(),
639                    }
640                }
641            }
642
643            ((element.request_layout(window, cx), element), state)
644        })
645    }
646
647    fn prepaint(
648        &mut self,
649        _id: Option<&GlobalElementId>,
650        _inspector_id: Option<&InspectorElementId>,
651        _bounds: crate::Bounds<crate::Pixels>,
652        element: &mut Self::RequestLayoutState,
653        window: &mut Window,
654        cx: &mut App,
655    ) -> Self::PrepaintState {
656        element.prepaint(window, cx);
657    }
658
659    fn paint(
660        &mut self,
661        _id: Option<&GlobalElementId>,
662        _inspector_id: Option<&InspectorElementId>,
663        _bounds: crate::Bounds<crate::Pixels>,
664        element: &mut Self::RequestLayoutState,
665        _: &mut Self::PrepaintState,
666        window: &mut Window,
667        cx: &mut App,
668    ) {
669        element.paint(window, cx);
670    }
671}
672
673mod easing {
674    use std::f32::consts::PI;
675
676    /// The linear easing function, or delta itself
677    pub fn linear(delta: f32) -> f32 {
678        delta
679    }
680
681    /// The quadratic easing function, delta * delta
682    pub fn quadratic(delta: f32) -> f32 {
683        delta * delta
684    }
685
686    /// The quadratic ease-in-out function, which starts and ends slowly but speeds up in the middle
687    pub fn ease_in_out(delta: f32) -> f32 {
688        if delta < 0.5 {
689            2.0 * delta * delta
690        } else {
691            let x = -2.0 * delta + 2.0;
692            1.0 - x * x / 2.0
693        }
694    }
695
696    /// The Quint ease-out function, which starts quickly and decelerates to a stop
697    pub fn ease_out_quint() -> impl Fn(f32) -> f32 {
698        move |delta| 1.0 - (1.0 - delta).powi(5)
699    }
700
701    /// Apply the given easing function, first in the forward direction and then in the reverse direction
702    pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 {
703        move |delta| {
704            if delta < 0.5 {
705                easing(delta * 2.0)
706            } else {
707                easing((1.0 - delta) * 2.0)
708            }
709        }
710    }
711
712    /// A custom easing function for pulsating alpha that slows down as it approaches 0.1
713    pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 {
714        let range = max - min;
715
716        move |delta| {
717            // Use a combination of sine and cubic functions for a more natural breathing rhythm
718            let t = (delta * 2.0 * PI).sin();
719            let breath = (t * t * t + t) / 2.0;
720
721            // Map the breath to our desired alpha range
722            let normalized_alpha = (breath + 1.0) / 2.0;
723
724            min + (normalized_alpha * range)
725        }
726    }
727}
728
729#[cfg(test)]
730mod tests {
731    use crate::{InteractiveElement, Styled, div, px};
732
733    use super::*;
734
735    #[test]
736    fn test_animation_parent() {
737        div()
738            .id("id")
739            .with_animation(
740                "animation",
741                Animation::new(Duration::from_secs(1)),
742                |el, _t| el,
743            )
744            .child(div());
745    }
746
747    #[test]
748    fn test_spring_animation_parent() {
749        div()
750            .id("id")
751            .with_spring(
752                "spring-animation",
753                SpringAnimation::new(SpringConfig::new(100.0, 10.0, 1.0))
754                    .to(px(10.0))
755                    .from(px(0.0)),
756                |element, value| element.left(value),
757            )
758            .child(div());
759    }
760
761    #[test]
762    fn test_with_max_fps_stores_positive_and_invalid_rates() {
763        let capped = Animation::new(Duration::from_secs(1)).with_max_fps(12.0);
764        assert_eq!(capped.max_fps, Some(12.0));
765
766        let invalid = Animation::new(Duration::from_secs(1)).with_max_fps(0.0);
767        assert_eq!(invalid.max_fps, Some(0.0));
768    }
769
770    #[test]
771    fn test_with_delay_stores_delay() {
772        let delayed = Animation::new(Duration::from_secs(1)).with_delay(Duration::from_millis(250));
773        assert_eq!(delayed.delay, Duration::from_millis(250));
774
775        let immediate = Animation::new(Duration::from_secs(1));
776        assert_eq!(immediate.delay, Duration::ZERO);
777    }
778
779    #[test]
780    fn test_repeat_builders() {
781        assert_eq!(
782            Animation::new(Duration::from_secs(1)).repeat,
783            AnimationRepeat::Once
784        );
785        assert_eq!(
786            Animation::new(Duration::from_secs(1)).repeat().repeat,
787            AnimationRepeat::Forever
788        );
789        assert_eq!(
790            Animation::new(Duration::from_secs(1)).repeat_n(3).repeat,
791            AnimationRepeat::Times(3)
792        );
793        assert_eq!(
794            Animation::new(Duration::from_secs(1))
795                .with_repeat(AnimationRepeat::Times(2))
796                .repeat,
797            AnimationRepeat::Times(2)
798        );
799    }
800
801    #[test]
802    fn test_animation_pass_once() {
803        let animation = Animation::new(Duration::from_secs(2));
804        assert_eq!(
805            animation_pass(&animation, Duration::ZERO),
806            AnimationPass::Running { cycle_delta: 0.0 }
807        );
808        assert_eq!(
809            animation_pass(&animation, Duration::from_secs(1)),
810            AnimationPass::Running { cycle_delta: 0.5 }
811        );
812        assert_eq!(
813            animation_pass(&animation, Duration::from_secs(2)),
814            AnimationPass::Finished
815        );
816        assert_eq!(
817            animation_pass(&animation, Duration::from_secs(3)),
818            AnimationPass::Finished
819        );
820    }
821
822    #[test]
823    fn test_animation_pass_delay_holds_zero_then_runs() {
824        let animation =
825            Animation::new(Duration::from_secs(2)).with_delay(Duration::from_millis(500));
826        assert_eq!(
827            animation_pass(&animation, Duration::from_millis(499)),
828            AnimationPass::Delayed
829        );
830        assert_eq!(
831            animation_pass(&animation, Duration::from_millis(500)),
832            AnimationPass::Running { cycle_delta: 0.0 }
833        );
834        assert_eq!(
835            animation_pass(&animation, Duration::from_millis(1500)),
836            AnimationPass::Running { cycle_delta: 0.5 }
837        );
838        assert_eq!(
839            animation_pass(&animation, Duration::from_millis(2600)),
840            AnimationPass::Finished
841        );
842    }
843
844    #[test]
845    fn test_animation_pass_forever_wraps() {
846        let animation = Animation::new(Duration::from_secs(1)).repeat();
847        assert_eq!(
848            animation_pass(&animation, Duration::from_millis(1500)),
849            AnimationPass::Running { cycle_delta: 0.5 }
850        );
851        assert_eq!(
852            animation_pass(&animation, Duration::from_secs(3)),
853            AnimationPass::Running { cycle_delta: 0.0 }
854        );
855    }
856
857    #[test]
858    fn test_animation_pass_finite_times() {
859        let animation = Animation::new(Duration::from_secs(1)).repeat_n(3);
860        assert_eq!(
861            animation_pass(&animation, Duration::from_millis(1500)),
862            AnimationPass::Running { cycle_delta: 0.5 }
863        );
864        // Third and final pass.
865        assert_eq!(
866            animation_pass(&animation, Duration::from_millis(2500)),
867            AnimationPass::Running { cycle_delta: 0.5 }
868        );
869        // Finish exactly when the final pass completes.
870        assert_eq!(
871            animation_pass(&animation, Duration::from_secs(3)),
872            AnimationPass::Finished
873        );
874        assert_eq!(
875            animation_pass(&animation, Duration::from_millis(3100)),
876            AnimationPass::Finished
877        );
878    }
879
880    #[test]
881    fn test_animation_pass_times_zero_clamps_to_once() {
882        let animation = Animation::new(Duration::from_secs(1)).repeat_n(0);
883        assert_eq!(
884            animation_pass(&animation, Duration::from_millis(500)),
885            AnimationPass::Running { cycle_delta: 0.5 }
886        );
887        assert_eq!(
888            animation_pass(&animation, Duration::from_millis(1500)),
889            AnimationPass::Finished
890        );
891    }
892
893    #[test]
894    fn test_animation_pass_zero_duration_finishes_immediately() {
895        let animation = Animation::new(Duration::ZERO);
896
897        assert_eq!(
898            animation_pass(&animation, Duration::ZERO),
899            AnimationPass::Finished
900        );
901        assert_eq!(
902            animation_pass(&animation.repeat(), Duration::from_secs(1)),
903            AnimationPass::Finished
904        );
905    }
906}