1#[cfg(not(target_family = "wasm"))]
2use std::time::Instant;
3use std::{rc::Rc, time::Duration};
4#[cfg(target_family = "wasm")]
5use web_time::Instant;
6
7use gpui::{
8 App, Bounds, ElementId, Pixels, SharedString, Size, SpringConfig, SpringState, SpringTarget,
9 Window,
10};
11
12use crate::animation::{Lerp, ease_out_cubic};
13
14mod easing;
15mod keyframes;
16mod presence;
17mod reveal;
18mod sequence;
19mod stagger;
20mod timing;
21
22pub use easing::{Easing, EasingError, LinearStop, StepPosition};
23pub use keyframes::{Discrete, DiscreteError, Keyframe, KeyframeError, Keyframes};
24pub use presence::{Presence, PresencePhase, PresenceSample};
25pub use reveal::MotionReveal;
26pub use sequence::{Sequence, SequenceSample, SequenceStep};
27pub use stagger::{Stagger, StaggerOrigin};
28pub use timing::{
29 IterationCount, MotionPhase, PlaybackDirection, SignedDuration, Timing, TimingSample,
30};
31
32const DEFAULT_SPRING_EPSILON: f32 = 0.001;
34
35pub trait Interpolate: Clone {
37 fn interpolate(&self, target: &Self, progress: f32) -> Self;
38}
39
40impl<T: Lerp> Interpolate for T {
41 fn interpolate(&self, target: &Self, progress: f32) -> Self {
42 self.lerp(target, progress)
43 }
44}
45
46impl Interpolate for Size<Pixels> {
47 fn interpolate(&self, target: &Self, progress: f32) -> Self {
48 Size::new(
49 self.width.lerp(&target.width, progress),
50 self.height.lerp(&target.height, progress),
51 )
52 }
53}
54
55impl Interpolate for Bounds<Pixels> {
56 fn interpolate(&self, target: &Self, progress: f32) -> Self {
57 Bounds::new(
58 self.origin.lerp(&target.origin, progress),
59 self.size.interpolate(&target.size, progress),
60 )
61 }
62}
63
64#[derive(Clone, Copy, Debug, PartialEq)]
66pub struct MotionTransform {
67 pub translation: gpui::Point<Pixels>,
68 pub scale: gpui::Point<f32>,
69 pub rotation_radians: f32,
70 pub opacity: f32,
71}
72
73impl MotionTransform {
74 pub fn identity() -> Self {
75 Self {
76 translation: gpui::point(gpui::px(0.0), gpui::px(0.0)),
77 scale: gpui::point(1.0, 1.0),
78 rotation_radians: 0.0,
79 opacity: 1.0,
80 }
81 }
82}
83
84impl Default for MotionTransform {
85 fn default() -> Self {
86 Self::identity()
87 }
88}
89
90impl Interpolate for MotionTransform {
91 fn interpolate(&self, target: &Self, progress: f32) -> Self {
92 Self {
93 translation: self.translation.lerp(&target.translation, progress),
94 scale: gpui::point(
95 self.scale.x.lerp(&target.scale.x, progress),
96 self.scale.y.lerp(&target.scale.y, progress),
97 ),
98 rotation_radians: self
99 .rotation_radians
100 .lerp(&target.rotation_radians, progress),
101 opacity: self.opacity.lerp(&target.opacity, progress),
102 }
103 }
104}
105
106#[derive(Clone)]
112pub struct Transition {
113 duration: Duration,
114 delay: SignedDuration,
115 easing: Easing,
116}
117
118impl Transition {
119 pub fn new(duration: Duration) -> Self {
120 Self {
121 duration,
122 delay: SignedDuration::ZERO,
123 easing: Easing::Custom(Rc::new(ease_out_cubic)),
124 }
125 }
126
127 pub fn delay(mut self, delay: impl Into<SignedDuration>) -> Self {
128 self.delay = delay.into();
129 self
130 }
131
132 pub fn ease(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
133 self.easing = Easing::Custom(Rc::new(easing));
134 self
135 }
136
137 pub fn easing(mut self, easing: Easing) -> Self {
138 self.easing = easing;
139 self
140 }
141
142 fn sample(&self, progress: f32) -> f32 {
143 self.easing.sample(progress)
144 }
145
146 pub(crate) fn curve(&self) -> &Easing {
149 &self.easing
150 }
151
152 fn progress(&self, elapsed: Duration, duration: Duration) -> (f32, MotionStatus) {
153 let Some(active_elapsed) = self.delay.active_elapsed(elapsed) else {
154 return (0.0, MotionStatus::Delayed);
155 };
156 if duration.is_zero() || active_elapsed >= duration {
157 return (1.0, MotionStatus::Finished);
158 }
159 (
160 active_elapsed.as_secs_f32() / duration.as_secs_f32(),
161 MotionStatus::Running,
162 )
163 }
164
165 fn finishes_after(&self) -> Duration {
168 match self.delay {
169 SignedDuration::Positive(delay) => delay.saturating_add(self.duration),
170 SignedDuration::Negative(delay) => self.duration.saturating_sub(delay),
171 }
172 }
173}
174
175impl From<Duration> for SignedDuration {
176 fn from(duration: Duration) -> Self {
177 Self::positive(duration)
178 }
179}
180
181#[derive(Clone, Debug, Eq, Hash, PartialEq)]
183pub struct TransitionId(ElementId);
184
185impl From<ElementId> for TransitionId {
186 fn from(id: ElementId) -> Self {
187 Self(id)
188 }
189}
190
191impl From<&'static str> for TransitionId {
192 fn from(id: &'static str) -> Self {
193 Self(id.into())
194 }
195}
196
197impl From<String> for TransitionId {
198 fn from(id: String) -> Self {
199 Self(id.into())
200 }
201}
202
203impl From<SharedString> for TransitionId {
204 fn from(id: SharedString) -> Self {
205 Self(id.into())
206 }
207}
208
209impl From<usize> for TransitionId {
210 fn from(id: usize) -> Self {
211 Self(id.into())
212 }
213}
214
215impl From<i32> for TransitionId {
216 fn from(id: i32) -> Self {
217 Self(id.into())
218 }
219}
220
221impl From<TransitionId> for ElementId {
222 fn from(id: TransitionId) -> Self {
223 ElementId::NamedChild(id.0.into(), "__base-transition-state".into())
224 }
225}
226
227impl<I, C> From<(I, C)> for TransitionId
228where
229 I: Into<ElementId>,
230 C: Into<SharedString>,
231{
232 fn from((id, channel): (I, C)) -> Self {
233 Self(ElementId::NamedChild(id.into().into(), channel.into()))
234 }
235}
236
237#[derive(Clone)]
238struct ValueTransition<T> {
239 from: T,
240 target: T,
241 started_at: Instant,
242 reversing_factor: f32,
243 duration: Duration,
244}
245
246#[derive(Clone, Copy, Debug, Eq, PartialEq)]
247pub enum MotionStatus {
248 Idle,
249 Delayed,
250 Running,
251 Finished,
252}
253
254#[derive(Clone, Copy, Debug, PartialEq)]
255pub struct MotionValue<T> {
256 pub value: T,
257 pub status: MotionStatus,
258}
259
260pub fn transition<T>(
269 id: impl Into<TransitionId>,
270 target: T,
271 policy: Transition,
272 window: &mut Window,
273 cx: &mut App,
274) -> T
275where
276 T: Interpolate + PartialEq + 'static,
277{
278 transition_with_status(id, target, policy, window, cx).value
279}
280
281pub fn transition_with_status<T>(
282 id: impl Into<TransitionId>,
283 target: T,
284 policy: Transition,
285 window: &mut Window,
286 cx: &mut App,
287) -> MotionValue<T>
288where
289 T: Interpolate + PartialEq + 'static,
290{
291 let id: ElementId = id.into().into();
292 let now = cx.background_executor().now();
293 let state = window.use_keyed_state(id, cx, |_, _| ValueTransition {
294 from: target.clone(),
295 target: target.clone(),
296 started_at: now,
297 reversing_factor: 1.0,
298 duration: policy.duration,
299 });
300
301 let snapshot = state.read(cx).clone();
302
303 if cx.reduce_motion() || policy.duration.is_zero() {
304 if snapshot.from != target || snapshot.target != target {
305 state.update(cx, |state, _| {
306 state.from = target.clone();
307 state.target = target.clone();
308 state.started_at = now;
309 state.reversing_factor = 1.0;
310 state.duration = policy.duration;
311 });
312 }
313 return MotionValue {
314 value: target,
315 status: MotionStatus::Finished,
316 };
317 }
318
319 let elapsed = now.saturating_duration_since(snapshot.started_at);
320 let (progress, status) = policy.progress(elapsed, snapshot.duration);
321 let sampled = snapshot
322 .from
323 .interpolate(&snapshot.target, policy.sample(progress));
324
325 let (value, status) = if snapshot.target != target {
326 let reversing = target == snapshot.from;
327 let reversing_factor = if reversing {
328 (policy.sample(progress) * snapshot.reversing_factor
329 + (1.0 - snapshot.reversing_factor))
330 .clamp(0.0, 1.0)
331 } else {
332 1.0
333 };
334 let duration = policy.duration.mul_f64(f64::from(reversing_factor));
335 state.update(cx, |state, _| {
336 state.from = sampled.clone();
337 state.target = target.clone();
338 state.started_at = now;
339 state.reversing_factor = reversing_factor;
340 state.duration = duration;
341 });
342 let (initial_progress, initial_status) = policy.progress(Duration::ZERO, duration);
343 (
344 sampled.interpolate(&target, policy.sample(initial_progress)),
345 initial_status,
346 )
347 } else {
348 (
349 sampled,
350 if snapshot.from == snapshot.target {
351 MotionStatus::Idle
352 } else {
353 status
354 },
355 )
356 };
357 if matches!(status, MotionStatus::Delayed | MotionStatus::Running) {
358 window.request_animation_frame();
359 }
360 MotionValue { value, status }
361}
362
363#[derive(Clone, Copy)]
364struct KeyframePlayback {
365 started_at: Instant,
366}
367
368pub fn animate_keyframes<T>(
375 id: impl Into<TransitionId>,
376 keyframes: &Keyframes<T>,
377 timing: Timing,
378 window: &mut Window,
379 cx: &mut App,
380) -> MotionValue<T>
381where
382 T: Interpolate + 'static,
383{
384 let id: TransitionId = id.into();
385 let id = ElementId::NamedChild(ElementId::from(id).into(), "__keyframes".into());
386 let now = cx.background_executor().now();
387 let state = window.use_keyed_state(id, cx, |_, _| KeyframePlayback { started_at: now });
388 let started_at = state.read(cx).started_at;
389
390 if cx.reduce_motion() {
391 return MotionValue {
392 value: keyframes.sample(1.0),
393 status: MotionStatus::Finished,
394 };
395 }
396
397 let sample = timing.sample(now.saturating_duration_since(started_at));
398 let status = match sample.phase {
399 MotionPhase::Before => MotionStatus::Delayed,
400 MotionPhase::Active => MotionStatus::Running,
401 MotionPhase::After => MotionStatus::Finished,
402 };
403 if matches!(status, MotionStatus::Delayed | MotionStatus::Running) {
404 window.request_animation_frame();
405 }
406 MotionValue {
407 value: keyframes.sample(sample.directed_progress),
408 status,
409 }
410}
411
412#[derive(Clone, Copy, Debug)]
421pub struct Spring {
422 response: Duration,
423 damping: f32,
424 epsilon: f32,
425 travel: bool,
426}
427
428#[derive(Clone, Copy, Debug, Eq, PartialEq)]
430pub enum SpringError {
431 InvalidDamping,
432 InvalidEpsilon,
433}
434
435impl std::fmt::Display for SpringError {
436 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
437 match self {
438 Self::InvalidDamping => f.write_str("spring damping must be finite and non-negative"),
439 Self::InvalidEpsilon => {
440 f.write_str("spring epsilon must be finite and greater than zero")
441 }
442 }
443 }
444}
445
446impl std::error::Error for SpringError {}
447
448impl Spring {
449 pub const fn new(response: Duration) -> Self {
464 Self {
465 response,
466 damping: 1.0,
467 epsilon: DEFAULT_SPRING_EPSILON,
468 travel: true,
469 }
470 }
471
472 pub const fn with_damping(self, ratio: f32) -> Self {
487 match self.try_with_damping(ratio) {
488 Ok(spring) => spring,
489 Err(_) => panic!("spring damping must be finite and non-negative"),
490 }
491 }
492
493 pub const fn try_with_damping(mut self, ratio: f32) -> Result<Self, SpringError> {
495 if !ratio.is_finite() || ratio < 0.0 {
496 return Err(SpringError::InvalidDamping);
497 }
498 self.damping = ratio;
499 Ok(self)
500 }
501
502 pub const fn with_travel(mut self, travel: bool) -> Self {
515 self.travel = travel;
516 self
517 }
518
519 pub const fn with_epsilon(self, epsilon: f32) -> Self {
531 match self.try_with_epsilon(epsilon) {
532 Ok(spring) => spring,
533 Err(_) => panic!("spring epsilon must be finite and greater than zero"),
534 }
535 }
536
537 pub const fn try_with_epsilon(mut self, epsilon: f32) -> Result<Self, SpringError> {
539 if !epsilon.is_finite() || epsilon <= 0.0 {
540 return Err(SpringError::InvalidEpsilon);
541 }
542 self.epsilon = epsilon;
543 Ok(self)
544 }
545
546 pub const fn epsilon(self) -> f32 {
548 self.epsilon
549 }
550
551 fn config(&self) -> SpringConfig {
558 let frequency = std::f32::consts::TAU / self.response.as_secs_f32();
559 SpringConfig::new(frequency * frequency, 2.0 * self.damping * frequency, 1.0)
560 }
561}
562
563#[derive(Clone, Copy)]
564struct SpringTransition {
565 state: SpringState,
566 target: f32,
567 updated_at: Instant,
568}
569
570pub fn spring<T>(
580 id: impl Into<TransitionId>,
581 target: T,
582 policy: Spring,
583 window: &mut Window,
584 cx: &mut App,
585) -> T::Output
586where
587 T: SpringTarget,
588{
589 let id: ElementId = id.into().into();
590 let now = cx.background_executor().now();
591 let target_position = target.target();
592 let state = window.use_keyed_state(id, cx, |_, _| SpringTransition {
593 state: SpringState {
594 position: target_position,
595 velocity: 0.0,
596 },
597 target: target_position,
598 updated_at: now,
599 });
600
601 let snapshot = *state.read(cx);
602 let at_rest_on_target =
603 snapshot.state.position == target_position && snapshot.state.velocity == 0.0;
604
605 if at_rest_on_target {
616 return target.resolve(target_position);
617 }
618
619 let settle = |state: &mut SpringTransition| {
620 state.state = SpringState {
621 position: target_position,
622 velocity: 0.0,
623 };
624 state.target = target_position;
625 state.updated_at = now;
626 };
627
628 if cx.reduce_motion() || !policy.travel || policy.response.is_zero() {
629 state.update(cx, |state, _| settle(state));
630 return target.resolve(target_position);
631 }
632
633 let elapsed = now
636 .saturating_duration_since(snapshot.updated_at)
637 .as_secs_f32();
638 let config = policy.config();
639 let stepped = config.step(snapshot.state, snapshot.target, elapsed);
640
641 if config.is_settled(stepped, target_position, policy.epsilon) {
642 state.update(cx, |state, _| settle(state));
643 return target.resolve(target_position);
644 }
645
646 state.update(cx, |state, _| {
647 state.state = stepped;
648 state.target = target_position;
649 state.updated_at = now;
650 });
651 window.request_animation_frame();
652 target.resolve(stepped.position)
653}
654
655#[cfg(test)]
656mod css_timing_tests {
657 use super::{
658 Easing, IterationCount, LinearStop, MotionPhase, PlaybackDirection, SignedDuration,
659 StepPosition, Timing,
660 };
661 use std::time::Duration;
662
663 #[test]
664 fn css_keyword_easing_matches_published_reference_samples() {
665 for (easing, samples) in [
666 (Easing::Ease, [(0.2, 0.295), (0.5, 0.802), (0.8, 0.976)]),
667 (Easing::EaseIn, [(0.2, 0.062), (0.5, 0.315), (0.8, 0.692)]),
668 (Easing::EaseOut, [(0.2, 0.308), (0.5, 0.685), (0.8, 0.938)]),
669 (Easing::EaseInOut, [(0.2, 0.082), (0.5, 0.5), (0.8, 0.918)]),
670 ] {
671 for (progress, expected) in samples {
672 let actual = easing.sample(progress);
673 assert!(
674 (actual - expected).abs() < 0.002,
675 "{easing:?}({progress}) = {actual}, expected {expected}"
676 );
677 }
678 }
679 }
680
681 #[test]
682 fn cubic_bezier_easing_keeps_anticipation_and_overshoot() {
683 let anticipate = Easing::cubic_bezier(1.0, -0.4, 0.35, 0.95).unwrap();
684 let overshoot = Easing::cubic_bezier(0.175, 0.885, 0.32, 1.275).unwrap();
685
686 assert!(anticipate.sample(0.2) < 0.0);
687 assert!(overshoot.sample(0.8) > 1.0);
688 for easing in [anticipate, overshoot] {
689 assert_eq!(easing.sample(-1.0), 0.0);
690 assert_eq!(easing.sample(2.0), 1.0);
691 }
692 }
693
694 #[test]
695 fn step_easing_observes_css_jump_positions() {
696 let start = Easing::steps(4, StepPosition::JumpStart).unwrap();
697 let end = Easing::steps(4, StepPosition::JumpEnd).unwrap();
698
699 assert_eq!(start.sample(0.0), 0.25);
700 assert_eq!(start.sample(0.24), 0.25);
701 assert_eq!(start.sample(0.25), 0.5);
702 assert_eq!(end.sample(0.0), 0.0);
703 assert_eq!(end.sample(0.24), 0.0);
704 assert_eq!(end.sample(0.25), 0.25);
705 assert!(Easing::steps(0, StepPosition::JumpEnd).is_err());
706
707 let none = Easing::steps(4, StepPosition::JumpNone).unwrap();
708 let both = Easing::steps(4, StepPosition::JumpBoth).unwrap();
709 assert_eq!(none.sample(0.0), 0.0);
710 assert!((none.sample(0.5) - 2.0 / 3.0).abs() < f32::EPSILON);
711 assert_eq!(none.sample(1.0), 1.0);
712 assert_eq!(both.sample(0.0), 0.2);
713 assert_eq!(both.sample(1.0), 1.0);
714 assert!(Easing::steps(1, StepPosition::JumpNone).is_err());
715 }
716
717 #[test]
718 fn linear_stops_fill_omitted_positions_before_sampling() {
719 let easing = Easing::linear_stops([
720 LinearStop::at(0.0, 0.0),
721 LinearStop::new(0.2),
722 LinearStop::new(0.8),
723 LinearStop::at(1.0, 1.0),
724 ])
725 .unwrap();
726
727 assert!((easing.sample(1.0 / 3.0) - 0.2).abs() < 1e-6);
728 assert!((easing.sample(0.5) - 0.5).abs() < 1e-6);
729 assert!(
730 Easing::linear_stops([LinearStop::at(0.0, 0.8), LinearStop::at(1.0, 0.2)]).is_err()
731 );
732 }
733
734 #[test]
735 fn negative_delay_starts_inside_the_active_interval() {
736 let timing = Timing::new(Duration::from_millis(100))
737 .delay(SignedDuration::negative(Duration::from_millis(25)));
738 let sample = timing.sample(Duration::ZERO);
739
740 assert_eq!(sample.phase, MotionPhase::Active);
741 assert!((sample.directed_progress - 0.25).abs() < f32::EPSILON);
742 assert!(sample.active);
743 assert!(!sample.finished);
744 }
745
746 #[test]
747 fn alternate_direction_reverses_odd_iterations() {
748 let timing = Timing::new(Duration::from_millis(100))
749 .iterations(IterationCount::Finite(2))
750 .direction(PlaybackDirection::Alternate)
751 .ease(Easing::Linear);
752
753 let first = timing.sample(Duration::from_millis(25));
754 let second = timing.sample(Duration::from_millis(125));
755 let finished = timing.sample(Duration::from_millis(200));
756
757 assert_eq!(first.iteration, 0);
758 assert_eq!(first.directed_progress, 0.25);
759 assert_eq!(second.iteration, 1);
760 assert_eq!(second.directed_progress, 0.75);
761 assert_eq!(finished.phase, MotionPhase::After);
762 assert_eq!(finished.directed_progress, 0.0);
763 assert!(finished.finished);
764 }
765}
766
767#[cfg(test)]
768mod motion_track_tests {
769 use super::{
770 Discrete, Easing, Interpolate as _, Keyframe, KeyframeError, Keyframes, MotionTransform,
771 Stagger, StaggerOrigin,
772 };
773 use gpui::{Bounds, Point, Size, point, px, size};
774 use std::time::Duration;
775
776 #[test]
777 fn keyframes_validate_offsets_and_sample_each_segments_easing() {
778 assert!(matches!(
779 Keyframes::try_new([Keyframe::new(0.2, 0.0_f32), Keyframe::new(1.0, 1.0_f32),]),
780 Err(KeyframeError::MissingEndpoint)
781 ));
782 assert!(matches!(
783 Keyframes::try_new([
784 Keyframe::new(0.0, 0.0_f32),
785 Keyframe::new(0.8, 1.0_f32),
786 Keyframe::new(0.7, 2.0_f32),
787 Keyframe::new(1.0, 3.0_f32),
788 ]),
789 Err(KeyframeError::OffsetsNotMonotonic)
790 ));
791
792 let track = Keyframes::try_new([
793 Keyframe::new(0.0, 0.0_f32)
794 .ease(Easing::steps(2, super::StepPosition::JumpEnd).unwrap()),
795 Keyframe::new(0.5, 10.0_f32).ease(Easing::Linear),
796 Keyframe::new(1.0, 20.0_f32),
797 ])
798 .unwrap();
799
800 assert_eq!(track.sample(0.2), 0.0);
801 assert_eq!(track.sample(0.3), 5.0);
802 assert_eq!(track.sample(0.75), 15.0);
803 assert_eq!(track.sample(1.0), 20.0);
804 }
805
806 #[test]
807 fn discrete_values_switch_only_at_the_requested_progress() {
808 let value = Discrete::new("old", "new").switch_at(0.75).unwrap();
809 assert_eq!(value.sample(0.749), "old");
810 assert_eq!(value.sample(0.75), "new");
811 assert!(Discrete::new(0, 1).switch_at(f32::NAN).is_err());
812 }
813
814 #[test]
815 fn stagger_origins_produce_stable_delays_without_allocating_a_schedule() {
816 let interval = Duration::from_millis(20);
817 let first = Stagger::new(interval, StaggerOrigin::First);
818 let last = Stagger::new(interval, StaggerOrigin::Last);
819 let center = Stagger::new(interval, StaggerOrigin::Center);
820
821 assert_eq!(first.delay(3, 5), Duration::from_millis(60));
822 assert_eq!(last.delay(3, 5), Duration::from_millis(20));
823 assert_eq!(center.delay(2, 5), Duration::ZERO);
824 assert_eq!(center.delay(0, 5), Duration::from_millis(40));
825 assert_eq!(first.delay(7, 0), Duration::ZERO);
826 }
827
828 #[test]
829 fn common_gpui_geometry_interpolates_channel_by_channel() {
830 let from_size = size(px(10.0), px(20.0));
831 let to_size = size(px(30.0), px(60.0));
832 assert_eq!(
833 from_size.interpolate(&to_size, 0.25),
834 size(px(15.0), px(30.0))
835 );
836
837 let from = Bounds::new(point(px(0.0), px(10.0)), from_size);
838 let to = Bounds::new(point(px(40.0), px(50.0)), to_size);
839 assert_eq!(
840 from.interpolate(&to, 0.5),
841 Bounds::new(point(px(20.0), px(30.0)), size(px(20.0), px(40.0)))
842 );
843
844 let _: Point<gpui::Pixels> = from.origin;
845 let _: Size<gpui::Pixels> = from.size;
846
847 let transform = MotionTransform::identity().interpolate(
848 &MotionTransform {
849 translation: point(px(20.0), px(40.0)),
850 scale: point(2.0, 0.5),
851 rotation_radians: std::f32::consts::PI,
852 opacity: 0.0,
853 },
854 0.5,
855 );
856 assert_eq!(transform.translation, point(px(10.0), px(20.0)));
857 assert_eq!(transform.scale, point(1.5, 0.75));
858 assert_eq!(transform.rotation_radians, std::f32::consts::FRAC_PI_2);
859 assert_eq!(transform.opacity, 0.5);
860 }
861}
862
863#[cfg(test)]
864mod tests {
865 use std::{
866 cell::{Cell, RefCell},
867 rc::Rc,
868 time::Duration,
869 };
870
871 use gpui::{Empty, IntoElement, Render, TestAppContext, WindowHandle, px, size};
872
873 use super::*;
874
875 struct StatusView {
876 target: Rc<Cell<f32>>,
877 policy: Transition,
878 samples: Rc<RefCell<Vec<MotionValue<f32>>>>,
879 }
880
881 impl Render for StatusView {
882 fn render(
883 &mut self,
884 window: &mut Window,
885 cx: &mut gpui::Context<Self>,
886 ) -> impl IntoElement {
887 self.samples.borrow_mut().push(transition_with_status(
888 ("status-test", "value"),
889 self.target.get(),
890 self.policy.clone(),
891 window,
892 cx,
893 ));
894 Empty
895 }
896 }
897
898 struct StatusFixture {
899 window: WindowHandle<StatusView>,
900 target: Rc<Cell<f32>>,
901 samples: Rc<RefCell<Vec<MotionValue<f32>>>>,
902 }
903
904 impl StatusFixture {
905 fn open(cx: &mut TestAppContext, policy: Transition) -> Self {
906 let target = Rc::new(Cell::new(0.0));
907 let samples = Rc::new(RefCell::new(Vec::new()));
908 let window = cx.open_window(size(px(100.), px(100.)), {
909 let target = target.clone();
910 let samples = samples.clone();
911 move |_, _| StatusView {
912 target,
913 policy,
914 samples,
915 }
916 });
917 cx.run_until_parked();
918 Self {
919 window,
920 target,
921 samples,
922 }
923 }
924
925 fn render(&self, cx: &mut TestAppContext, target: f32) -> MotionValue<f32> {
926 self.target.set(target);
927 self.window
928 .update(cx, |_, window, _| window.refresh())
929 .unwrap();
930 cx.run_until_parked();
931 *self.samples.borrow().last().unwrap()
932 }
933 }
934
935 #[gpui::test]
936 fn status_transition_reports_delay_running_and_finished(cx: &mut TestAppContext) {
937 let fixture = StatusFixture::open(
938 cx,
939 Transition::new(Duration::from_millis(100)).delay(Duration::from_millis(20)),
940 );
941 assert_eq!(fixture.render(cx, 1.0).status, MotionStatus::Delayed);
942
943 cx.executor().advance_clock(Duration::from_millis(20));
944 assert_eq!(fixture.render(cx, 1.0).status, MotionStatus::Running);
945 cx.executor().advance_clock(Duration::from_millis(100));
946 assert_eq!(fixture.render(cx, 1.0).status, MotionStatus::Finished);
947 }
948
949 #[gpui::test]
950 fn negative_delay_samples_a_target_change_inside_its_interval(cx: &mut TestAppContext) {
951 let fixture = StatusFixture::open(
952 cx,
953 Transition::new(Duration::from_millis(100))
954 .delay(SignedDuration::negative(Duration::from_millis(25)))
955 .ease(|t| t),
956 );
957 let sample = fixture.render(cx, 1.0);
958 assert_eq!(sample.status, MotionStatus::Running);
959 assert_eq!(sample.value, 0.25);
960 }
961
962 #[gpui::test]
963 fn a_direct_reversal_shortens_the_return_transition(cx: &mut TestAppContext) {
964 let fixture =
965 StatusFixture::open(cx, Transition::new(Duration::from_millis(100)).ease(|t| t));
966 assert_eq!(fixture.render(cx, 1.0).value, 0.0);
967 cx.executor().advance_clock(Duration::from_millis(50));
968 assert_eq!(fixture.render(cx, 0.0).value, 0.5);
969 cx.executor().advance_clock(Duration::from_millis(25));
970 assert_eq!(fixture.render(cx, 0.0).value, 0.25);
971 }
972
973 struct KeyframeView {
974 track: Keyframes<f32>,
975 timing: Timing,
976 samples: Rc<RefCell<Vec<MotionValue<f32>>>>,
977 }
978
979 impl Render for KeyframeView {
980 fn render(
981 &mut self,
982 window: &mut Window,
983 cx: &mut gpui::Context<Self>,
984 ) -> impl IntoElement {
985 self.samples.borrow_mut().push(animate_keyframes(
986 "keyframe-test",
987 &self.track,
988 self.timing.clone(),
989 window,
990 cx,
991 ));
992 Empty
993 }
994 }
995
996 #[gpui::test]
997 fn keyed_keyframes_follow_timing_and_stop_after_completion(cx: &mut TestAppContext) {
998 let samples = Rc::new(RefCell::new(Vec::new()));
999 let window = cx.open_window(size(px(100.), px(100.)), {
1000 let samples = samples.clone();
1001 move |_, _| KeyframeView {
1002 track: Keyframes::try_new([Keyframe::new(0.0, 0.0), Keyframe::new(1.0, 10.0)])
1003 .unwrap(),
1004 timing: Timing::new(Duration::from_millis(100)),
1005 samples,
1006 }
1007 });
1008 cx.run_until_parked();
1009 assert_eq!(samples.borrow().last().unwrap().value, 0.0);
1010 assert_eq!(
1011 samples.borrow().last().unwrap().status,
1012 MotionStatus::Running
1013 );
1014
1015 cx.executor().advance_clock(Duration::from_millis(50));
1016 assert_eq!(
1017 window
1018 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
1019 .unwrap(),
1020 1
1021 );
1022 cx.run_until_parked();
1023 assert_eq!(samples.borrow().last().unwrap().value, 5.0);
1024
1025 cx.executor().advance_clock(Duration::from_millis(50));
1026 window.update(cx, |_, window, _| window.refresh()).unwrap();
1027 cx.run_until_parked();
1028 assert_eq!(
1029 samples.borrow().last().unwrap().status,
1030 MotionStatus::Finished
1031 );
1032 window
1033 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
1034 .unwrap();
1035 cx.run_until_parked();
1036 assert_eq!(
1037 window
1038 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
1039 .unwrap(),
1040 0
1041 );
1042 }
1043
1044 struct PresenceView {
1045 present: Rc<Cell<bool>>,
1046 samples: Rc<RefCell<Vec<PresenceSample>>>,
1047 }
1048
1049 impl Render for PresenceView {
1050 fn render(
1051 &mut self,
1052 window: &mut Window,
1053 cx: &mut gpui::Context<Self>,
1054 ) -> impl IntoElement {
1055 self.samples.borrow_mut().push(
1056 Presence::new("presence-test", self.present.get())
1057 .transition(Transition::new(Duration::from_millis(100)).ease(|t| t))
1058 .sample(window, cx),
1059 );
1060 Empty
1061 }
1062 }
1063
1064 struct PresenceFixture {
1065 window: WindowHandle<PresenceView>,
1066 present: Rc<Cell<bool>>,
1067 samples: Rc<RefCell<Vec<PresenceSample>>>,
1068 }
1069
1070 impl PresenceFixture {
1071 fn open(cx: &mut TestAppContext, initially_present: bool) -> Self {
1072 let present = Rc::new(Cell::new(initially_present));
1073 let samples = Rc::new(RefCell::new(Vec::new()));
1074 let window = cx.open_window(size(px(100.), px(100.)), {
1075 let present = present.clone();
1076 let samples = samples.clone();
1077 move |_, _| PresenceView { present, samples }
1078 });
1079 cx.run_until_parked();
1080 Self {
1081 window,
1082 present,
1083 samples,
1084 }
1085 }
1086
1087 fn render(&self, cx: &mut TestAppContext, present: bool) -> PresenceSample {
1088 self.present.set(present);
1089 self.window
1090 .update(cx, |_, window, _| window.refresh())
1091 .unwrap();
1092 cx.run_until_parked();
1093 *self.samples.borrow().last().unwrap()
1094 }
1095 }
1096
1097 #[gpui::test]
1098 fn presence_enters_exits_and_only_unmounts_after_exit(cx: &mut TestAppContext) {
1099 let fixture = PresenceFixture::open(cx, true);
1100 let entering = *fixture.samples.borrow().last().unwrap();
1101 assert_eq!(entering.phase, PresencePhase::Entering);
1102 assert_eq!(entering.progress, 0.0);
1103 assert!(entering.should_render());
1104
1105 cx.executor().advance_clock(Duration::from_millis(100));
1106 let present = fixture.render(cx, true);
1107 assert_eq!(present.phase, PresencePhase::Present);
1108 assert_eq!(present.progress, 1.0);
1109
1110 let exiting = fixture.render(cx, false);
1111 assert_eq!(exiting.phase, PresencePhase::Exiting);
1112 assert_eq!(exiting.progress, 1.0);
1113 assert!(exiting.should_render());
1114
1115 cx.executor().advance_clock(Duration::from_millis(100));
1116 let absent = fixture.render(cx, false);
1117 assert_eq!(absent.phase, PresencePhase::Absent);
1118 assert_eq!(absent.progress, 0.0);
1119 assert!(!absent.should_render());
1120 }
1121
1122 #[gpui::test]
1123 fn presence_reentry_reverses_from_the_exit_sample(cx: &mut TestAppContext) {
1124 let fixture = PresenceFixture::open(cx, true);
1125 cx.executor().advance_clock(Duration::from_millis(100));
1126 fixture.render(cx, true);
1127 fixture.render(cx, false);
1128 cx.executor().advance_clock(Duration::from_millis(40));
1129 let reentering = fixture.render(cx, true);
1130
1131 assert_eq!(reentering.phase, PresencePhase::Entering);
1132 assert_eq!(reentering.progress, 0.6);
1133 }
1134
1135 #[gpui::test]
1136 fn reduced_motion_resolves_presence_without_a_pending_frame(cx: &mut TestAppContext) {
1137 cx.update(|cx| cx.set_reduce_motion(true));
1138 let fixture = PresenceFixture::open(cx, true);
1139 assert_eq!(
1140 fixture.samples.borrow().last().unwrap().phase,
1141 PresencePhase::Present
1142 );
1143 assert_eq!(
1144 fixture
1145 .window
1146 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
1147 .unwrap(),
1148 0
1149 );
1150 assert_eq!(fixture.render(cx, false).phase, PresencePhase::Absent);
1151 }
1152
1153 struct SequenceView {
1154 steps: Rc<RefCell<Vec<SequenceStep<f32>>>>,
1155 samples: Rc<RefCell<Vec<SequenceSample<f32>>>>,
1156 }
1157
1158 impl Render for SequenceView {
1159 fn render(
1160 &mut self,
1161 window: &mut Window,
1162 cx: &mut gpui::Context<Self>,
1163 ) -> impl IntoElement {
1164 self.samples.borrow_mut().push(
1165 Sequence::new("sequence-test", 0.0)
1166 .with_steps(self.steps.borrow().iter().cloned())
1167 .sample(window, cx),
1168 );
1169 Empty
1170 }
1171 }
1172
1173 struct SequenceFixture {
1174 window: WindowHandle<SequenceView>,
1175 steps: Rc<RefCell<Vec<SequenceStep<f32>>>>,
1176 samples: Rc<RefCell<Vec<SequenceSample<f32>>>>,
1177 }
1178
1179 impl SequenceFixture {
1180 fn open(cx: &mut TestAppContext, steps: Vec<SequenceStep<f32>>) -> Self {
1181 let steps = Rc::new(RefCell::new(steps));
1182 let samples = Rc::new(RefCell::new(Vec::new()));
1183 let window = cx.open_window(size(px(100.), px(100.)), {
1184 let steps = steps.clone();
1185 let samples = samples.clone();
1186 move |_, _| SequenceView { steps, samples }
1187 });
1188 cx.run_until_parked();
1189 Self {
1190 window,
1191 steps,
1192 samples,
1193 }
1194 }
1195
1196 fn linear(millis: u64) -> Transition {
1197 Transition::new(Duration::from_millis(millis)).ease(|t| t)
1198 }
1199
1200 fn last(&self) -> SequenceSample<f32> {
1201 *self.samples.borrow().last().unwrap()
1202 }
1203
1204 fn render(&self, cx: &mut TestAppContext) -> SequenceSample<f32> {
1205 self.window
1206 .update(cx, |_, window, _| window.refresh())
1207 .unwrap();
1208 cx.run_until_parked();
1209 self.last()
1210 }
1211
1212 fn advance(&self, cx: &mut TestAppContext, millis: u64) -> SequenceSample<f32> {
1213 cx.executor().advance_clock(Duration::from_millis(millis));
1214 self.render(cx)
1215 }
1216
1217 fn pending_frame(&self, cx: &mut TestAppContext) -> usize {
1218 self.window
1219 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
1220 .unwrap()
1221 }
1222 }
1223
1224 fn assert_sample(sample: SequenceSample<f32>, value: f32, step: usize, status: MotionStatus) {
1225 assert_eq!(
1226 (*sample.value(), sample.step(), sample.status()),
1227 (value, step, status)
1228 );
1229 }
1230
1231 #[gpui::test]
1232 fn sequence_steps_advance_in_order_at_their_boundaries(cx: &mut TestAppContext) {
1233 let fixture = SequenceFixture::open(
1234 cx,
1235 vec![
1236 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1237 SequenceStep::new(
1238 20.0,
1239 SequenceFixture::linear(100).delay(Duration::from_millis(50)),
1240 ),
1241 ],
1242 );
1243 assert_sample(fixture.last(), 0.0, 0, MotionStatus::Running);
1244 assert_sample(fixture.advance(cx, 50), 5.0, 0, MotionStatus::Running);
1245
1246 assert_sample(fixture.advance(cx, 50), 10.0, 1, MotionStatus::Delayed);
1249 assert_sample(fixture.advance(cx, 50), 10.0, 1, MotionStatus::Running);
1250 assert_sample(fixture.advance(cx, 50), 15.0, 1, MotionStatus::Running);
1251 assert_sample(fixture.advance(cx, 50), 20.0, 1, MotionStatus::Finished);
1252 }
1253
1254 #[gpui::test]
1255 fn a_sequence_starts_its_next_step_where_the_previous_ended_not_at_the_frame(
1256 cx: &mut TestAppContext,
1257 ) {
1258 let fixture = SequenceFixture::open(
1259 cx,
1260 vec![
1261 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1262 SequenceStep::new(20.0, SequenceFixture::linear(100)),
1263 ],
1264 );
1265 fixture.advance(cx, 50);
1268 assert_sample(fixture.advance(cx, 125), 17.5, 1, MotionStatus::Running);
1269 }
1270
1271 #[gpui::test]
1272 fn zero_duration_steps_complete_within_the_frame_that_reaches_them(cx: &mut TestAppContext) {
1273 let fixture = SequenceFixture::open(
1274 cx,
1275 vec![
1276 SequenceStep::new(5.0, SequenceFixture::linear(0)),
1277 SequenceStep::new(6.0, SequenceFixture::linear(0)),
1278 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1279 ],
1280 );
1281 assert_sample(fixture.last(), 6.0, 2, MotionStatus::Running);
1282 assert_sample(fixture.advance(cx, 50), 8.0, 2, MotionStatus::Running);
1283 }
1284
1285 #[gpui::test]
1286 fn a_sequence_reports_finished_once_and_then_stops_requesting_frames(cx: &mut TestAppContext) {
1287 let fixture = SequenceFixture::open(
1288 cx,
1289 vec![
1290 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1291 SequenceStep::new(20.0, SequenceFixture::linear(100)),
1292 ],
1293 );
1294 assert_eq!(fixture.pending_frame(cx), 1);
1295 cx.run_until_parked();
1296
1297 cx.executor().advance_clock(Duration::from_millis(100));
1298 assert_eq!(fixture.pending_frame(cx), 1);
1299 cx.run_until_parked();
1300 assert_eq!(fixture.last().status(), MotionStatus::Running);
1301 assert!(
1302 fixture
1303 .samples
1304 .borrow()
1305 .iter()
1306 .all(|sample| !sample.is_finished()),
1307 "the first step's end must not read as the sequence finishing"
1308 );
1309
1310 assert_sample(fixture.advance(cx, 100), 20.0, 1, MotionStatus::Finished);
1311 fixture.pending_frame(cx);
1312 cx.run_until_parked();
1313 assert_eq!(fixture.pending_frame(cx), 0);
1314 assert_sample(fixture.advance(cx, 1_000), 20.0, 1, MotionStatus::Finished);
1315 }
1316
1317 #[gpui::test]
1318 fn reduced_motion_adopts_a_sequences_last_target_without_requesting_a_frame(
1319 cx: &mut TestAppContext,
1320 ) {
1321 cx.update(|cx| cx.set_reduce_motion(true));
1322 let fixture = SequenceFixture::open(
1323 cx,
1324 vec![
1325 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1326 SequenceStep::new(20.0, SequenceFixture::linear(100)),
1327 ],
1328 );
1329 assert_sample(fixture.last(), 20.0, 1, MotionStatus::Finished);
1330 assert_eq!(fixture.pending_frame(cx), 0);
1331 }
1332
1333 #[gpui::test]
1334 fn a_changed_step_target_restarts_the_sequence_from_the_sampled_value(cx: &mut TestAppContext) {
1335 let fixture = SequenceFixture::open(
1336 cx,
1337 vec![
1338 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1339 SequenceStep::new(20.0, SequenceFixture::linear(100)),
1340 ],
1341 );
1342 fixture.advance(cx, 150);
1343 assert_sample(fixture.last(), 15.0, 1, MotionStatus::Running);
1344
1345 *fixture.steps.borrow_mut() = vec![
1346 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1347 SequenceStep::new(35.0, SequenceFixture::linear(100)),
1348 ];
1349 assert_sample(fixture.render(cx), 15.0, 0, MotionStatus::Running);
1350 assert_sample(fixture.advance(cx, 50), 12.5, 0, MotionStatus::Running);
1351 assert_sample(fixture.advance(cx, 100), 22.5, 1, MotionStatus::Running);
1352
1353 *fixture.steps.borrow_mut() = vec![SequenceStep::new(0.0, SequenceFixture::linear(100))];
1355 assert_sample(fixture.render(cx), 22.5, 0, MotionStatus::Running);
1356 assert_sample(fixture.advance(cx, 100), 0.0, 0, MotionStatus::Finished);
1357 }
1358
1359 #[gpui::test]
1360 fn a_change_to_a_step_not_being_played_does_not_restart_the_sequence(cx: &mut TestAppContext) {
1361 let fixture = SequenceFixture::open(
1362 cx,
1363 vec![
1364 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1365 SequenceStep::new(20.0, SequenceFixture::linear(100)),
1366 ],
1367 );
1368 fixture.advance(cx, 50);
1369 *fixture.steps.borrow_mut() = vec![
1370 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1371 SequenceStep::new(30.0, SequenceFixture::linear(100)),
1372 ];
1373 assert_sample(fixture.render(cx), 5.0, 0, MotionStatus::Running);
1374 assert_sample(fixture.advance(cx, 100), 20.0, 1, MotionStatus::Running);
1375 }
1376
1377 struct SingleStepView {
1378 armed: Rc<Cell<bool>>,
1379 samples: Rc<RefCell<Vec<(SequenceSample<f32>, MotionValue<f32>)>>>,
1380 }
1381
1382 impl Render for SingleStepView {
1383 fn render(
1384 &mut self,
1385 window: &mut Window,
1386 cx: &mut gpui::Context<Self>,
1387 ) -> impl IntoElement {
1388 let policy = Transition::new(Duration::from_millis(100))
1389 .delay(Duration::from_millis(20))
1390 .easing(Easing::EaseInOut);
1391 if !self.armed.get() {
1395 transition_with_status("plain", 0.0, policy, window, cx);
1396 return Empty;
1397 }
1398 let sequence = Sequence::new("single-step", 0.0)
1399 .with_step(10.0, policy.clone())
1400 .sample(window, cx);
1401 let plain = transition_with_status("plain", 10.0, policy, window, cx);
1402 self.samples.borrow_mut().push((sequence, plain));
1403 Empty
1404 }
1405 }
1406
1407 #[gpui::test]
1408 fn a_single_step_sequence_matches_a_plain_transition(cx: &mut TestAppContext) {
1409 let armed = Rc::new(Cell::new(false));
1410 let samples = Rc::new(RefCell::new(Vec::new()));
1411 let window = cx.open_window(size(px(100.), px(100.)), {
1412 let armed = armed.clone();
1413 let samples = samples.clone();
1414 move |_, _| SingleStepView { armed, samples }
1415 });
1416 cx.run_until_parked();
1417 armed.set(true);
1418 for millis in [0, 10, 30, 70, 120] {
1419 cx.executor().advance_clock(Duration::from_millis(millis));
1420 window.update(cx, |_, window, _| window.refresh()).unwrap();
1421 cx.run_until_parked();
1422 let (sequence, plain) = *samples.borrow().last().unwrap();
1423 assert_eq!(*sequence.value(), plain.value, "value after {millis} ms");
1424 assert_eq!(sequence.status(), plain.status, "status after {millis} ms");
1425 assert_eq!(sequence.step(), 0);
1426 }
1427 }
1428
1429 #[gpui::test]
1430 fn an_empty_sequence_idles_at_its_starting_value(cx: &mut TestAppContext) {
1431 let fixture = SequenceFixture::open(cx, Vec::new());
1432 assert_sample(fixture.last(), 0.0, 0, MotionStatus::Idle);
1433 assert_eq!(fixture.pending_frame(cx), 0);
1434 }
1435
1436 #[test]
1437 fn transition_ids_accept_element_like_scalars_and_named_channels() {
1438 assert_eq!(
1439 TransitionId::from("opacity"),
1440 TransitionId::from(ElementId::from("opacity"))
1441 );
1442 assert_ne!(
1443 TransitionId::from(("terms", "fill")),
1444 TransitionId::from(("terms", "mark-opacity"))
1445 );
1446 let _: TransitionId = 7usize.into();
1447 let _: TransitionId = 7i32.into();
1448 }
1449
1450 struct TestView {
1451 target: Rc<Cell<f32>>,
1452 duration: Duration,
1453 samples: Rc<RefCell<Vec<f32>>>,
1454 }
1455
1456 impl Render for TestView {
1457 fn render(
1458 &mut self,
1459 window: &mut Window,
1460 cx: &mut gpui::Context<Self>,
1461 ) -> impl IntoElement {
1462 self.samples.borrow_mut().push(transition(
1463 ("test", "value"),
1464 self.target.get(),
1465 Transition::new(self.duration).ease(|t| t),
1466 window,
1467 cx,
1468 ));
1469 Empty
1470 }
1471 }
1472
1473 struct DelayedView {
1474 target: Rc<Cell<f32>>,
1475 samples: Rc<RefCell<Vec<f32>>>,
1476 }
1477
1478 impl Render for DelayedView {
1479 fn render(
1480 &mut self,
1481 window: &mut Window,
1482 cx: &mut gpui::Context<Self>,
1483 ) -> impl IntoElement {
1484 self.samples.borrow_mut().push(transition(
1485 ("delayed-test", "value"),
1486 self.target.get(),
1487 Transition::new(Duration::from_millis(100))
1488 .delay(Duration::from_millis(50))
1489 .ease(|t| t),
1490 window,
1491 cx,
1492 ));
1493 Empty
1494 }
1495 }
1496
1497 struct Fixture {
1498 window: WindowHandle<TestView>,
1499 target: Rc<Cell<f32>>,
1500 samples: Rc<RefCell<Vec<f32>>>,
1501 }
1502
1503 impl Fixture {
1504 fn open(cx: &mut TestAppContext, duration: Duration) -> Self {
1505 let target = Rc::new(Cell::new(0.0));
1506 let samples = Rc::new(RefCell::new(Vec::new()));
1507 let window = cx.open_window(size(px(100.), px(100.)), {
1508 let target = target.clone();
1509 let samples = samples.clone();
1510 move |_, _| TestView {
1511 target,
1512 duration,
1513 samples,
1514 }
1515 });
1516 cx.run_until_parked();
1517 Self {
1518 window,
1519 target,
1520 samples,
1521 }
1522 }
1523
1524 fn render(&self, cx: &mut TestAppContext, target: f32) -> f32 {
1525 self.target.set(target);
1526 self.window
1527 .update(cx, |_, window, _| window.refresh())
1528 .unwrap();
1529 cx.run_until_parked();
1530 *self.samples.borrow().last().unwrap()
1531 }
1532
1533 fn pending_frame(&self, cx: &mut TestAppContext) -> usize {
1534 self.window
1535 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
1536 .unwrap()
1537 }
1538 }
1539
1540 #[gpui::test]
1541 fn a_zero_duration_target_change_is_immediate(cx: &mut TestAppContext) {
1542 let fixture = Fixture::open(cx, Duration::ZERO);
1543 assert_eq!(fixture.render(cx, 1.0), 1.0);
1544 }
1545
1546 #[gpui::test]
1547 fn a_changed_target_transitions_over_time(cx: &mut TestAppContext) {
1548 let duration = Duration::from_millis(100);
1549 let fixture = Fixture::open(cx, duration);
1550 assert_eq!(fixture.render(cx, 10.0), 0.0);
1551
1552 cx.executor().advance_clock(Duration::from_millis(50));
1553 assert_eq!(fixture.render(cx, 10.0), 5.0);
1554 }
1555
1556 #[gpui::test]
1557 fn requested_animation_frames_resample_without_manual_refresh(cx: &mut TestAppContext) {
1558 let duration = Duration::from_millis(100);
1559 let fixture = Fixture::open(cx, duration);
1560 assert_eq!(fixture.render(cx, 10.0), 0.0);
1561
1562 cx.executor().advance_clock(Duration::from_millis(50));
1563 assert_eq!(fixture.pending_frame(cx), 1);
1564 cx.run_until_parked();
1565
1566 assert_eq!(*fixture.samples.borrow().last().unwrap(), 5.0);
1567 }
1568
1569 #[gpui::test]
1570 fn reversing_uses_the_current_sample_and_shortens_the_return(cx: &mut TestAppContext) {
1571 let duration = Duration::from_millis(100);
1572 let fixture = Fixture::open(cx, duration);
1573 assert_eq!(fixture.render(cx, 10.0), 0.0);
1574
1575 cx.executor().advance_clock(Duration::from_millis(50));
1576 assert_eq!(fixture.render(cx, 0.0), 5.0);
1577 cx.executor().advance_clock(Duration::from_millis(25));
1578 assert_eq!(fixture.render(cx, 0.0), 2.5);
1579 }
1580
1581 #[gpui::test]
1582 fn delay_holds_the_previous_value_before_interpolation(cx: &mut TestAppContext) {
1583 let target = Rc::new(Cell::new(0.0));
1584 let samples = Rc::new(RefCell::new(Vec::new()));
1585 let window = cx.open_window(size(px(100.), px(100.)), {
1586 let target = target.clone();
1587 let samples = samples.clone();
1588 move |_, _| DelayedView { target, samples }
1589 });
1590 cx.run_until_parked();
1591
1592 target.set(10.0);
1593 window.update(cx, |_, window, _| window.refresh()).unwrap();
1594 cx.run_until_parked();
1595 assert_eq!(*samples.borrow().last().unwrap(), 0.0);
1596
1597 cx.executor().advance_clock(Duration::from_millis(50));
1598 window.update(cx, |_, window, _| window.refresh()).unwrap();
1599 cx.run_until_parked();
1600 assert_eq!(*samples.borrow().last().unwrap(), 0.0);
1601
1602 cx.executor().advance_clock(Duration::from_millis(50));
1603 window.update(cx, |_, window, _| window.refresh()).unwrap();
1604 cx.run_until_parked();
1605 assert_eq!(*samples.borrow().last().unwrap(), 5.0);
1606 }
1607
1608 #[gpui::test]
1609 fn a_completed_transition_stops_requesting_frames(cx: &mut TestAppContext) {
1610 let duration = Duration::from_millis(100);
1611 let fixture = Fixture::open(cx, duration);
1612 fixture.render(cx, 1.0);
1613 assert_eq!(fixture.pending_frame(cx), 1);
1614
1615 cx.executor().advance_clock(duration);
1616 assert_eq!(fixture.render(cx, 1.0), 1.0);
1617 fixture.pending_frame(cx);
1618 cx.run_until_parked();
1619 assert_eq!(fixture.pending_frame(cx), 0);
1620 }
1621
1622 #[gpui::test]
1623 fn reduced_motion_adopts_the_target_without_requesting_a_frame(cx: &mut TestAppContext) {
1624 cx.update(|cx| cx.set_reduce_motion(true));
1625 let duration = Duration::from_millis(100);
1626 let fixture = Fixture::open(cx, duration);
1627 assert_eq!(fixture.render(cx, 1.0), 1.0);
1628 assert_eq!(fixture.pending_frame(cx), 0);
1629 }
1630
1631 struct SpringView {
1632 target: Rc<Cell<f32>>,
1633 policy: Rc<Cell<Spring>>,
1634 samples: Rc<RefCell<Vec<f32>>>,
1635 }
1636
1637 impl Render for SpringView {
1638 fn render(
1639 &mut self,
1640 window: &mut Window,
1641 cx: &mut gpui::Context<Self>,
1642 ) -> impl IntoElement {
1643 self.samples.borrow_mut().push(spring(
1644 ("spring-test", "value"),
1645 self.target.get(),
1646 self.policy.get(),
1647 window,
1648 cx,
1649 ));
1650 Empty
1651 }
1652 }
1653
1654 struct SpringFixture {
1655 window: WindowHandle<SpringView>,
1656 target: Rc<Cell<f32>>,
1657 policy: Rc<Cell<Spring>>,
1658 samples: Rc<RefCell<Vec<f32>>>,
1659 }
1660
1661 impl SpringFixture {
1662 fn open(cx: &mut TestAppContext, policy: Spring) -> Self {
1663 let target = Rc::new(Cell::new(0.0));
1664 let policy = Rc::new(Cell::new(policy));
1665 let samples = Rc::new(RefCell::new(Vec::new()));
1666 let window = cx.open_window(size(px(100.), px(100.)), {
1667 let target = target.clone();
1668 let policy = policy.clone();
1669 let samples = samples.clone();
1670 move |_, _| SpringView {
1671 target,
1672 policy,
1673 samples,
1674 }
1675 });
1676 cx.run_until_parked();
1677 Self {
1678 window,
1679 target,
1680 policy,
1681 samples,
1682 }
1683 }
1684
1685 fn render(&self, cx: &mut TestAppContext, target: f32) -> f32 {
1686 self.target.set(target);
1687 self.window
1688 .update(cx, |_, window, _| window.refresh())
1689 .unwrap();
1690 cx.run_until_parked();
1691 *self.samples.borrow().last().unwrap()
1692 }
1693
1694 fn advance(&self, cx: &mut TestAppContext, millis: u64, target: f32) -> f32 {
1695 cx.executor().advance_clock(Duration::from_millis(millis));
1696 self.render(cx, target)
1697 }
1698
1699 fn pending_frame(&self, cx: &mut TestAppContext) -> usize {
1700 self.window
1701 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
1702 .unwrap()
1703 }
1704 }
1705
1706 #[gpui::test]
1707 fn a_spring_adopts_its_first_target_immediately(cx: &mut TestAppContext) {
1708 let fixture = SpringFixture::open(cx, Spring::new(Duration::from_millis(300)));
1709 assert_eq!(*fixture.samples.borrow().first().unwrap(), 0.0);
1710 }
1711
1712 #[gpui::test]
1713 fn a_spring_travels_toward_its_target_over_time(cx: &mut TestAppContext) {
1714 let fixture = SpringFixture::open(cx, Spring::new(Duration::from_millis(300)));
1715 assert_eq!(fixture.render(cx, 1.0), 0.0);
1716
1717 let early = fixture.advance(cx, 50, 1.0);
1718 let late = fixture.advance(cx, 50, 1.0);
1719 assert!(
1720 0.0 < early && early < late && late < 1.0,
1721 "expected monotonic approach, got {early} then {late}"
1722 );
1723 }
1724
1725 #[gpui::test]
1726 fn a_reversed_spring_keeps_its_momentum_before_turning_around(cx: &mut TestAppContext) {
1727 let fixture = SpringFixture::open(cx, Spring::new(Duration::from_millis(300)));
1728 fixture.render(cx, 1.0);
1729 let reversed_at = fixture.advance(cx, 100, 1.0);
1730
1731 assert_eq!(fixture.render(cx, 0.0), reversed_at);
1734
1735 let next = fixture.advance(cx, 16, 0.0);
1736 assert!(
1737 next > reversed_at,
1738 "expected the spring to carry its velocity past {reversed_at}, got {next}"
1739 );
1740
1741 assert_eq!(fixture.advance(cx, 1_000, 0.0), 0.0);
1742 }
1743
1744 #[gpui::test]
1745 fn a_bouncy_spring_overshoots_its_target(cx: &mut TestAppContext) {
1746 let fixture = SpringFixture::open(
1747 cx,
1748 Spring::new(Duration::from_millis(350)).with_damping(0.7),
1749 );
1750 fixture.render(cx, 1.0);
1751 for _ in 0..30 {
1752 fixture.advance(cx, 16, 1.0);
1753 }
1754
1755 let peak = fixture
1756 .samples
1757 .borrow()
1758 .iter()
1759 .copied()
1760 .fold(f32::MIN, f32::max);
1761 assert!(peak > 1.0, "expected an overshoot past 1.0, got {peak}");
1762 }
1763
1764 #[gpui::test]
1765 fn a_settled_spring_stops_requesting_frames(cx: &mut TestAppContext) {
1766 let fixture = SpringFixture::open(cx, Spring::new(Duration::from_millis(300)));
1767 fixture.render(cx, 1.0);
1768 assert_eq!(fixture.pending_frame(cx), 1);
1769
1770 assert_eq!(fixture.advance(cx, 2_000, 1.0), 1.0);
1771 fixture.pending_frame(cx);
1772 cx.run_until_parked();
1773 assert_eq!(fixture.pending_frame(cx), 0);
1774 }
1775
1776 #[gpui::test]
1777 fn a_spring_that_is_not_travelling_adopts_its_target_on_the_spot(cx: &mut TestAppContext) {
1778 let travelling = Spring::new(Duration::from_millis(300));
1779 let fixture = SpringFixture::open(cx, travelling.with_travel(false));
1780
1781 assert_eq!(fixture.render(cx, 1.0), 1.0);
1782 assert_eq!(fixture.pending_frame(cx), 0);
1783 assert_eq!(fixture.advance(cx, 100, 5.0), 5.0);
1784
1785 fixture.policy.set(travelling);
1788 assert_eq!(fixture.render(cx, 6.0), 5.0);
1789 let next = fixture.advance(cx, 50, 6.0);
1790 assert!(
1791 5.0 < next && next < 6.0,
1792 "expected travel to resume from 5.0, got {next}"
1793 );
1794 }
1795
1796 #[gpui::test]
1797 fn a_zero_response_spring_resolves_instead_of_dividing_by_its_period(cx: &mut TestAppContext) {
1798 let fixture = SpringFixture::open(cx, Spring::new(Duration::ZERO));
1799 assert_eq!(fixture.render(cx, 1.0), 1.0);
1800 assert_eq!(fixture.pending_frame(cx), 0);
1801 }
1802
1803 #[test]
1804 fn spring_rejects_non_finite_or_negative_physical_parameters() {
1805 let spring = Spring::new(Duration::from_millis(300));
1806
1807 assert_eq!(
1808 spring.try_with_damping(f32::NAN).unwrap_err(),
1809 SpringError::InvalidDamping
1810 );
1811 assert_eq!(
1812 spring.try_with_damping(-0.1).unwrap_err(),
1813 SpringError::InvalidDamping
1814 );
1815 assert_eq!(
1816 spring.try_with_epsilon(f32::INFINITY).unwrap_err(),
1817 SpringError::InvalidEpsilon
1818 );
1819 assert_eq!(
1820 spring.try_with_epsilon(-0.1).unwrap_err(),
1821 SpringError::InvalidEpsilon
1822 );
1823 }
1824
1825 #[test]
1826 fn spring_reports_its_unit_specific_settling_tolerance() {
1827 let normalized = Spring::new(Duration::from_millis(180));
1828 let pixels = Spring::new(Duration::from_millis(180)).with_epsilon(0.1);
1829
1830 assert!(normalized.epsilon() < 0.01);
1831 assert_eq!(pixels.epsilon(), 0.1);
1832 }
1833
1834 #[gpui::test]
1835 fn reduced_motion_adopts_the_spring_target_without_requesting_a_frame(cx: &mut TestAppContext) {
1836 cx.update(|cx| cx.set_reduce_motion(true));
1837 let fixture = SpringFixture::open(
1838 cx,
1839 Spring::new(Duration::from_millis(350)).with_damping(0.7),
1840 );
1841 assert_eq!(fixture.render(cx, 1.0), 1.0);
1842 assert_eq!(fixture.pending_frame(cx), 0);
1843 }
1844}