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 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 let expected = SpringConfig::new(100.0, 2.0, 1.0).step(
747 SpringState {
748 position: 0.0,
749 velocity: 0.0,
750 },
751 100.0,
752 0.05,
753 );
754 assert!((value_before_retargeting.0 - expected.position).abs() < 0.001);
755
756 window
757 .update(cx, |view, _, cx| {
758 view.target = px(0.0);
759 cx.notify();
760 })
761 .unwrap();
762 cx.run_until_parked();
763
764 cx.executor().advance_clock(Duration::from_millis(5));
765 assert!(simulate_next_frame(&window, cx) > 0);
766 let value_after_retargeting = *rendered_values.borrow().last().unwrap();
767 assert!(value_after_retargeting > value_before_retargeting);
768 }
769
770 #[gpui::test]
771 fn test_paused_spring_resumes_with_its_velocity(cx: &mut TestAppContext) {
772 let rendered_values = Rc::new(RefCell::new(Vec::new()));
773 let window = cx.open_window(size(px(100.0), px(100.0)), {
774 let rendered_values = rendered_values.clone();
775 move |_, _| SpringAnimationTestView {
776 target: px(0.0),
777 initial: None,
778 playback: SpringPlayback::Running,
779 rendered_values,
780 }
781 });
782 cx.run_until_parked();
783
784 window
785 .update(cx, |view, _, cx| {
786 view.target = px(100.0);
787 cx.notify();
788 })
789 .unwrap();
790 cx.run_until_parked();
791 cx.executor().advance_clock(Duration::from_millis(50));
792 assert!(simulate_next_frame(&window, cx) > 0);
793
794 window
795 .update(cx, |view, _, cx| {
796 view.target = px(0.0);
797 view.playback = SpringPlayback::Paused;
798 cx.notify();
799 })
800 .unwrap();
801 cx.run_until_parked();
802 let paused_value = *rendered_values.borrow().last().unwrap();
803
804 cx.executor().advance_clock(Duration::from_millis(500));
805 assert!(simulate_next_frame(&window, cx) > 0);
806 assert_eq!(*rendered_values.borrow().last().unwrap(), paused_value);
807 assert_eq!(simulate_next_frame(&window, cx), 0);
808
809 window
810 .update(cx, |view, _, cx| {
811 view.playback = SpringPlayback::Running;
812 cx.notify();
813 })
814 .unwrap();
815 cx.run_until_parked();
816 cx.executor().advance_clock(Duration::from_millis(5));
817 assert!(simulate_next_frame(&window, cx) > 0);
818 assert!(*rendered_values.borrow().last().unwrap() > paused_value);
819 }
820
821 #[gpui::test]
822 fn test_stopped_spring_resumes_without_velocity(cx: &mut TestAppContext) {
823 let rendered_values = Rc::new(RefCell::new(Vec::new()));
824 let window = cx.open_window(size(px(100.0), px(100.0)), {
825 let rendered_values = rendered_values.clone();
826 move |_, _| SpringAnimationTestView {
827 target: px(0.0),
828 initial: None,
829 playback: SpringPlayback::Running,
830 rendered_values,
831 }
832 });
833 cx.run_until_parked();
834
835 window
836 .update(cx, |view, _, cx| {
837 view.target = px(1_000_000.0);
838 cx.notify();
839 })
840 .unwrap();
841 cx.run_until_parked();
842 cx.executor().advance_clock(Duration::from_millis(50));
843 assert!(simulate_next_frame(&window, cx) > 0);
844
845 window
846 .update(cx, |view, _, cx| {
847 view.target = px(0.0);
848 view.playback = SpringPlayback::Stopped;
849 cx.notify();
850 })
851 .unwrap();
852 cx.run_until_parked();
853 let stopped_value = *rendered_values.borrow().last().unwrap();
854
855 cx.executor().advance_clock(Duration::from_millis(500));
856 assert!(simulate_next_frame(&window, cx) > 0);
857 assert_eq!(*rendered_values.borrow().last().unwrap(), stopped_value);
858 assert_eq!(simulate_next_frame(&window, cx), 0);
859
860 window
861 .update(cx, |view, _, cx| {
862 view.target = stopped_value;
863 view.playback = SpringPlayback::Running;
864 cx.notify();
865 })
866 .unwrap();
867 cx.run_until_parked();
868 assert_eq!(*rendered_values.borrow().last().unwrap(), stopped_value);
869 assert_eq!(simulate_next_frame(&window, cx), 0);
870 }
871
872 #[gpui::test]
873 fn test_cancelled_and_completed_springs_resolve_their_endpoints(cx: &mut TestAppContext) {
874 let rendered_values = Rc::new(RefCell::new(Vec::new()));
875 let window = cx.open_window(size(px(100.0), px(100.0)), {
876 let rendered_values = rendered_values.clone();
877 move |_, _| SpringAnimationTestView {
878 target: px(100.0),
879 initial: Some(px(20.0)),
880 playback: SpringPlayback::Running,
881 rendered_values,
882 }
883 });
884 cx.run_until_parked();
885 assert_eq!(*rendered_values.borrow(), vec![px(20.0)]);
886
887 cx.executor().advance_clock(Duration::from_millis(50));
888 assert!(simulate_next_frame(&window, cx) > 0);
889 assert!(*rendered_values.borrow().last().unwrap() > px(20.0));
890
891 window
892 .update(cx, |view, _, cx| {
893 view.playback = SpringPlayback::Cancelled;
894 cx.notify();
895 })
896 .unwrap();
897 cx.run_until_parked();
898 assert_eq!(*rendered_values.borrow().last().unwrap(), px(20.0));
899 assert!(simulate_next_frame(&window, cx) > 0);
900 assert_eq!(simulate_next_frame(&window, cx), 0);
901
902 window
903 .update(cx, |view, _, cx| {
904 view.playback = SpringPlayback::Completed;
905 cx.notify();
906 })
907 .unwrap();
908 cx.run_until_parked();
909 assert_eq!(*rendered_values.borrow().last().unwrap(), px(100.0));
910 assert_eq!(simulate_next_frame(&window, cx), 0);
911 }
912
913 #[gpui::test]
914 fn test_spring_animation_respects_reduced_motion(cx: &mut TestAppContext) {
915 cx.update(|cx| cx.set_reduce_motion(true));
916 let rendered_values = Rc::new(RefCell::new(Vec::new()));
917 let window = cx.open_window(size(px(100.0), px(100.0)), {
918 let rendered_values = rendered_values.clone();
919 move |_, _| SpringAnimationTestView {
920 target: px(100.0),
921 initial: None,
922 playback: SpringPlayback::Running,
923 rendered_values,
924 }
925 });
926 cx.run_until_parked();
927
928 assert_eq!(*rendered_values.borrow(), vec![px(100.0)]);
929 assert_eq!(simulate_next_frame(&window, cx), 0);
930 }
931
932 #[gpui::test]
933 fn test_repeating_animation_schedules_animation_frames(cx: &mut TestAppContext) {
934 let (rendered_deltas, window) = open_test_window(cx);
935
936 assert_eq!(rendered_deltas.borrow().len(), 1);
937
938 for expected_frames in 2..=3 {
939 assert_eq!(simulate_next_frame(&window, cx), 1);
940 assert_eq!(rendered_deltas.borrow().len(), expected_frames);
941 }
942 }
943
944 #[gpui::test]
945 fn test_max_fps_schedules_timer_driven_frames(cx: &mut TestAppContext) {
946 let (rendered_deltas, window) = open_test_window_with_max_fps(cx, Some(10.0));
947
948 let assert_deltas_approx_eq = |expected: &[f32]| {
951 let actual = rendered_deltas.borrow();
952 assert_eq!(actual.len(), expected.len(), "deltas: {actual:?}");
953 for (actual, expected) in actual.iter().zip(expected) {
954 assert!(
955 (actual - expected).abs() < 1e-2,
956 "expected {expected}, got {actual}"
957 );
958 }
959 };
960
961 assert_deltas_approx_eq(&[0.0]);
962
963 assert_eq!(simulate_next_frame(&window, cx), 0);
965 assert_deltas_approx_eq(&[0.0]);
966
967 cx.executor().advance_clock(Duration::from_millis(105));
968 cx.run_until_parked();
969 assert_deltas_approx_eq(&[0.0, 0.105]);
970
971 cx.executor().advance_clock(Duration::from_millis(105));
972 cx.run_until_parked();
973 assert_deltas_approx_eq(&[0.0, 0.105, 0.21]);
974 }
975
976 #[gpui::test]
977 fn test_synced_animations_share_phase_across_elements(cx: &mut TestAppContext) {
978 let first_deltas = Rc::new(RefCell::new(Vec::new()));
979 let second_deltas = Rc::new(RefCell::new(Vec::new()));
980 let window = cx.open_window(size(px(100.), px(100.)), {
981 let first_deltas = first_deltas.clone();
982 let second_deltas = second_deltas.clone();
983 move |_, _| SyncedAnimationTestView {
984 show_second: false,
985 first_deltas,
986 second_deltas,
987 }
988 });
989 cx.run_until_parked();
990
991 assert_eq!(*first_deltas.borrow(), vec![0.0]);
992
993 cx.executor().advance_clock(Duration::from_millis(250));
994 simulate_next_frame(&window, cx);
995 assert_eq!(*first_deltas.borrow(), vec![0.0, 0.25]);
996
997 window
1000 .update(cx, |view, _, cx| {
1001 view.show_second = true;
1002 cx.notify();
1003 })
1004 .unwrap();
1005 cx.run_until_parked();
1006 cx.executor().advance_clock(Duration::from_millis(250));
1007 simulate_next_frame(&window, cx);
1008
1009 assert_eq!(*second_deltas.borrow().last().unwrap(), 0.5);
1010 assert_eq!(
1011 *first_deltas.borrow().last().unwrap(),
1012 *second_deltas.borrow().last().unwrap()
1013 );
1014 assert!(second_deltas.borrow().iter().all(|delta| *delta > 0.0));
1015
1016 cx.executor().advance_clock(Duration::from_millis(2250));
1018 simulate_next_frame(&window, cx);
1019 assert_eq!(*first_deltas.borrow().last().unwrap(), 0.75);
1020
1021 cx.executor()
1024 .advance_clock(Duration::from_secs(300 * 24 * 60 * 60) + Duration::from_millis(500));
1025 simulate_next_frame(&window, cx);
1026 assert_eq!(*first_deltas.borrow().last().unwrap(), 0.25);
1027 }
1028
1029 #[gpui::test]
1030 fn test_reduce_motion_renders_single_static_frame(cx: &mut TestAppContext) {
1031 cx.update(|cx| cx.set_reduce_motion(true));
1032 let (rendered_deltas, window) = open_test_window(cx);
1033
1034 assert_eq!(*rendered_deltas.borrow(), vec![0.0]);
1035
1036 assert_eq!(simulate_next_frame(&window, cx), 0);
1037 assert_eq!(*rendered_deltas.borrow(), vec![0.0]);
1038 }
1039}