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#[derive(Clone)]
15pub struct Animation {
16 pub duration: Duration,
18 pub oneshot: bool,
20 pub synced: bool,
22 pub easing: Rc<dyn Fn(f32) -> f32>,
25 pub max_fps: Option<f32>,
28}
29
30impl Animation {
31 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 pub fn repeat(mut self) -> Self {
45 self.oneshot = false;
46 self
47 }
48
49 pub fn repeat_synced(mut self) -> Self {
51 self.oneshot = false;
52 self.synced = true;
53 self
54 }
55
56 pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
61 self.easing = Rc::new(easing);
62 self
63 }
64
65 pub fn with_max_fps(mut self, max_fps: f32) -> Self {
70 self.max_fps = Some(max_fps);
71 self
72 }
73}
74
75pub trait AnimationExt {
83 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 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 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
160pub 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
168pub 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 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 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 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 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 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 pub fn linear(delta: f32) -> f32 {
511 delta
512 }
513
514 pub fn quadratic(delta: f32) -> f32 {
516 delta * delta
517 }
518
519 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 pub fn ease_out_quint() -> impl Fn(f32) -> f32 {
531 move |delta| 1.0 - (1.0 - delta).powi(5)
532 }
533
534 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 pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 {
547 let range = max - min;
548
549 move |delta| {
550 let t = (delta * 2.0 * PI).sin();
552 let breath = (t * t * t + t) / 2.0;
553
554 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 #[test]
704 fn test_animation_parent() {
705 div()
706 .id("id")
707 .with_animation(
709 "animation",
710 Animation::new(Duration::from_secs(1)),
711 |el, _t| {
712 el
714 },
715 )
716 .child(
717 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 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 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 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 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 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 assert!(simulate_next_frame(&window, cx) > 0);
1053 assert!(last_delta() < 1e-2, "delta: {}", last_delta());
1054
1055 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}