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 = Instant::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 mut state = state.unwrap_or_else(|| AnimationState {
403 start: Instant::now(),
404 animation_ix: 0,
405 delayed_frame_pending: Rc::new(Cell::new(false)),
406 });
407 let (animation_ix, delta, done) = if cx.reduce_motion() {
408 let animation_ix = self.animations.len() - 1;
409 let delta = if self.animations[animation_ix].oneshot {
410 1.0
411 } else {
412 0.0
413 };
414 (animation_ix, delta, true)
415 } else {
416 let animation_ix = state.animation_ix;
417 let duration = self.animations[animation_ix].duration;
418
419 let elapsed = if self.animations[animation_ix].synced && !duration.is_zero() {
420 let elapsed = cx.background_executor().now() - cx.synced_animation_epoch;
421 Duration::from_nanos((elapsed.as_nanos() % duration.as_nanos()) as u64)
423 } else {
424 state.start.elapsed()
425 };
426 let mut delta = elapsed.as_secs_f32() / duration.as_secs_f32();
427
428 let mut done = false;
429 if delta > 1.0 {
430 if self.animations[animation_ix].oneshot {
431 if animation_ix >= self.animations.len() - 1 {
432 done = true;
433 } else {
434 state.start = Instant::now();
435 state.animation_ix += 1;
436 }
437 delta = 1.0;
438 } else {
439 delta %= 1.0;
440 }
441 }
442 (animation_ix, delta, done)
443 };
444 let delta = (self.animations[animation_ix].easing)(delta);
445
446 debug_assert!(delta.is_finite(), "animated value should be finite");
447
448 let element = self.element.take().expect("should only be called once");
449 let mut element = (self.animator)(element, animation_ix, delta).into_any_element();
450
451 if !done {
452 match self.animations[animation_ix].max_fps {
453 Some(max_fps) if max_fps.is_finite() && max_fps > 0.0 => {
454 if !state.delayed_frame_pending.get() {
455 state.delayed_frame_pending.set(true);
456 let delayed_frame_pending = state.delayed_frame_pending.clone();
457 let view = window.current_view();
458 let interval = Duration::from_secs_f32(1.0 / max_fps);
459 window
460 .spawn(cx, async move |cx| {
461 cx.background_executor().timer(interval).await;
462 delayed_frame_pending.set(false);
463 cx.update(move |_, cx| cx.notify(view)).ok();
464 })
465 .detach();
466 }
467 }
468 _ => window.request_animation_frame(),
469 }
470 }
471
472 ((element.request_layout(window, cx), element), state)
473 })
474 }
475
476 fn prepaint(
477 &mut self,
478 _id: Option<&GlobalElementId>,
479 _inspector_id: Option<&InspectorElementId>,
480 _bounds: crate::Bounds<crate::Pixels>,
481 element: &mut Self::RequestLayoutState,
482 window: &mut Window,
483 cx: &mut App,
484 ) -> Self::PrepaintState {
485 element.prepaint(window, cx);
486 }
487
488 fn paint(
489 &mut self,
490 _id: Option<&GlobalElementId>,
491 _inspector_id: Option<&InspectorElementId>,
492 _bounds: crate::Bounds<crate::Pixels>,
493 element: &mut Self::RequestLayoutState,
494 _: &mut Self::PrepaintState,
495 window: &mut Window,
496 cx: &mut App,
497 ) {
498 element.paint(window, cx);
499 }
500}
501
502mod easing {
503 use std::f32::consts::PI;
504
505 pub fn linear(delta: f32) -> f32 {
507 delta
508 }
509
510 pub fn quadratic(delta: f32) -> f32 {
512 delta * delta
513 }
514
515 pub fn ease_in_out(delta: f32) -> f32 {
517 if delta < 0.5 {
518 2.0 * delta * delta
519 } else {
520 let x = -2.0 * delta + 2.0;
521 1.0 - x * x / 2.0
522 }
523 }
524
525 pub fn ease_out_quint() -> impl Fn(f32) -> f32 {
527 move |delta| 1.0 - (1.0 - delta).powi(5)
528 }
529
530 pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 {
532 move |delta| {
533 if delta < 0.5 {
534 easing(delta * 2.0)
535 } else {
536 easing((1.0 - delta) * 2.0)
537 }
538 }
539 }
540
541 pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 {
543 let range = max - min;
544
545 move |delta| {
546 let t = (delta * 2.0 * PI).sin();
548 let breath = (t * t * t + t) / 2.0;
549
550 let normalized_alpha = (breath + 1.0) / 2.0;
552
553 min + (normalized_alpha * range)
554 }
555 }
556}
557
558#[cfg(test)]
559mod tests {
560 use std::{cell::RefCell, rc::Rc, time::Duration};
561
562 use crate::{
563 Animation, Context, InteractiveElement, Pixels, Render, SpringAnimation, SpringConfig,
564 TestAppContext, WindowHandle, div, prelude::*, px, size,
565 };
566
567 use super::*;
568
569 struct AnimationTestView {
570 rendered_deltas: Rc<RefCell<Vec<f32>>>,
571 max_fps: Option<f32>,
572 }
573
574 struct SyncedAnimationTestView {
575 show_second: bool,
576 first_deltas: Rc<RefCell<Vec<f32>>>,
577 second_deltas: Rc<RefCell<Vec<f32>>>,
578 }
579
580 struct SpringAnimationTestView {
581 target: Pixels,
582 initial: Option<Pixels>,
583 playback: SpringPlayback,
584 rendered_values: Rc<RefCell<Vec<Pixels>>>,
585 }
586
587 impl Render for SpringAnimationTestView {
588 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
589 let rendered_values = self.rendered_values.clone();
590 let mut animation = SpringAnimation::new(SpringConfig::new(100.0, 2.0, 1.0))
591 .to(self.target)
592 .with_epsilon(0.01)
593 .playback(self.playback);
594 if let Some(initial) = self.initial {
595 animation = animation.from(initial);
596 }
597 div().with_spring("spring-animation", animation, move |this, value| {
598 rendered_values.borrow_mut().push(value);
599 this.left(value)
600 })
601 }
602 }
603
604 impl Render for SyncedAnimationTestView {
605 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
606 let record_deltas = |deltas: Rc<RefCell<Vec<f32>>>| {
607 move |this, delta| {
608 deltas.borrow_mut().push(delta);
609 this
610 }
611 };
612 div()
613 .size_full()
614 .child(div().with_animation(
615 "first-synced-animation",
616 Animation::new(Duration::from_secs(1)).repeat_synced(),
617 record_deltas(self.first_deltas.clone()),
618 ))
619 .when(self.show_second, |this| {
620 this.child(div().with_animation(
621 "second-synced-animation",
622 Animation::new(Duration::from_secs(1)).repeat_synced(),
623 record_deltas(self.second_deltas.clone()),
624 ))
625 })
626 }
627 }
628
629 impl Render for AnimationTestView {
630 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
631 let rendered_deltas = self.rendered_deltas.clone();
632 let mut animation = Animation::new(Duration::from_secs(1));
635 if let Some(max_fps) = self.max_fps {
636 animation = animation.repeat_synced().with_max_fps(max_fps);
637 } else {
638 animation = animation.repeat();
639 }
640 div().size_full().child(div().with_animation(
641 "repeating-animation",
642 animation,
643 move |this, delta| {
644 rendered_deltas.borrow_mut().push(delta);
645 this
646 },
647 ))
648 }
649 }
650
651 fn open_test_window(
652 cx: &mut TestAppContext,
653 ) -> (Rc<RefCell<Vec<f32>>>, WindowHandle<AnimationTestView>) {
654 open_test_window_with_max_fps(cx, None)
655 }
656
657 fn open_test_window_with_max_fps(
658 cx: &mut TestAppContext,
659 max_fps: Option<f32>,
660 ) -> (Rc<RefCell<Vec<f32>>>, WindowHandle<AnimationTestView>) {
661 let rendered_deltas = Rc::new(RefCell::new(Vec::new()));
662 let window = cx.open_window(size(px(100.), px(100.)), {
663 let rendered_deltas = rendered_deltas.clone();
664 move |_, _| AnimationTestView {
665 rendered_deltas,
666 max_fps,
667 }
668 });
669 cx.run_until_parked();
670 (rendered_deltas, window)
671 }
672
673 fn simulate_next_frame<V: Render>(window: &WindowHandle<V>, cx: &mut TestAppContext) -> usize {
674 let callback_count = window
675 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
676 .unwrap();
677 cx.run_until_parked();
678 callback_count
679 }
680 #[test]
684 fn test_animation_parent() {
685 div()
686 .id("id")
687 .with_animation(
689 "animation",
690 Animation::new(Duration::from_secs(1)),
691 |el, _t| {
692 el
694 },
695 )
696 .child(
697 div(),
699 );
700 }
701
702 #[test]
703 fn test_spring_animation_parent() {
704 div()
705 .id("id")
706 .with_spring(
707 "spring-animation",
708 SpringAnimation::new(SpringConfig::new(100.0, 10.0, 1.0))
709 .to(px(10.0))
710 .from(px(0.0)),
711 |element, value| element.left(value),
712 )
713 .child(div());
714 }
715
716 #[gpui::test]
717 fn test_spring_animation_preserves_velocity_when_retargeted(cx: &mut TestAppContext) {
718 let rendered_values = Rc::new(RefCell::new(Vec::new()));
719 let window = cx.open_window(size(px(100.0), px(100.0)), {
720 let rendered_values = rendered_values.clone();
721 move |_, _| SpringAnimationTestView {
722 target: px(0.0),
723 initial: None,
724 playback: SpringPlayback::Running,
725 rendered_values,
726 }
727 });
728 cx.run_until_parked();
729 assert_eq!(*rendered_values.borrow(), vec![px(0.0)]);
730
731 window
732 .update(cx, |view, _, cx| {
733 view.target = px(100.0);
734 cx.notify();
735 })
736 .unwrap();
737 cx.run_until_parked();
738
739 cx.executor().advance_clock(Duration::from_millis(50));
740 assert!(simulate_next_frame(&window, cx) > 0);
741 let value_before_retargeting = *rendered_values.borrow().last().unwrap();
742 assert!(value_before_retargeting > px(0.0));
743 assert!(value_before_retargeting < px(100.0));
744
745 window
746 .update(cx, |view, _, cx| {
747 view.target = px(0.0);
748 cx.notify();
749 })
750 .unwrap();
751 cx.run_until_parked();
752
753 cx.executor().advance_clock(Duration::from_millis(5));
754 assert!(simulate_next_frame(&window, cx) > 0);
755 let value_after_retargeting = *rendered_values.borrow().last().unwrap();
756 assert!(value_after_retargeting > value_before_retargeting);
757 }
758
759 #[gpui::test]
760 fn test_paused_spring_resumes_with_its_velocity(cx: &mut TestAppContext) {
761 let rendered_values = Rc::new(RefCell::new(Vec::new()));
762 let window = cx.open_window(size(px(100.0), px(100.0)), {
763 let rendered_values = rendered_values.clone();
764 move |_, _| SpringAnimationTestView {
765 target: px(0.0),
766 initial: None,
767 playback: SpringPlayback::Running,
768 rendered_values,
769 }
770 });
771 cx.run_until_parked();
772
773 window
774 .update(cx, |view, _, cx| {
775 view.target = px(100.0);
776 cx.notify();
777 })
778 .unwrap();
779 cx.run_until_parked();
780 cx.executor().advance_clock(Duration::from_millis(50));
781 assert!(simulate_next_frame(&window, cx) > 0);
782
783 window
784 .update(cx, |view, _, cx| {
785 view.target = px(0.0);
786 view.playback = SpringPlayback::Paused;
787 cx.notify();
788 })
789 .unwrap();
790 cx.run_until_parked();
791 let paused_value = *rendered_values.borrow().last().unwrap();
792
793 cx.executor().advance_clock(Duration::from_millis(500));
794 assert!(simulate_next_frame(&window, cx) > 0);
795 assert_eq!(*rendered_values.borrow().last().unwrap(), paused_value);
796 assert_eq!(simulate_next_frame(&window, cx), 0);
797
798 window
799 .update(cx, |view, _, cx| {
800 view.playback = SpringPlayback::Running;
801 cx.notify();
802 })
803 .unwrap();
804 cx.run_until_parked();
805 cx.executor().advance_clock(Duration::from_millis(5));
806 assert!(simulate_next_frame(&window, cx) > 0);
807 assert!(*rendered_values.borrow().last().unwrap() > paused_value);
808 }
809
810 #[gpui::test]
811 fn test_stopped_spring_resumes_without_velocity(cx: &mut TestAppContext) {
812 let rendered_values = Rc::new(RefCell::new(Vec::new()));
813 let window = cx.open_window(size(px(100.0), px(100.0)), {
814 let rendered_values = rendered_values.clone();
815 move |_, _| SpringAnimationTestView {
816 target: px(0.0),
817 initial: None,
818 playback: SpringPlayback::Running,
819 rendered_values,
820 }
821 });
822 cx.run_until_parked();
823
824 window
825 .update(cx, |view, _, cx| {
826 view.target = px(1_000_000.0);
827 cx.notify();
828 })
829 .unwrap();
830 cx.run_until_parked();
831 cx.executor().advance_clock(Duration::from_millis(50));
832 assert!(simulate_next_frame(&window, cx) > 0);
833
834 window
835 .update(cx, |view, _, cx| {
836 view.target = px(0.0);
837 view.playback = SpringPlayback::Stopped;
838 cx.notify();
839 })
840 .unwrap();
841 cx.run_until_parked();
842 let stopped_value = *rendered_values.borrow().last().unwrap();
843
844 cx.executor().advance_clock(Duration::from_millis(500));
845 assert!(simulate_next_frame(&window, cx) > 0);
846 assert_eq!(*rendered_values.borrow().last().unwrap(), stopped_value);
847 assert_eq!(simulate_next_frame(&window, cx), 0);
848
849 window
850 .update(cx, |view, _, cx| {
851 view.target = stopped_value;
852 view.playback = SpringPlayback::Running;
853 cx.notify();
854 })
855 .unwrap();
856 cx.run_until_parked();
857 assert_eq!(*rendered_values.borrow().last().unwrap(), stopped_value);
858 assert_eq!(simulate_next_frame(&window, cx), 0);
859 }
860
861 #[gpui::test]
862 fn test_cancelled_and_completed_springs_resolve_their_endpoints(cx: &mut TestAppContext) {
863 let rendered_values = Rc::new(RefCell::new(Vec::new()));
864 let window = cx.open_window(size(px(100.0), px(100.0)), {
865 let rendered_values = rendered_values.clone();
866 move |_, _| SpringAnimationTestView {
867 target: px(100.0),
868 initial: Some(px(20.0)),
869 playback: SpringPlayback::Running,
870 rendered_values,
871 }
872 });
873 cx.run_until_parked();
874 assert_eq!(*rendered_values.borrow(), vec![px(20.0)]);
875
876 cx.executor().advance_clock(Duration::from_millis(50));
877 assert!(simulate_next_frame(&window, cx) > 0);
878 assert!(*rendered_values.borrow().last().unwrap() > px(20.0));
879
880 window
881 .update(cx, |view, _, cx| {
882 view.playback = SpringPlayback::Cancelled;
883 cx.notify();
884 })
885 .unwrap();
886 cx.run_until_parked();
887 assert_eq!(*rendered_values.borrow().last().unwrap(), px(20.0));
888 assert!(simulate_next_frame(&window, cx) > 0);
889 assert_eq!(simulate_next_frame(&window, cx), 0);
890
891 window
892 .update(cx, |view, _, cx| {
893 view.playback = SpringPlayback::Completed;
894 cx.notify();
895 })
896 .unwrap();
897 cx.run_until_parked();
898 assert_eq!(*rendered_values.borrow().last().unwrap(), px(100.0));
899 assert_eq!(simulate_next_frame(&window, cx), 0);
900 }
901
902 #[gpui::test]
903 fn test_spring_animation_respects_reduced_motion(cx: &mut TestAppContext) {
904 cx.update(|cx| cx.set_reduce_motion(true));
905 let rendered_values = Rc::new(RefCell::new(Vec::new()));
906 let window = cx.open_window(size(px(100.0), px(100.0)), {
907 let rendered_values = rendered_values.clone();
908 move |_, _| SpringAnimationTestView {
909 target: px(100.0),
910 initial: None,
911 playback: SpringPlayback::Running,
912 rendered_values,
913 }
914 });
915 cx.run_until_parked();
916
917 assert_eq!(*rendered_values.borrow(), vec![px(100.0)]);
918 assert_eq!(simulate_next_frame(&window, cx), 0);
919 }
920
921 #[gpui::test]
922 fn test_repeating_animation_schedules_animation_frames(cx: &mut TestAppContext) {
923 let (rendered_deltas, window) = open_test_window(cx);
924
925 assert_eq!(rendered_deltas.borrow().len(), 1);
926
927 for expected_frames in 2..=3 {
928 assert_eq!(simulate_next_frame(&window, cx), 1);
929 assert_eq!(rendered_deltas.borrow().len(), expected_frames);
930 }
931 }
932
933 #[gpui::test]
934 fn test_max_fps_schedules_timer_driven_frames(cx: &mut TestAppContext) {
935 let (rendered_deltas, window) = open_test_window_with_max_fps(cx, Some(10.0));
936
937 let assert_deltas_approx_eq = |expected: &[f32]| {
940 let actual = rendered_deltas.borrow();
941 assert_eq!(actual.len(), expected.len(), "deltas: {actual:?}");
942 for (actual, expected) in actual.iter().zip(expected) {
943 assert!(
944 (actual - expected).abs() < 1e-2,
945 "expected {expected}, got {actual}"
946 );
947 }
948 };
949
950 assert_deltas_approx_eq(&[0.0]);
951
952 assert_eq!(simulate_next_frame(&window, cx), 0);
954 assert_deltas_approx_eq(&[0.0]);
955
956 cx.executor().advance_clock(Duration::from_millis(105));
957 cx.run_until_parked();
958 assert_deltas_approx_eq(&[0.0, 0.105]);
959
960 cx.executor().advance_clock(Duration::from_millis(105));
961 cx.run_until_parked();
962 assert_deltas_approx_eq(&[0.0, 0.105, 0.21]);
963 }
964
965 #[gpui::test]
966 fn test_synced_animations_share_phase_across_elements(cx: &mut TestAppContext) {
967 let first_deltas = Rc::new(RefCell::new(Vec::new()));
968 let second_deltas = Rc::new(RefCell::new(Vec::new()));
969 let window = cx.open_window(size(px(100.), px(100.)), {
970 let first_deltas = first_deltas.clone();
971 let second_deltas = second_deltas.clone();
972 move |_, _| SyncedAnimationTestView {
973 show_second: false,
974 first_deltas,
975 second_deltas,
976 }
977 });
978 cx.run_until_parked();
979
980 assert_eq!(*first_deltas.borrow(), vec![0.0]);
981
982 cx.executor().advance_clock(Duration::from_millis(250));
983 simulate_next_frame(&window, cx);
984 assert_eq!(*first_deltas.borrow(), vec![0.0, 0.25]);
985
986 window
989 .update(cx, |view, _, cx| {
990 view.show_second = true;
991 cx.notify();
992 })
993 .unwrap();
994 cx.run_until_parked();
995 cx.executor().advance_clock(Duration::from_millis(250));
996 simulate_next_frame(&window, cx);
997
998 assert_eq!(*second_deltas.borrow().last().unwrap(), 0.5);
999 assert_eq!(
1000 *first_deltas.borrow().last().unwrap(),
1001 *second_deltas.borrow().last().unwrap()
1002 );
1003 assert!(second_deltas.borrow().iter().all(|delta| *delta > 0.0));
1004
1005 cx.executor().advance_clock(Duration::from_millis(2250));
1007 simulate_next_frame(&window, cx);
1008 assert_eq!(*first_deltas.borrow().last().unwrap(), 0.75);
1009
1010 cx.executor()
1013 .advance_clock(Duration::from_secs(300 * 24 * 60 * 60) + Duration::from_millis(500));
1014 simulate_next_frame(&window, cx);
1015 assert_eq!(*first_deltas.borrow().last().unwrap(), 0.25);
1016 }
1017
1018 #[gpui::test]
1019 fn test_reduce_motion_renders_single_static_frame(cx: &mut TestAppContext) {
1020 cx.update(|cx| cx.set_reduce_motion(true));
1021 let (rendered_deltas, window) = open_test_window(cx);
1022
1023 assert_eq!(*rendered_deltas.borrow(), vec![0.0]);
1024
1025 assert_eq!(simulate_next_frame(&window, cx), 0);
1026 assert_eq!(*rendered_deltas.borrow(), vec![0.0]);
1027 }
1028}