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, AppContext, Element, ElementId, GlobalElementId, InspectorElementId,
6    IntoElement, Motion, ParentElement, SpringAnimation, SpringConfig, SpringDescription,
7    SpringPlayback, SpringState, SpringTarget, Window,
8};
9
10pub use easing::*;
11use smallvec::SmallVec;
12
13/// A timed change to an element, applied with [`AnimationExt::with_animation`].
14///
15/// An animation is a [`Motion`] with two more options: whether its phase follows the app's shared
16/// clock, and an optional cap on how often it redraws. A [`Duration`](std::time::Duration) turns
17/// into one linear pass.
18///
19/// # Examples
20///
21/// ```
22/// use std::time::Duration;
23///
24/// use gpui::{Animation, Direction, Iterations, Motion, ease_out_quint, millis};
25///
26/// let once = Animation::new(Duration::from_millis(300)).with_easing(ease_out_quint());
27/// assert!(!once.synced);
28/// assert_eq!(once.motion.playback().iterations(), Iterations::default());
29///
30/// // Pass an alternating motion, then repeat it, to ping-pong forever.
31/// let ping_pong = Animation::new(Motion::new(millis(300)).alternate()).repeat();
32/// assert_eq!(ping_pong.motion.playback().iterations(), Iterations::Forever);
33/// assert_eq!(ping_pong.motion.playback().direction(), Direction::Alternate);
34///
35/// assert_eq!(Animation::new(millis(16)).with_max_fps(30.).max_fps, Some(30.0));
36/// ```
37#[derive(Clone)]
38pub struct Animation {
39    /// The timing, repetition and easing of this animation.
40    pub motion: Motion,
41    /// Whether the phase follows the app's shared clock rather than the time this element began
42    /// animating. See [`Animation::repeat_synced`].
43    pub synced: bool,
44    /// The maximum number of re-renders per second. `None` re-renders on every frame.
45    /// See [`Animation::with_max_fps`].
46    pub max_fps: Option<f32>,
47}
48
49impl Animation {
50    /// Creates an animation from a [`Motion`] or a [`Duration`](std::time::Duration).
51    ///
52    /// A duration creates one linear pass that runs once.
53    pub fn new(motion: impl Into<Motion>) -> Self {
54        Self {
55            motion: motion.into(),
56            synced: false,
57            max_fps: None,
58        }
59    }
60
61    /// Repeats the animation forever.
62    ///
63    /// Each cycle starts again from the beginning. The direction doesn't reverse. To ping-pong,
64    /// build the motion with [`Motion::alternate`] and pass it to [`Animation::new`].
65    pub fn repeat(mut self) -> Self {
66        self.motion = self.motion.repeat_forever();
67        self
68    }
69
70    /// Repeats the animation forever, with its phase set by a clock shared by the whole [`App`](crate::App).
71    ///
72    /// Elements that use it stay in step with each other. An element that mounts later joins the
73    /// shared phase, rather than starting from zero.
74    pub fn repeat_synced(mut self) -> Self {
75        self.motion = self.motion.repeat_forever();
76        self.synced = true;
77        self
78    }
79
80    /// Sets the easing curve.
81    ///
82    /// The output isn't clamped. An overshooting curve gives values outside `0.0..=1.0`, so clamp
83    /// in your animator if the property can't take them. A curve such as [`ease_out_quint`] stays
84    /// within that range.
85    pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
86        self.motion = self.motion.with_easing(easing);
87        self
88    }
89
90    /// Caps re-renders at `max_fps` per second, using a timer between frames.
91    ///
92    /// A value that is not finite or not positive is ignored, and the animation re-renders on every
93    /// frame.
94    pub fn with_max_fps(mut self, max_fps: f32) -> Self {
95        self.max_fps = Some(max_fps);
96        self
97    }
98}
99
100/// Adds animation methods to every element and component.
101///
102/// It is implemented for any [`IntoElement`] type, so you can call these methods on a
103/// [`Div`](crate::Div) or on your own component.
104///
105/// Animations honor [`App::reduce_motion`](crate::App::reduce_motion). When it is set, the element
106/// renders in a static state: the end state for a one-pass animation, and the start state for a
107/// repeating one. No animation frames are scheduled. GPUI doesn't read the operating system's
108/// setting, so set it yourself with [`App::set_reduce_motion`](crate::App::set_reduce_motion).
109pub trait AnimationExt {
110    /// Renders this element with `animation`, passing `animator` the eased progress on every frame.
111    ///
112    /// `animator` receives the element and a value from `0.0` to `1.0`, and returns the styled
113    /// element. The `id` identifies the animation across frames. Change it to start the animation
114    /// again, and keep it stable otherwise, or the animation restarts on every render.
115    ///
116    /// # Examples
117    ///
118    /// Under reduce motion, a one-pass animation renders at its end state, `1.0`.
119    ///
120    /// ```
121    /// use std::{cell::RefCell, rc::Rc, time::Duration};
122    ///
123    /// use gpui::{
124    ///     Animation, AnimationExt, Context, IntoElement, Render, TestApp, Window, div, prelude::*, px,
125    /// };
126    ///
127    /// struct Toast {
128    ///     deltas: Rc<RefCell<Vec<f32>>>,
129    /// }
130    ///
131    /// impl Render for Toast {
132    ///     fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
133    ///         let deltas = self.deltas.clone();
134    ///         div().child("Saved").with_animation(
135    ///             "toast-enter",
136    ///             Animation::new(Duration::from_millis(300)),
137    ///             move |toast, delta| {
138    ///                 deltas.borrow_mut().push(delta);
139    ///                 toast.opacity(delta).mt(px(8. * (1. - delta)))
140    ///             },
141    ///         )
142    ///     }
143    /// }
144    ///
145    /// let deltas = Rc::new(RefCell::new(Vec::new()));
146    /// let mut app = TestApp::new();
147    /// app.update(|cx| cx.set_reduce_motion(true));
148    /// let mut window = app.open_window(|_, _| Toast { deltas: deltas.clone() });
149    /// window.draw();
150    /// assert_eq!(deltas.borrow().last(), Some(&1.0));
151    /// ```
152    fn with_animation(
153        self,
154        id: impl Into<ElementId>,
155        animation: Animation,
156        animator: impl Fn(Self, f32) -> Self + 'static,
157    ) -> AnimationElement<Self>
158    where
159        Self: Sized,
160    {
161        AnimationElement {
162            id: id.into(),
163            element: Some(self),
164            animator: Box::new(move |this, _, value| animator(this, value)),
165            animations: smallvec::smallvec![animation],
166        }
167    }
168
169    /// Renders this element with a sequence of animations, one after another.
170    ///
171    /// `animator` receives the index of the running step and its progress from `0.0` to `1.0`.
172    /// Each step starts when the one before it finishes. A repeating step never finishes, so only
173    /// the last step should repeat.
174    ///
175    /// Under reduce motion, the element renders at the end of the last step.
176    ///
177    /// # Panics
178    ///
179    /// Panics if `animations` is empty. Debug builds assert this when the element is created.
180    ///
181    /// # Examples
182    ///
183    /// ```
184    /// use std::{cell::RefCell, rc::Rc, time::Duration};
185    ///
186    /// use gpui::{
187    ///     Animation, AnimationExt, Context, IntoElement, Render, TestApp, Window, div, ease_out_quint,
188    ///     prelude::*, px,
189    /// };
190    ///
191    /// struct Pop {
192    ///     steps: Rc<RefCell<Vec<(usize, f32)>>>,
193    /// }
194    ///
195    /// impl Render for Pop {
196    ///     fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
197    ///         let steps = self.steps.clone();
198    ///         div().child("Done").with_animations(
199    ///             "pop",
200    ///             vec![
201    ///                 Animation::new(Duration::from_millis(150)),
202    ///                 Animation::new(Duration::from_millis(300)).with_easing(ease_out_quint()),
203    ///             ],
204    ///             move |el, step, delta| {
205    ///                 steps.borrow_mut().push((step, delta));
206    ///                 match step {
207    ///                     0 => el.opacity(delta),
208    ///                     _ => el.opacity(1.0).mt(px(8. * (1. - delta))),
209    ///                 }
210    ///             },
211    ///         )
212    ///     }
213    /// }
214    ///
215    /// let steps = Rc::new(RefCell::new(Vec::new()));
216    /// let mut app = TestApp::new();
217    /// app.update(|cx| cx.set_reduce_motion(true));
218    /// let mut window = app.open_window(|_, _| Pop { steps: steps.clone() });
219    /// window.draw();
220    /// assert_eq!(steps.borrow().last(), Some(&(1, 1.0)));
221    /// ```
222    fn with_animations(
223        self,
224        id: impl Into<ElementId>,
225        animations: Vec<Animation>,
226        animator: impl Fn(Self, usize, f32) -> Self + 'static,
227    ) -> AnimationElement<Self>
228    where
229        Self: Sized,
230    {
231        debug_assert!(!animations.is_empty(), "animations must not be empty");
232
233        AnimationElement {
234            id: id.into(),
235            element: Some(self),
236            animator: Box::new(animator),
237            animations: animations.into(),
238        }
239    }
240
241    /// Renders this element at the value of a spring, passed to `animator`.
242    ///
243    /// The `id` keeps the spring's position and velocity across frames, so a retargeted spring
244    /// bends toward the new target. A newly mounted spring starts at its target, unless you set
245    /// a start with [`SpringAnimation::from`]. Under reduce motion, the spring jumps to its target.
246    /// See [`SpringAnimation`] for an example.
247    fn with_spring<T>(
248        self,
249        id: impl Into<ElementId>,
250        animation: SpringAnimation<T>,
251        animator: impl FnOnce(Self, T::Output) -> Self + 'static,
252    ) -> SpringAnimationElement<Self>
253    where
254        Self: Sized,
255        T: SpringTarget,
256        T::Output: 'static,
257    {
258        let SpringAnimation {
259            motion,
260            target,
261            initial,
262            playback,
263        } = animation;
264        let scalar_target = target.target();
265        SpringAnimationElement {
266            id: id.into(),
267            element: Some(self),
268            motion,
269            target: scalar_target,
270            initial,
271            playback,
272            animator: Some(Box::new(move |this, value| {
273                animator(this, target.resolve(value))
274            })),
275        }
276    }
277}
278
279impl<E: IntoElement + 'static> AnimationExt for E {}
280
281/// The element returned by [`AnimationExt::with_animation`] and [`AnimationExt::with_animations`].
282///
283/// It wraps the element being animated and runs the animator on each frame.
284pub struct AnimationElement<E> {
285    id: ElementId,
286    element: Option<E>,
287    animations: SmallVec<[Animation; 1]>,
288    animator: Box<dyn Fn(E, usize, f32) -> E + 'static>,
289}
290
291/// The element returned by [`AnimationExt::with_spring`].
292///
293/// It wraps the element being animated and renders it at the spring's current value.
294pub struct SpringAnimationElement<E> {
295    id: ElementId,
296    element: Option<E>,
297    motion: Motion<SpringDescription>,
298    target: f32,
299    initial: Option<f32>,
300    playback: SpringPlayback,
301    animator: Option<Box<dyn FnOnce(E, f32) -> E + 'static>>,
302}
303
304impl<E: ParentElement> ParentElement for SpringAnimationElement<E> {
305    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
306        let Some(element) = &mut self.element else {
307            return;
308        };
309
310        element.extend(elements);
311    }
312}
313
314impl<E> SpringAnimationElement<E> {
315    /// Applies `f` to the element being animated, and returns the animation with the result.
316    ///
317    /// The spring's animator still runs on the element that `f` returns.
318    pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> SpringAnimationElement<E> {
319        self.element = self.element.map(f);
320        self
321    }
322}
323
324impl<E: IntoElement + 'static> IntoElement for SpringAnimationElement<E> {
325    type Element = SpringAnimationElement<E>;
326
327    fn into_element(self) -> Self::Element {
328        self
329    }
330}
331
332impl<E: ParentElement> ParentElement for AnimationElement<E> {
333    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
334        let Some(element) = &mut self.element else {
335            return;
336        };
337
338        element.extend(elements);
339    }
340}
341
342impl<E> AnimationElement<E> {
343    /// Applies `f` to the element being animated, and returns the animation with the result.
344    ///
345    /// The animator still runs on the element that `f` returns.
346    ///
347    /// # Examples
348    ///
349    /// ```
350    /// use std::time::Duration;
351    ///
352    /// use gpui::{Animation, AnimationExt, div, prelude::*, px};
353    ///
354    /// let _fade = div()
355    ///     .child("Saved")
356    ///     .with_animation(
357    ///         "fade",
358    ///         Animation::new(Duration::from_millis(200)),
359    ///         |el, delta| el.opacity(delta),
360    ///     )
361    ///     .map_element(|el| el.w(px(120.)));
362    /// ```
363    pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement<E> {
364        self.element = self.element.map(f);
365        self
366    }
367}
368
369impl<E: IntoElement + 'static> IntoElement for AnimationElement<E> {
370    type Element = AnimationElement<E>;
371
372    fn into_element(self) -> Self::Element {
373        self
374    }
375}
376
377struct AnimationState {
378    start: Instant,
379    animation_ix: usize,
380    /// Whether a throttled re-render (see [`Animation::with_max_fps`]) is
381    /// already scheduled, so overlapping renders don't stack extra timers.
382    delayed_frame_pending: Rc<Cell<bool>>,
383}
384
385struct SpringElementState {
386    spring: SpringState,
387    target: f32,
388    config: SpringConfig,
389    initial: f32,
390    playback: SpringPlayback,
391    updated_at: Instant,
392}
393
394impl<E: IntoElement + 'static> Element for SpringAnimationElement<E> {
395    type RequestLayoutState = AnyElement;
396    type PrepaintState = ();
397
398    fn id(&self) -> Option<ElementId> {
399        Some(self.id.clone())
400    }
401
402    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
403        None
404    }
405
406    fn request_layout(
407        &mut self,
408        global_id: Option<&GlobalElementId>,
409        _inspector_id: Option<&InspectorElementId>,
410        window: &mut Window,
411        cx: &mut App,
412    ) -> (crate::LayoutId, Self::RequestLayoutState) {
413        window.with_element_state(global_id.unwrap(), |state, window| {
414            // Use the executor clock so spring progression is deterministic in
415            // tests and remains consistent with scheduled animation work.
416            let now = cx.background_executor().now();
417            let initial = self.initial.unwrap_or(self.target);
418            let mut state = state.unwrap_or_else(|| SpringElementState {
419                spring: SpringState {
420                    position: initial,
421                    velocity: 0.0,
422                },
423                target: self.target,
424                config: self.motion.config,
425                initial,
426                playback: self.playback,
427                updated_at: now,
428            });
429
430            let elapsed = now.duration_since(state.updated_at).as_secs_f32();
431            match state.playback {
432                SpringPlayback::Running => {
433                    state.spring = state.config.step(state.spring, state.target, elapsed);
434                }
435                SpringPlayback::Paused
436                | SpringPlayback::Stopped
437                | SpringPlayback::Completed
438                | SpringPlayback::Cancelled => {}
439            }
440
441            state.config = self.motion.config;
442            state.target = self.target;
443
444            let done = match self.playback {
445                SpringPlayback::Running => {
446                    if cx.reduce_motion() {
447                        state.spring = SpringState {
448                            position: state.target,
449                            velocity: 0.0,
450                        };
451                        true
452                    } else {
453                        let done = state.config.is_settled(
454                            state.spring,
455                            state.target,
456                            self.motion.epsilon,
457                        );
458                        if done {
459                            state.spring = SpringState {
460                                position: state.target,
461                                velocity: 0.0,
462                            };
463                        }
464                        done
465                    }
466                }
467                SpringPlayback::Paused => true,
468                SpringPlayback::Stopped => {
469                    state.spring.velocity = 0.0;
470                    true
471                }
472                SpringPlayback::Completed => {
473                    state.spring = SpringState {
474                        position: state.target,
475                        velocity: 0.0,
476                    };
477                    true
478                }
479                SpringPlayback::Cancelled => {
480                    state.spring = SpringState {
481                        position: state.initial,
482                        velocity: 0.0,
483                    };
484                    true
485                }
486            };
487            state.playback = self.playback;
488            state.updated_at = now;
489
490            let element = self.element.take().expect("should only be called once");
491            let animator = self.animator.take().expect("should only be called once");
492            let mut element = animator(element, state.spring.position).into_any_element();
493
494            if !done {
495                window.request_animation_frame();
496            }
497
498            ((element.request_layout(window, cx), element), state)
499        })
500    }
501
502    fn prepaint(
503        &mut self,
504        _id: Option<&GlobalElementId>,
505        _inspector_id: Option<&InspectorElementId>,
506        _bounds: crate::Bounds<crate::Pixels>,
507        element: &mut Self::RequestLayoutState,
508        window: &mut Window,
509        cx: &mut App,
510    ) -> Self::PrepaintState {
511        element.prepaint(window, cx);
512    }
513
514    fn paint(
515        &mut self,
516        _id: Option<&GlobalElementId>,
517        _inspector_id: Option<&InspectorElementId>,
518        _bounds: crate::Bounds<crate::Pixels>,
519        element: &mut Self::RequestLayoutState,
520        _: &mut Self::PrepaintState,
521        window: &mut Window,
522        cx: &mut App,
523    ) {
524        element.paint(window, cx);
525    }
526}
527
528impl<E: IntoElement + 'static> Element for AnimationElement<E> {
529    type RequestLayoutState = AnyElement;
530    type PrepaintState = ();
531
532    fn id(&self) -> Option<ElementId> {
533        Some(self.id.clone())
534    }
535
536    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
537        None
538    }
539
540    fn request_layout(
541        &mut self,
542        global_id: Option<&GlobalElementId>,
543        _inspector_id: Option<&InspectorElementId>,
544        window: &mut Window,
545        cx: &mut App,
546    ) -> (crate::LayoutId, Self::RequestLayoutState) {
547        window.with_element_state(global_id.unwrap(), |state, window| {
548            let now = cx.background_executor().now();
549            let mut state = state.unwrap_or_else(|| AnimationState {
550                start: now,
551                animation_ix: 0,
552                delayed_frame_pending: Rc::new(Cell::new(false)),
553            });
554            let (animation_ix, delta, done) = if cx.reduce_motion() {
555                let animation_ix = self.animations.len() - 1;
556                let delta = self.animations[animation_ix]
557                    .motion
558                    .resting_progress()
559                    .get();
560                (animation_ix, delta, true)
561            } else {
562                let animation_ix = state.animation_ix;
563                let animation = &self.animations[animation_ix];
564                let elapsed = if animation.synced {
565                    now - cx.synced_animation_epoch
566                } else {
567                    now - state.start
568                };
569                let sample = animation.motion.sample(elapsed);
570                let mut done = !sample.is_active;
571                if done && animation_ix < self.animations.len() - 1 {
572                    state.start = now;
573                    state.animation_ix += 1;
574                    done = false;
575                }
576                (animation_ix, sample.progress.get(), done)
577            };
578
579            debug_assert!(delta.is_finite(), "animated value should be finite");
580
581            let element = self.element.take().expect("should only be called once");
582            let mut element = (self.animator)(element, animation_ix, delta).into_any_element();
583
584            if !done {
585                match self.animations[animation_ix].max_fps {
586                    Some(max_fps) if max_fps.is_finite() && max_fps > 0.0 => {
587                        if !state.delayed_frame_pending.get() {
588                            state.delayed_frame_pending.set(true);
589                            let delayed_frame_pending = state.delayed_frame_pending.clone();
590                            let view = window.current_view();
591                            let interval = Duration::from_secs_f32(1.0 / max_fps);
592                            window
593                                .spawn(cx, async move |cx| {
594                                    cx.background_executor().timer(interval).await;
595                                    delayed_frame_pending.set(false);
596                                    cx.update(move |_, cx| cx.notify(view)).ok();
597                                })
598                                .detach();
599                        }
600                    }
601                    _ => window.request_animation_frame(),
602                }
603            }
604
605            ((element.request_layout(window, cx), element), state)
606        })
607    }
608
609    fn prepaint(
610        &mut self,
611        _id: Option<&GlobalElementId>,
612        _inspector_id: Option<&InspectorElementId>,
613        _bounds: crate::Bounds<crate::Pixels>,
614        element: &mut Self::RequestLayoutState,
615        window: &mut Window,
616        cx: &mut App,
617    ) -> Self::PrepaintState {
618        element.prepaint(window, cx);
619    }
620
621    fn paint(
622        &mut self,
623        _id: Option<&GlobalElementId>,
624        _inspector_id: Option<&InspectorElementId>,
625        _bounds: crate::Bounds<crate::Pixels>,
626        element: &mut Self::RequestLayoutState,
627        _: &mut Self::PrepaintState,
628        window: &mut Window,
629        cx: &mut App,
630    ) {
631        element.paint(window, cx);
632    }
633}
634
635mod easing {
636    use std::f32::consts::PI;
637
638    /// Returns `delta` unchanged. This is the default easing.
639    ///
640    /// # Examples
641    ///
642    /// ```
643    /// use gpui::linear;
644    ///
645    /// assert_eq!(linear(0.25), 0.25);
646    /// ```
647    pub fn linear(delta: f32) -> f32 {
648        delta
649    }
650
651    /// Accelerates from rest. Returns `delta * delta`.
652    ///
653    /// # Examples
654    ///
655    /// ```
656    /// use gpui::quadratic;
657    ///
658    /// assert_eq!(quadratic(0.5), 0.25);
659    /// ```
660    pub fn quadratic(delta: f32) -> f32 {
661        delta * delta
662    }
663
664    /// Starts and ends slowly, and moves fastest at the midpoint.
665    ///
666    /// # Examples
667    ///
668    /// ```
669    /// use gpui::ease_in_out;
670    ///
671    /// assert_eq!(ease_in_out(0.25), 0.125);
672    /// assert_eq!(ease_in_out(0.5), 0.5);
673    /// assert_eq!(ease_in_out(0.75), 0.875);
674    /// ```
675    pub fn ease_in_out(delta: f32) -> f32 {
676        if delta < 0.5 {
677            2.0 * delta * delta
678        } else {
679            let x = -2.0 * delta + 2.0;
680            1.0 - x * x / 2.0
681        }
682    }
683
684    /// Starts quickly and decelerates to a stop.
685    ///
686    /// This returns the easing as a closure, so call it as `ease_out_quint()`.
687    ///
688    /// # Examples
689    ///
690    /// ```
691    /// use std::time::Duration;
692    ///
693    /// use gpui::{Animation, ease_out_quint};
694    ///
695    /// let easing = ease_out_quint();
696    /// assert_eq!(easing(0.5), 0.96875);
697    /// assert_eq!(easing(1.0), 1.0);
698    ///
699    /// let animation = Animation::new(Duration::from_millis(300)).with_easing(ease_out_quint());
700    /// assert!(!animation.synced);
701    /// ```
702    pub fn ease_out_quint() -> impl Fn(f32) -> f32 {
703        move |delta| 1.0 - (1.0 - delta).powi(5)
704    }
705
706    /// Runs `easing` forward over the first half, then backward over the second half.
707    ///
708    /// The motion returns to its start value. The result is a closure, so call it as `bounce(...)`.
709    ///
710    /// # Examples
711    ///
712    /// ```
713    /// use gpui::{bounce, linear, quadratic};
714    ///
715    /// let easing = bounce(linear);
716    /// assert_eq!(easing(0.0), 0.0);
717    /// assert_eq!(easing(0.5), 1.0);
718    /// assert_eq!(easing(1.0), 0.0);
719    ///
720    /// assert_eq!(bounce(quadratic)(0.25), 0.25);
721    /// ```
722    pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 {
723        move |delta| {
724            if delta < 0.5 {
725                easing(delta * 2.0)
726            } else {
727                easing((1.0 - delta) * 2.0)
728            }
729        }
730    }
731
732    /// Breathes between `min` and `max`, for idle indicators.
733    ///
734    /// Over one cycle the value is at `max` near a quarter of the way through, at `min` near three
735    /// quarters, and at the midpoint at the start, the middle and the end. Use it with
736    /// [`Animation::repeat_synced`](crate::Animation::repeat_synced) so that several indicators pulse together. The result is a
737    /// closure, so call it as `pulsating_between(min, max)`.
738    ///
739    /// # Examples
740    ///
741    /// ```
742    /// use gpui::pulsating_between;
743    ///
744    /// let breathe = pulsating_between(0.3, 1.0);
745    /// assert!((breathe(0.25) - 1.0).abs() < 1e-6);
746    /// assert!((breathe(0.75) - 0.3).abs() < 1e-6);
747    /// assert!((breathe(0.0) - 0.65).abs() < 1e-6);
748    /// ```
749    pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 {
750        let range = max - min;
751
752        move |delta| {
753            // Use a combination of sine and cubic functions for a more natural breathing rhythm
754            let t = (delta * 2.0 * PI).sin();
755            let breath = (t * t * t + t) / 2.0;
756
757            // Map the breath to our desired alpha range
758            let normalized_alpha = (breath + 1.0) / 2.0;
759
760            min + (normalized_alpha * range)
761        }
762    }
763}
764
765#[cfg(test)]
766mod tests {
767    use std::{cell::RefCell, rc::Rc, time::Duration};
768
769    use crate::{
770        Animation, Context, InteractiveElement, Pixels, Render, SpringAnimation, SpringConfig,
771        TestAppContext, WindowHandle, div, prelude::*, px, size,
772    };
773
774    use super::*;
775
776    struct AnimationTestView {
777        rendered_deltas: Rc<RefCell<Vec<f32>>>,
778        max_fps: Option<f32>,
779    }
780
781    struct AnimationSequenceTestView {
782        rendered_samples: Rc<RefCell<Vec<(usize, f32)>>>,
783    }
784
785    struct SyncedAnimationTestView {
786        show_second: bool,
787        first_deltas: Rc<RefCell<Vec<f32>>>,
788        second_deltas: Rc<RefCell<Vec<f32>>>,
789    }
790
791    struct SpringAnimationTestView {
792        target: Pixels,
793        initial: Option<Pixels>,
794        playback: SpringPlayback,
795        rendered_values: Rc<RefCell<Vec<Pixels>>>,
796    }
797
798    impl Render for SpringAnimationTestView {
799        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
800            let rendered_values = self.rendered_values.clone();
801            let mut animation = Motion::spring(SpringConfig::new(100.0, 2.0, 1.0))
802                .with_epsilon(0.01)
803                .to(self.target)
804                .playback(self.playback);
805            if let Some(initial) = self.initial {
806                animation = animation.from(initial);
807            }
808            div().with_spring("spring-animation", animation, move |this, value| {
809                rendered_values.borrow_mut().push(value);
810                this.left(value)
811            })
812        }
813    }
814
815    impl Render for SyncedAnimationTestView {
816        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
817            let record_deltas = |deltas: Rc<RefCell<Vec<f32>>>| {
818                move |this, delta| {
819                    deltas.borrow_mut().push(delta);
820                    this
821                }
822            };
823            div()
824                .size_full()
825                .child(div().with_animation(
826                    "first-synced-animation",
827                    Animation::new(Duration::from_secs(1)).repeat_synced(),
828                    record_deltas(self.first_deltas.clone()),
829                ))
830                .when(self.show_second, |this| {
831                    this.child(div().with_animation(
832                        "second-synced-animation",
833                        Animation::new(Duration::from_secs(1)).repeat_synced(),
834                        record_deltas(self.second_deltas.clone()),
835                    ))
836                })
837        }
838    }
839
840    impl Render for AnimationTestView {
841        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
842            let rendered_deltas = self.rendered_deltas.clone();
843            // The throttled variant syncs to the shared clock so the deltas
844            // follow the test scheduler's clock rather than wall time.
845            let mut animation = Animation::new(Motion::new(Duration::from_secs(1)));
846            if let Some(max_fps) = self.max_fps {
847                animation = animation.repeat_synced().with_max_fps(max_fps);
848            } else {
849                animation = animation.repeat();
850            }
851            div().size_full().child(div().with_animation(
852                "repeating-animation",
853                animation,
854                move |this, delta| {
855                    rendered_deltas.borrow_mut().push(delta);
856                    this
857                },
858            ))
859        }
860    }
861
862    impl Render for AnimationSequenceTestView {
863        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
864            let rendered_samples = self.rendered_samples.clone();
865            div().with_animations(
866                "animation-sequence",
867                vec![
868                    Animation::new(Duration::ZERO),
869                    Animation::new(Duration::ZERO),
870                ],
871                move |this, animation_ix, delta| {
872                    rendered_samples.borrow_mut().push((animation_ix, delta));
873                    this
874                },
875            )
876        }
877    }
878
879    fn open_test_window(
880        cx: &mut TestAppContext,
881    ) -> (Rc<RefCell<Vec<f32>>>, WindowHandle<AnimationTestView>) {
882        open_test_window_with_max_fps(cx, None)
883    }
884
885    fn open_test_window_with_max_fps(
886        cx: &mut TestAppContext,
887        max_fps: Option<f32>,
888    ) -> (Rc<RefCell<Vec<f32>>>, WindowHandle<AnimationTestView>) {
889        let rendered_deltas = Rc::new(RefCell::new(Vec::new()));
890        let window = cx.open_window(size(px(100.), px(100.)), {
891            let rendered_deltas = rendered_deltas.clone();
892            move |_, _| AnimationTestView {
893                rendered_deltas,
894                max_fps,
895            }
896        });
897        cx.run_until_parked();
898        (rendered_deltas, window)
899    }
900
901    fn simulate_next_frame<V: Render>(window: &WindowHandle<V>, cx: &mut TestAppContext) -> usize {
902        let callback_count = window
903            .update(cx, |_, window, cx| window.simulate_next_frame(cx))
904            .unwrap();
905        cx.run_until_parked();
906        callback_count
907    }
908
909    #[test]
910    fn test_animation_wrappers_accept_children() {
911        div()
912            .id("id")
913            .with_animation(
914                "animation",
915                Animation::new(Duration::from_secs(1)),
916                |element, _progress| element,
917            )
918            .child(div());
919
920        div()
921            .id("id")
922            .with_spring(
923                "spring-animation",
924                SpringAnimation::new(SpringConfig::new(100.0, 10.0, 1.0))
925                    .to(px(10.0))
926                    .from(px(0.0)),
927                |element, value| element.left(value),
928            )
929            .child(div());
930    }
931
932    #[gpui::test]
933    fn test_spring_animation_preserves_velocity_when_retargeted(cx: &mut TestAppContext) {
934        let rendered_values = Rc::new(RefCell::new(Vec::new()));
935        let window = cx.open_window(size(px(100.0), px(100.0)), {
936            let rendered_values = rendered_values.clone();
937            move |_, _| SpringAnimationTestView {
938                target: px(0.0),
939                initial: None,
940                playback: SpringPlayback::Running,
941                rendered_values,
942            }
943        });
944        cx.run_until_parked();
945        assert_eq!(*rendered_values.borrow(), vec![px(0.0)]);
946
947        window
948            .update(cx, |view, _, cx| {
949                view.target = px(100.0);
950                cx.notify();
951            })
952            .unwrap();
953        cx.run_until_parked();
954
955        cx.executor().advance_clock(Duration::from_millis(50));
956        assert!(simulate_next_frame(&window, cx) > 0);
957        // Delivering the frame callback only notifies the view. Explicitly
958        // flush that invalidation so randomized test-scheduler ordering cannot
959        // leave the assertion observing the pre-frame value.
960        cx.refresh().unwrap();
961        cx.run_until_parked();
962        let value_before_retargeting = *rendered_values.borrow().last().unwrap();
963        assert!(value_before_retargeting > px(0.0));
964        assert!(value_before_retargeting < px(100.0));
965
966        window
967            .update(cx, |view, _, cx| {
968                view.target = px(0.0);
969                cx.notify();
970            })
971            .unwrap();
972        cx.run_until_parked();
973
974        cx.executor().advance_clock(Duration::from_millis(5));
975        assert!(simulate_next_frame(&window, cx) > 0);
976        cx.refresh().unwrap();
977        cx.run_until_parked();
978        let value_after_retargeting = *rendered_values.borrow().last().unwrap();
979        assert!(value_after_retargeting > value_before_retargeting);
980    }
981
982    #[gpui::test]
983    fn test_paused_spring_resumes_with_its_velocity(cx: &mut TestAppContext) {
984        let rendered_values = Rc::new(RefCell::new(Vec::new()));
985        let window = cx.open_window(size(px(100.0), px(100.0)), {
986            let rendered_values = rendered_values.clone();
987            move |_, _| SpringAnimationTestView {
988                target: px(0.0),
989                initial: None,
990                playback: SpringPlayback::Running,
991                rendered_values,
992            }
993        });
994        cx.run_until_parked();
995
996        window
997            .update(cx, |view, _, cx| {
998                view.target = px(100.0);
999                cx.notify();
1000            })
1001            .unwrap();
1002        cx.run_until_parked();
1003        cx.executor().advance_clock(Duration::from_millis(50));
1004        assert!(simulate_next_frame(&window, cx) > 0);
1005
1006        window
1007            .update(cx, |view, _, cx| {
1008                view.target = px(0.0);
1009                view.playback = SpringPlayback::Paused;
1010                cx.notify();
1011            })
1012            .unwrap();
1013        cx.run_until_parked();
1014        let paused_value = *rendered_values.borrow().last().unwrap();
1015
1016        cx.executor().advance_clock(Duration::from_millis(500));
1017        assert!(simulate_next_frame(&window, cx) > 0);
1018        assert_eq!(*rendered_values.borrow().last().unwrap(), paused_value);
1019        assert_eq!(simulate_next_frame(&window, cx), 0);
1020
1021        window
1022            .update(cx, |view, _, cx| {
1023                view.playback = SpringPlayback::Running;
1024                cx.notify();
1025            })
1026            .unwrap();
1027        cx.run_until_parked();
1028        cx.executor().advance_clock(Duration::from_millis(5));
1029        assert!(simulate_next_frame(&window, cx) > 0);
1030        assert!(*rendered_values.borrow().last().unwrap() > paused_value);
1031    }
1032
1033    #[gpui::test]
1034    fn test_stopped_spring_resumes_without_velocity(cx: &mut TestAppContext) {
1035        let rendered_values = Rc::new(RefCell::new(Vec::new()));
1036        let window = cx.open_window(size(px(100.0), px(100.0)), {
1037            let rendered_values = rendered_values.clone();
1038            move |_, _| SpringAnimationTestView {
1039                target: px(0.0),
1040                initial: None,
1041                playback: SpringPlayback::Running,
1042                rendered_values,
1043            }
1044        });
1045        cx.run_until_parked();
1046
1047        window
1048            .update(cx, |view, _, cx| {
1049                view.target = px(1_000_000.0);
1050                cx.notify();
1051            })
1052            .unwrap();
1053        cx.run_until_parked();
1054        cx.executor().advance_clock(Duration::from_millis(50));
1055        assert!(simulate_next_frame(&window, cx) > 0);
1056
1057        window
1058            .update(cx, |view, _, cx| {
1059                view.target = px(0.0);
1060                view.playback = SpringPlayback::Stopped;
1061                cx.notify();
1062            })
1063            .unwrap();
1064        cx.run_until_parked();
1065        let stopped_value = *rendered_values.borrow().last().unwrap();
1066
1067        cx.executor().advance_clock(Duration::from_millis(500));
1068        assert!(simulate_next_frame(&window, cx) > 0);
1069        assert_eq!(*rendered_values.borrow().last().unwrap(), stopped_value);
1070        assert_eq!(simulate_next_frame(&window, cx), 0);
1071
1072        window
1073            .update(cx, |view, _, cx| {
1074                view.target = stopped_value;
1075                view.playback = SpringPlayback::Running;
1076                cx.notify();
1077            })
1078            .unwrap();
1079        cx.run_until_parked();
1080        assert_eq!(*rendered_values.borrow().last().unwrap(), stopped_value);
1081        assert_eq!(simulate_next_frame(&window, cx), 0);
1082    }
1083
1084    #[gpui::test]
1085    fn test_cancelled_and_completed_springs_resolve_their_endpoints(cx: &mut TestAppContext) {
1086        let rendered_values = Rc::new(RefCell::new(Vec::new()));
1087        let window = cx.open_window(size(px(100.0), px(100.0)), {
1088            let rendered_values = rendered_values.clone();
1089            move |_, _| SpringAnimationTestView {
1090                target: px(100.0),
1091                initial: Some(px(20.0)),
1092                playback: SpringPlayback::Running,
1093                rendered_values,
1094            }
1095        });
1096        cx.run_until_parked();
1097        assert_eq!(*rendered_values.borrow(), vec![px(20.0)]);
1098
1099        cx.executor().advance_clock(Duration::from_millis(50));
1100        assert!(simulate_next_frame(&window, cx) > 0);
1101        assert!(*rendered_values.borrow().last().unwrap() > px(20.0));
1102
1103        window
1104            .update(cx, |view, _, cx| {
1105                view.playback = SpringPlayback::Cancelled;
1106                cx.notify();
1107            })
1108            .unwrap();
1109        cx.run_until_parked();
1110        assert_eq!(*rendered_values.borrow().last().unwrap(), px(20.0));
1111        assert!(simulate_next_frame(&window, cx) > 0);
1112        assert_eq!(simulate_next_frame(&window, cx), 0);
1113
1114        window
1115            .update(cx, |view, _, cx| {
1116                view.playback = SpringPlayback::Completed;
1117                cx.notify();
1118            })
1119            .unwrap();
1120        cx.run_until_parked();
1121        assert_eq!(*rendered_values.borrow().last().unwrap(), px(100.0));
1122        assert_eq!(simulate_next_frame(&window, cx), 0);
1123    }
1124
1125    #[gpui::test]
1126    fn test_animations_respect_reduced_motion(cx: &mut TestAppContext) {
1127        cx.update(|cx| cx.set_reduce_motion(true));
1128        let rendered_values = Rc::new(RefCell::new(Vec::new()));
1129        let window = cx.open_window(size(px(100.0), px(100.0)), {
1130            let rendered_values = rendered_values.clone();
1131            move |_, _| SpringAnimationTestView {
1132                target: px(100.0),
1133                initial: None,
1134                playback: SpringPlayback::Running,
1135                rendered_values,
1136            }
1137        });
1138        cx.run_until_parked();
1139
1140        assert_eq!(*rendered_values.borrow(), vec![px(100.0)]);
1141        assert_eq!(simulate_next_frame(&window, cx), 0);
1142
1143        let (rendered_deltas, window) = open_test_window(cx);
1144        assert_eq!(*rendered_deltas.borrow(), vec![0.0]);
1145        assert_eq!(simulate_next_frame(&window, cx), 0);
1146    }
1147
1148    #[gpui::test]
1149    fn test_zero_duration_animation_sequence_advances_without_nan(cx: &mut TestAppContext) {
1150        let rendered_samples = Rc::new(RefCell::new(Vec::new()));
1151        let window = cx.open_window(size(px(100.0), px(100.0)), {
1152            let rendered_samples = rendered_samples.clone();
1153            move |_, _| AnimationSequenceTestView { rendered_samples }
1154        });
1155        cx.run_until_parked();
1156
1157        assert_eq!(*rendered_samples.borrow(), vec![(0, 1.0)]);
1158        assert_eq!(simulate_next_frame(&window, cx), 1);
1159        assert_eq!(*rendered_samples.borrow(), vec![(0, 1.0), (1, 1.0)]);
1160        assert_eq!(simulate_next_frame(&window, cx), 0);
1161    }
1162
1163    #[gpui::test]
1164    fn test_repeating_animation_schedules_animation_frames(cx: &mut TestAppContext) {
1165        let (rendered_deltas, window) = open_test_window(cx);
1166
1167        assert_eq!(rendered_deltas.borrow().len(), 1);
1168
1169        for expected_frames in 2..=3 {
1170            assert_eq!(simulate_next_frame(&window, cx), 1);
1171            assert_eq!(rendered_deltas.borrow().len(), expected_frames);
1172        }
1173    }
1174
1175    #[gpui::test]
1176    fn test_max_fps_schedules_timer_driven_frames(cx: &mut TestAppContext) {
1177        let (rendered_deltas, window) = open_test_window_with_max_fps(cx, Some(10.0));
1178
1179        // The test scheduler's clock jitters forward slightly on each poll,
1180        // so compare against expectations loosely.
1181        let assert_deltas_approx_eq = |expected: &[f32]| {
1182            let actual = rendered_deltas.borrow();
1183            assert_eq!(actual.len(), expected.len(), "deltas: {actual:?}");
1184            for (actual, expected) in actual.iter().zip(expected) {
1185                assert!(
1186                    (actual - expected).abs() < 1e-2,
1187                    "expected {expected}, got {actual}"
1188                );
1189            }
1190        };
1191
1192        assert_deltas_approx_eq(&[0.0]);
1193
1194        // No per-frame callback is scheduled; re-renders are timer-driven.
1195        assert_eq!(simulate_next_frame(&window, cx), 0);
1196        assert_deltas_approx_eq(&[0.0]);
1197
1198        cx.executor().advance_clock(Duration::from_millis(105));
1199        cx.run_until_parked();
1200        assert_deltas_approx_eq(&[0.0, 0.105]);
1201
1202        cx.executor().advance_clock(Duration::from_millis(105));
1203        cx.run_until_parked();
1204        assert_deltas_approx_eq(&[0.0, 0.105, 0.21]);
1205    }
1206
1207    #[gpui::test]
1208    fn test_synced_animations_share_phase_across_elements(cx: &mut TestAppContext) {
1209        let first_deltas = Rc::new(RefCell::new(Vec::new()));
1210        let second_deltas = Rc::new(RefCell::new(Vec::new()));
1211        let window = cx.open_window(size(px(100.), px(100.)), {
1212            let first_deltas = first_deltas.clone();
1213            let second_deltas = second_deltas.clone();
1214            move |_, _| SyncedAnimationTestView {
1215                show_second: false,
1216                first_deltas,
1217                second_deltas,
1218            }
1219        });
1220        cx.run_until_parked();
1221
1222        assert_eq!(*first_deltas.borrow(), vec![0.0]);
1223
1224        cx.executor().advance_clock(Duration::from_millis(250));
1225        simulate_next_frame(&window, cx);
1226        assert_eq!(*first_deltas.borrow(), vec![0.0, 0.25]);
1227
1228        // The second element mounts a quarter through the cycle, yet renders
1229        // the shared phase rather than starting at zero.
1230        window
1231            .update(cx, |view, _, cx| {
1232                view.show_second = true;
1233                cx.notify();
1234            })
1235            .unwrap();
1236        cx.run_until_parked();
1237        cx.executor().advance_clock(Duration::from_millis(250));
1238        simulate_next_frame(&window, cx);
1239
1240        assert_eq!(*second_deltas.borrow().last().unwrap(), 0.5);
1241        assert_eq!(
1242            *first_deltas.borrow().last().unwrap(),
1243            *second_deltas.borrow().last().unwrap()
1244        );
1245        assert!(second_deltas.borrow().iter().all(|delta| *delta > 0.0));
1246
1247        // The phase wraps around each full cycle.
1248        cx.executor().advance_clock(Duration::from_millis(2250));
1249        simulate_next_frame(&window, cx);
1250        assert_eq!(*first_deltas.borrow().last().unwrap(), 0.75);
1251
1252        // Sub-second precision survives months of uptime: converting the raw
1253        // elapsed time to f32 would round 0.25 away entirely.
1254        cx.executor()
1255            .advance_clock(Duration::from_secs(300 * 24 * 60 * 60) + Duration::from_millis(500));
1256        simulate_next_frame(&window, cx);
1257        assert_eq!(*first_deltas.borrow().last().unwrap(), 0.25);
1258    }
1259}