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::{num::NonZeroU32, sync::Arc, 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 curves_built_without_stops_or_jumps_sample_linearly() {
736 let no_stops = Easing::LinearStops(Arc::from([]));
738 let no_jumps = Easing::Steps {
739 count: NonZeroU32::MIN,
740 position: StepPosition::JumpNone,
741 };
742
743 for (progress, expected) in [(-1.0, 0.0), (0.25, 0.25), (2.0, 1.0)] {
744 assert_eq!(
745 no_stops.sample(progress),
746 expected,
747 "no stops at {progress}"
748 );
749 assert_eq!(
750 no_jumps.sample(progress),
751 expected,
752 "no jumps at {progress}"
753 );
754 }
755 }
756
757 #[test]
758 fn negative_delay_starts_inside_the_active_interval() {
759 let timing = Timing::new(Duration::from_millis(100))
760 .delay(SignedDuration::negative(Duration::from_millis(25)));
761 let sample = timing.sample(Duration::ZERO);
762
763 assert_eq!(sample.phase, MotionPhase::Active);
764 assert!((sample.directed_progress - 0.25).abs() < f32::EPSILON);
765 assert!(sample.active);
766 assert!(!sample.finished);
767 }
768
769 #[test]
770 fn alternate_direction_reverses_odd_iterations() {
771 let timing = Timing::new(Duration::from_millis(100))
772 .iterations(IterationCount::Finite(2))
773 .direction(PlaybackDirection::Alternate)
774 .ease(Easing::Linear);
775
776 let first = timing.sample(Duration::from_millis(25));
777 let second = timing.sample(Duration::from_millis(125));
778 let finished = timing.sample(Duration::from_millis(200));
779
780 assert_eq!(first.iteration, 0);
781 assert_eq!(first.directed_progress, 0.25);
782 assert_eq!(second.iteration, 1);
783 assert_eq!(second.directed_progress, 0.75);
784 assert_eq!(finished.phase, MotionPhase::After);
785 assert_eq!(finished.directed_progress, 0.0);
786 assert!(finished.finished);
787 }
788}
789
790#[cfg(test)]
791mod motion_track_tests {
792 use super::{
793 Discrete, Easing, Interpolate as _, Keyframe, KeyframeError, Keyframes, MotionTransform,
794 Stagger, StaggerOrigin,
795 };
796 use gpui::{Bounds, Point, Size, point, px, size};
797 use std::time::Duration;
798
799 #[test]
800 fn keyframes_validate_offsets_and_sample_each_segments_easing() {
801 assert!(matches!(
802 Keyframes::try_new([Keyframe::new(0.2, 0.0_f32), Keyframe::new(1.0, 1.0_f32),]),
803 Err(KeyframeError::MissingEndpoint)
804 ));
805 assert!(matches!(
806 Keyframes::try_new([
807 Keyframe::new(0.0, 0.0_f32),
808 Keyframe::new(0.8, 1.0_f32),
809 Keyframe::new(0.7, 2.0_f32),
810 Keyframe::new(1.0, 3.0_f32),
811 ]),
812 Err(KeyframeError::OffsetsNotMonotonic)
813 ));
814
815 let track = Keyframes::try_new([
816 Keyframe::new(0.0, 0.0_f32)
817 .ease(Easing::steps(2, super::StepPosition::JumpEnd).unwrap()),
818 Keyframe::new(0.5, 10.0_f32).ease(Easing::Linear),
819 Keyframe::new(1.0, 20.0_f32),
820 ])
821 .unwrap();
822
823 assert_eq!(track.sample(0.2), 0.0);
824 assert_eq!(track.sample(0.3), 5.0);
825 assert_eq!(track.sample(0.75), 15.0);
826 assert_eq!(track.sample(1.0), 20.0);
827 }
828
829 #[test]
830 fn discrete_values_switch_only_at_the_requested_progress() {
831 let value = Discrete::new("old", "new").switch_at(0.75).unwrap();
832 assert_eq!(value.sample(0.749), "old");
833 assert_eq!(value.sample(0.75), "new");
834 assert!(Discrete::new(0, 1).switch_at(f32::NAN).is_err());
835 }
836
837 #[test]
838 fn stagger_origins_produce_stable_delays_without_allocating_a_schedule() {
839 let interval = Duration::from_millis(20);
840 let first = Stagger::new(interval, StaggerOrigin::First);
841 let last = Stagger::new(interval, StaggerOrigin::Last);
842 let center = Stagger::new(interval, StaggerOrigin::Center);
843
844 assert_eq!(first.delay(3, 5), Duration::from_millis(60));
845 assert_eq!(last.delay(3, 5), Duration::from_millis(20));
846 assert_eq!(center.delay(2, 5), Duration::ZERO);
847 assert_eq!(center.delay(0, 5), Duration::from_millis(40));
848 assert_eq!(first.delay(7, 0), Duration::ZERO);
849 }
850
851 #[test]
852 fn common_gpui_geometry_interpolates_channel_by_channel() {
853 let from_size = size(px(10.0), px(20.0));
854 let to_size = size(px(30.0), px(60.0));
855 assert_eq!(
856 from_size.interpolate(&to_size, 0.25),
857 size(px(15.0), px(30.0))
858 );
859
860 let from = Bounds::new(point(px(0.0), px(10.0)), from_size);
861 let to = Bounds::new(point(px(40.0), px(50.0)), to_size);
862 assert_eq!(
863 from.interpolate(&to, 0.5),
864 Bounds::new(point(px(20.0), px(30.0)), size(px(20.0), px(40.0)))
865 );
866
867 let _: Point<gpui::Pixels> = from.origin;
868 let _: Size<gpui::Pixels> = from.size;
869
870 let transform = MotionTransform::identity().interpolate(
871 &MotionTransform {
872 translation: point(px(20.0), px(40.0)),
873 scale: point(2.0, 0.5),
874 rotation_radians: std::f32::consts::PI,
875 opacity: 0.0,
876 },
877 0.5,
878 );
879 assert_eq!(transform.translation, point(px(10.0), px(20.0)));
880 assert_eq!(transform.scale, point(1.5, 0.75));
881 assert_eq!(transform.rotation_radians, std::f32::consts::FRAC_PI_2);
882 assert_eq!(transform.opacity, 0.5);
883 }
884}
885
886#[cfg(test)]
887mod tests {
888 use std::{
889 cell::{Cell, RefCell},
890 rc::Rc,
891 time::Duration,
892 };
893
894 use gpui::{Empty, IntoElement, Render, TestAppContext, WindowHandle, px, size};
895
896 use super::*;
897
898 struct StatusView {
899 target: Rc<Cell<f32>>,
900 policy: Transition,
901 samples: Rc<RefCell<Vec<MotionValue<f32>>>>,
902 }
903
904 impl Render for StatusView {
905 fn render(
906 &mut self,
907 window: &mut Window,
908 cx: &mut gpui::Context<Self>,
909 ) -> impl IntoElement {
910 self.samples.borrow_mut().push(transition_with_status(
911 ("status-test", "value"),
912 self.target.get(),
913 self.policy.clone(),
914 window,
915 cx,
916 ));
917 Empty
918 }
919 }
920
921 struct StatusFixture {
922 window: WindowHandle<StatusView>,
923 target: Rc<Cell<f32>>,
924 samples: Rc<RefCell<Vec<MotionValue<f32>>>>,
925 }
926
927 impl StatusFixture {
928 fn open(cx: &mut TestAppContext, policy: Transition) -> Self {
929 let target = Rc::new(Cell::new(0.0));
930 let samples = Rc::new(RefCell::new(Vec::new()));
931 let window = cx.open_window(size(px(100.), px(100.)), {
932 let target = target.clone();
933 let samples = samples.clone();
934 move |_, _| StatusView {
935 target,
936 policy,
937 samples,
938 }
939 });
940 cx.run_until_parked();
941 Self {
942 window,
943 target,
944 samples,
945 }
946 }
947
948 fn render(&self, cx: &mut TestAppContext, target: f32) -> MotionValue<f32> {
949 self.target.set(target);
950 self.window
951 .update(cx, |_, window, _| window.refresh())
952 .unwrap();
953 cx.run_until_parked();
954 *self.samples.borrow().last().unwrap()
955 }
956 }
957
958 #[gpui::test]
959 fn status_transition_reports_delay_running_and_finished(cx: &mut TestAppContext) {
960 let fixture = StatusFixture::open(
961 cx,
962 Transition::new(Duration::from_millis(100)).delay(Duration::from_millis(20)),
963 );
964 assert_eq!(fixture.render(cx, 1.0).status, MotionStatus::Delayed);
965
966 cx.executor().advance_clock(Duration::from_millis(20));
967 assert_eq!(fixture.render(cx, 1.0).status, MotionStatus::Running);
968 cx.executor().advance_clock(Duration::from_millis(100));
969 assert_eq!(fixture.render(cx, 1.0).status, MotionStatus::Finished);
970 }
971
972 #[gpui::test]
973 fn negative_delay_samples_a_target_change_inside_its_interval(cx: &mut TestAppContext) {
974 let fixture = StatusFixture::open(
975 cx,
976 Transition::new(Duration::from_millis(100))
977 .delay(SignedDuration::negative(Duration::from_millis(25)))
978 .ease(|t| t),
979 );
980 let sample = fixture.render(cx, 1.0);
981 assert_eq!(sample.status, MotionStatus::Running);
982 assert_eq!(sample.value, 0.25);
983 }
984
985 #[gpui::test]
986 fn a_direct_reversal_shortens_the_return_transition(cx: &mut TestAppContext) {
987 let fixture =
988 StatusFixture::open(cx, Transition::new(Duration::from_millis(100)).ease(|t| t));
989 assert_eq!(fixture.render(cx, 1.0).value, 0.0);
990 cx.executor().advance_clock(Duration::from_millis(50));
991 assert_eq!(fixture.render(cx, 0.0).value, 0.5);
992 cx.executor().advance_clock(Duration::from_millis(25));
993 assert_eq!(fixture.render(cx, 0.0).value, 0.25);
994 }
995
996 struct KeyframeView {
997 track: Keyframes<f32>,
998 timing: Timing,
999 samples: Rc<RefCell<Vec<MotionValue<f32>>>>,
1000 }
1001
1002 impl Render for KeyframeView {
1003 fn render(
1004 &mut self,
1005 window: &mut Window,
1006 cx: &mut gpui::Context<Self>,
1007 ) -> impl IntoElement {
1008 self.samples.borrow_mut().push(animate_keyframes(
1009 "keyframe-test",
1010 &self.track,
1011 self.timing.clone(),
1012 window,
1013 cx,
1014 ));
1015 Empty
1016 }
1017 }
1018
1019 #[gpui::test]
1020 fn keyed_keyframes_follow_timing_and_stop_after_completion(cx: &mut TestAppContext) {
1021 let samples = Rc::new(RefCell::new(Vec::new()));
1022 let window = cx.open_window(size(px(100.), px(100.)), {
1023 let samples = samples.clone();
1024 move |_, _| KeyframeView {
1025 track: Keyframes::try_new([Keyframe::new(0.0, 0.0), Keyframe::new(1.0, 10.0)])
1026 .unwrap(),
1027 timing: Timing::new(Duration::from_millis(100)),
1028 samples,
1029 }
1030 });
1031 cx.run_until_parked();
1032 assert_eq!(samples.borrow().last().unwrap().value, 0.0);
1033 assert_eq!(
1034 samples.borrow().last().unwrap().status,
1035 MotionStatus::Running
1036 );
1037
1038 cx.executor().advance_clock(Duration::from_millis(50));
1039 assert_eq!(
1040 window
1041 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
1042 .unwrap(),
1043 1
1044 );
1045 cx.run_until_parked();
1046 assert_eq!(samples.borrow().last().unwrap().value, 5.0);
1047
1048 cx.executor().advance_clock(Duration::from_millis(50));
1049 window.update(cx, |_, window, _| window.refresh()).unwrap();
1050 cx.run_until_parked();
1051 assert_eq!(
1052 samples.borrow().last().unwrap().status,
1053 MotionStatus::Finished
1054 );
1055 window
1056 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
1057 .unwrap();
1058 cx.run_until_parked();
1059 assert_eq!(
1060 window
1061 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
1062 .unwrap(),
1063 0
1064 );
1065 }
1066
1067 struct PresenceView {
1068 present: Rc<Cell<bool>>,
1069 samples: Rc<RefCell<Vec<PresenceSample>>>,
1070 }
1071
1072 impl Render for PresenceView {
1073 fn render(
1074 &mut self,
1075 window: &mut Window,
1076 cx: &mut gpui::Context<Self>,
1077 ) -> impl IntoElement {
1078 self.samples.borrow_mut().push(
1079 Presence::new("presence-test", self.present.get())
1080 .transition(Transition::new(Duration::from_millis(100)).ease(|t| t))
1081 .sample(window, cx),
1082 );
1083 Empty
1084 }
1085 }
1086
1087 struct PresenceFixture {
1088 window: WindowHandle<PresenceView>,
1089 present: Rc<Cell<bool>>,
1090 samples: Rc<RefCell<Vec<PresenceSample>>>,
1091 }
1092
1093 impl PresenceFixture {
1094 fn open(cx: &mut TestAppContext, initially_present: bool) -> Self {
1095 let present = Rc::new(Cell::new(initially_present));
1096 let samples = Rc::new(RefCell::new(Vec::new()));
1097 let window = cx.open_window(size(px(100.), px(100.)), {
1098 let present = present.clone();
1099 let samples = samples.clone();
1100 move |_, _| PresenceView { present, samples }
1101 });
1102 cx.run_until_parked();
1103 Self {
1104 window,
1105 present,
1106 samples,
1107 }
1108 }
1109
1110 fn render(&self, cx: &mut TestAppContext, present: bool) -> PresenceSample {
1111 self.present.set(present);
1112 self.window
1113 .update(cx, |_, window, _| window.refresh())
1114 .unwrap();
1115 cx.run_until_parked();
1116 *self.samples.borrow().last().unwrap()
1117 }
1118 }
1119
1120 #[gpui::test]
1121 fn presence_enters_exits_and_only_unmounts_after_exit(cx: &mut TestAppContext) {
1122 let fixture = PresenceFixture::open(cx, true);
1123 let entering = *fixture.samples.borrow().last().unwrap();
1124 assert_eq!(entering.phase, PresencePhase::Entering);
1125 assert_eq!(entering.progress, 0.0);
1126 assert!(entering.should_render());
1127
1128 cx.executor().advance_clock(Duration::from_millis(100));
1129 let present = fixture.render(cx, true);
1130 assert_eq!(present.phase, PresencePhase::Present);
1131 assert_eq!(present.progress, 1.0);
1132
1133 let exiting = fixture.render(cx, false);
1134 assert_eq!(exiting.phase, PresencePhase::Exiting);
1135 assert_eq!(exiting.progress, 1.0);
1136 assert!(exiting.should_render());
1137
1138 cx.executor().advance_clock(Duration::from_millis(100));
1139 let absent = fixture.render(cx, false);
1140 assert_eq!(absent.phase, PresencePhase::Absent);
1141 assert_eq!(absent.progress, 0.0);
1142 assert!(!absent.should_render());
1143 }
1144
1145 #[gpui::test]
1146 fn presence_reentry_reverses_from_the_exit_sample(cx: &mut TestAppContext) {
1147 let fixture = PresenceFixture::open(cx, true);
1148 cx.executor().advance_clock(Duration::from_millis(100));
1149 fixture.render(cx, true);
1150 fixture.render(cx, false);
1151 cx.executor().advance_clock(Duration::from_millis(40));
1152 let reentering = fixture.render(cx, true);
1153
1154 assert_eq!(reentering.phase, PresencePhase::Entering);
1155 assert_eq!(reentering.progress, 0.6);
1156 }
1157
1158 #[gpui::test]
1159 fn reduced_motion_resolves_presence_without_a_pending_frame(cx: &mut TestAppContext) {
1160 cx.update(|cx| cx.set_reduce_motion(true));
1161 let fixture = PresenceFixture::open(cx, true);
1162 assert_eq!(
1163 fixture.samples.borrow().last().unwrap().phase,
1164 PresencePhase::Present
1165 );
1166 assert_eq!(
1167 fixture
1168 .window
1169 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
1170 .unwrap(),
1171 0
1172 );
1173 assert_eq!(fixture.render(cx, false).phase, PresencePhase::Absent);
1174 }
1175
1176 struct SequenceView {
1177 steps: Rc<RefCell<Vec<SequenceStep<f32>>>>,
1178 samples: Rc<RefCell<Vec<SequenceSample<f32>>>>,
1179 }
1180
1181 impl Render for SequenceView {
1182 fn render(
1183 &mut self,
1184 window: &mut Window,
1185 cx: &mut gpui::Context<Self>,
1186 ) -> impl IntoElement {
1187 self.samples.borrow_mut().push(
1188 Sequence::new("sequence-test", 0.0)
1189 .with_steps(self.steps.borrow().iter().cloned())
1190 .sample(window, cx),
1191 );
1192 Empty
1193 }
1194 }
1195
1196 struct SequenceFixture {
1197 window: WindowHandle<SequenceView>,
1198 steps: Rc<RefCell<Vec<SequenceStep<f32>>>>,
1199 samples: Rc<RefCell<Vec<SequenceSample<f32>>>>,
1200 }
1201
1202 impl SequenceFixture {
1203 fn open(cx: &mut TestAppContext, steps: Vec<SequenceStep<f32>>) -> Self {
1204 let steps = Rc::new(RefCell::new(steps));
1205 let samples = Rc::new(RefCell::new(Vec::new()));
1206 let window = cx.open_window(size(px(100.), px(100.)), {
1207 let steps = steps.clone();
1208 let samples = samples.clone();
1209 move |_, _| SequenceView { steps, samples }
1210 });
1211 cx.run_until_parked();
1212 Self {
1213 window,
1214 steps,
1215 samples,
1216 }
1217 }
1218
1219 fn linear(millis: u64) -> Transition {
1220 Transition::new(Duration::from_millis(millis)).ease(|t| t)
1221 }
1222
1223 fn last(&self) -> SequenceSample<f32> {
1224 *self.samples.borrow().last().unwrap()
1225 }
1226
1227 fn render(&self, cx: &mut TestAppContext) -> SequenceSample<f32> {
1228 self.window
1229 .update(cx, |_, window, _| window.refresh())
1230 .unwrap();
1231 cx.run_until_parked();
1232 self.last()
1233 }
1234
1235 fn advance(&self, cx: &mut TestAppContext, millis: u64) -> SequenceSample<f32> {
1236 cx.executor().advance_clock(Duration::from_millis(millis));
1237 self.render(cx)
1238 }
1239
1240 fn pending_frame(&self, cx: &mut TestAppContext) -> usize {
1241 self.window
1242 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
1243 .unwrap()
1244 }
1245 }
1246
1247 fn assert_sample(sample: SequenceSample<f32>, value: f32, step: usize, status: MotionStatus) {
1248 assert_eq!(
1249 (*sample.value(), sample.step(), sample.status()),
1250 (value, step, status)
1251 );
1252 }
1253
1254 #[gpui::test]
1255 fn sequence_steps_advance_in_order_at_their_boundaries(cx: &mut TestAppContext) {
1256 let fixture = SequenceFixture::open(
1257 cx,
1258 vec![
1259 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1260 SequenceStep::new(
1261 20.0,
1262 SequenceFixture::linear(100).delay(Duration::from_millis(50)),
1263 ),
1264 ],
1265 );
1266 assert_sample(fixture.last(), 0.0, 0, MotionStatus::Running);
1267 assert_sample(fixture.advance(cx, 50), 5.0, 0, MotionStatus::Running);
1268
1269 assert_sample(fixture.advance(cx, 50), 10.0, 1, MotionStatus::Delayed);
1272 assert_sample(fixture.advance(cx, 50), 10.0, 1, MotionStatus::Running);
1273 assert_sample(fixture.advance(cx, 50), 15.0, 1, MotionStatus::Running);
1274 assert_sample(fixture.advance(cx, 50), 20.0, 1, MotionStatus::Finished);
1275 }
1276
1277 #[gpui::test]
1278 fn a_sequence_starts_its_next_step_where_the_previous_ended_not_at_the_frame(
1279 cx: &mut TestAppContext,
1280 ) {
1281 let fixture = SequenceFixture::open(
1282 cx,
1283 vec![
1284 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1285 SequenceStep::new(20.0, SequenceFixture::linear(100)),
1286 ],
1287 );
1288 fixture.advance(cx, 50);
1291 assert_sample(fixture.advance(cx, 125), 17.5, 1, MotionStatus::Running);
1292 }
1293
1294 #[gpui::test]
1295 fn zero_duration_steps_complete_within_the_frame_that_reaches_them(cx: &mut TestAppContext) {
1296 let fixture = SequenceFixture::open(
1297 cx,
1298 vec![
1299 SequenceStep::new(5.0, SequenceFixture::linear(0)),
1300 SequenceStep::new(6.0, SequenceFixture::linear(0)),
1301 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1302 ],
1303 );
1304 assert_sample(fixture.last(), 6.0, 2, MotionStatus::Running);
1305 assert_sample(fixture.advance(cx, 50), 8.0, 2, MotionStatus::Running);
1306 }
1307
1308 #[gpui::test]
1309 fn a_sequence_reports_finished_once_and_then_stops_requesting_frames(cx: &mut TestAppContext) {
1310 let fixture = SequenceFixture::open(
1311 cx,
1312 vec![
1313 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1314 SequenceStep::new(20.0, SequenceFixture::linear(100)),
1315 ],
1316 );
1317 assert_eq!(fixture.pending_frame(cx), 1);
1318 cx.run_until_parked();
1319
1320 cx.executor().advance_clock(Duration::from_millis(100));
1321 assert_eq!(fixture.pending_frame(cx), 1);
1322 cx.run_until_parked();
1323 assert_eq!(fixture.last().status(), MotionStatus::Running);
1324 assert!(
1325 fixture
1326 .samples
1327 .borrow()
1328 .iter()
1329 .all(|sample| !sample.is_finished()),
1330 "the first step's end must not read as the sequence finishing"
1331 );
1332
1333 assert_sample(fixture.advance(cx, 100), 20.0, 1, MotionStatus::Finished);
1334 fixture.pending_frame(cx);
1335 cx.run_until_parked();
1336 assert_eq!(fixture.pending_frame(cx), 0);
1337 assert_sample(fixture.advance(cx, 1_000), 20.0, 1, MotionStatus::Finished);
1338 }
1339
1340 #[gpui::test]
1341 fn reduced_motion_adopts_a_sequences_last_target_without_requesting_a_frame(
1342 cx: &mut TestAppContext,
1343 ) {
1344 cx.update(|cx| cx.set_reduce_motion(true));
1345 let fixture = SequenceFixture::open(
1346 cx,
1347 vec![
1348 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1349 SequenceStep::new(20.0, SequenceFixture::linear(100)),
1350 ],
1351 );
1352 assert_sample(fixture.last(), 20.0, 1, MotionStatus::Finished);
1353 assert_eq!(fixture.pending_frame(cx), 0);
1354 }
1355
1356 #[gpui::test]
1357 fn a_changed_step_target_restarts_the_sequence_from_the_sampled_value(cx: &mut TestAppContext) {
1358 let fixture = SequenceFixture::open(
1359 cx,
1360 vec![
1361 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1362 SequenceStep::new(20.0, SequenceFixture::linear(100)),
1363 ],
1364 );
1365 fixture.advance(cx, 150);
1366 assert_sample(fixture.last(), 15.0, 1, MotionStatus::Running);
1367
1368 *fixture.steps.borrow_mut() = vec![
1369 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1370 SequenceStep::new(35.0, SequenceFixture::linear(100)),
1371 ];
1372 assert_sample(fixture.render(cx), 15.0, 0, MotionStatus::Running);
1373 assert_sample(fixture.advance(cx, 50), 12.5, 0, MotionStatus::Running);
1374 assert_sample(fixture.advance(cx, 100), 22.5, 1, MotionStatus::Running);
1375
1376 *fixture.steps.borrow_mut() = vec![SequenceStep::new(0.0, SequenceFixture::linear(100))];
1378 assert_sample(fixture.render(cx), 22.5, 0, MotionStatus::Running);
1379 assert_sample(fixture.advance(cx, 100), 0.0, 0, MotionStatus::Finished);
1380 }
1381
1382 #[gpui::test]
1383 fn a_change_to_a_step_not_being_played_does_not_restart_the_sequence(cx: &mut TestAppContext) {
1384 let fixture = SequenceFixture::open(
1385 cx,
1386 vec![
1387 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1388 SequenceStep::new(20.0, SequenceFixture::linear(100)),
1389 ],
1390 );
1391 fixture.advance(cx, 50);
1392 *fixture.steps.borrow_mut() = vec![
1393 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1394 SequenceStep::new(30.0, SequenceFixture::linear(100)),
1395 ];
1396 assert_sample(fixture.render(cx), 5.0, 0, MotionStatus::Running);
1397 assert_sample(fixture.advance(cx, 100), 20.0, 1, MotionStatus::Running);
1398 }
1399
1400 struct SingleStepView {
1401 armed: Rc<Cell<bool>>,
1402 samples: Rc<RefCell<Vec<(SequenceSample<f32>, MotionValue<f32>)>>>,
1403 }
1404
1405 impl Render for SingleStepView {
1406 fn render(
1407 &mut self,
1408 window: &mut Window,
1409 cx: &mut gpui::Context<Self>,
1410 ) -> impl IntoElement {
1411 let policy = Transition::new(Duration::from_millis(100))
1412 .delay(Duration::from_millis(20))
1413 .easing(Easing::EaseInOut);
1414 if !self.armed.get() {
1418 transition_with_status("plain", 0.0, policy, window, cx);
1419 return Empty;
1420 }
1421 let sequence = Sequence::new("single-step", 0.0)
1422 .with_step(10.0, policy.clone())
1423 .sample(window, cx);
1424 let plain = transition_with_status("plain", 10.0, policy, window, cx);
1425 self.samples.borrow_mut().push((sequence, plain));
1426 Empty
1427 }
1428 }
1429
1430 #[gpui::test]
1431 fn a_single_step_sequence_matches_a_plain_transition(cx: &mut TestAppContext) {
1432 let armed = Rc::new(Cell::new(false));
1433 let samples = Rc::new(RefCell::new(Vec::new()));
1434 let window = cx.open_window(size(px(100.), px(100.)), {
1435 let armed = armed.clone();
1436 let samples = samples.clone();
1437 move |_, _| SingleStepView { armed, samples }
1438 });
1439 cx.run_until_parked();
1440 armed.set(true);
1441 for millis in [0, 10, 30, 70, 120] {
1442 cx.executor().advance_clock(Duration::from_millis(millis));
1443 window.update(cx, |_, window, _| window.refresh()).unwrap();
1444 cx.run_until_parked();
1445 let (sequence, plain) = *samples.borrow().last().unwrap();
1446 assert_eq!(*sequence.value(), plain.value, "value after {millis} ms");
1447 assert_eq!(sequence.status(), plain.status, "status after {millis} ms");
1448 assert_eq!(sequence.step(), 0);
1449 }
1450 }
1451
1452 #[gpui::test]
1453 fn an_empty_sequence_idles_at_its_starting_value(cx: &mut TestAppContext) {
1454 let fixture = SequenceFixture::open(cx, Vec::new());
1455 assert_sample(fixture.last(), 0.0, 0, MotionStatus::Idle);
1456 assert_eq!(fixture.pending_frame(cx), 0);
1457 }
1458
1459 #[test]
1460 fn transition_ids_accept_element_like_scalars_and_named_channels() {
1461 assert_eq!(
1462 TransitionId::from("opacity"),
1463 TransitionId::from(ElementId::from("opacity"))
1464 );
1465 assert_ne!(
1466 TransitionId::from(("terms", "fill")),
1467 TransitionId::from(("terms", "mark-opacity"))
1468 );
1469 let _: TransitionId = 7usize.into();
1470 let _: TransitionId = 7i32.into();
1471 }
1472
1473 struct TestView {
1474 target: Rc<Cell<f32>>,
1475 duration: Duration,
1476 samples: Rc<RefCell<Vec<f32>>>,
1477 }
1478
1479 impl Render for TestView {
1480 fn render(
1481 &mut self,
1482 window: &mut Window,
1483 cx: &mut gpui::Context<Self>,
1484 ) -> impl IntoElement {
1485 self.samples.borrow_mut().push(transition(
1486 ("test", "value"),
1487 self.target.get(),
1488 Transition::new(self.duration).ease(|t| t),
1489 window,
1490 cx,
1491 ));
1492 Empty
1493 }
1494 }
1495
1496 struct DelayedView {
1497 target: Rc<Cell<f32>>,
1498 samples: Rc<RefCell<Vec<f32>>>,
1499 }
1500
1501 impl Render for DelayedView {
1502 fn render(
1503 &mut self,
1504 window: &mut Window,
1505 cx: &mut gpui::Context<Self>,
1506 ) -> impl IntoElement {
1507 self.samples.borrow_mut().push(transition(
1508 ("delayed-test", "value"),
1509 self.target.get(),
1510 Transition::new(Duration::from_millis(100))
1511 .delay(Duration::from_millis(50))
1512 .ease(|t| t),
1513 window,
1514 cx,
1515 ));
1516 Empty
1517 }
1518 }
1519
1520 struct Fixture {
1521 window: WindowHandle<TestView>,
1522 target: Rc<Cell<f32>>,
1523 samples: Rc<RefCell<Vec<f32>>>,
1524 }
1525
1526 impl Fixture {
1527 fn open(cx: &mut TestAppContext, duration: Duration) -> Self {
1528 let target = Rc::new(Cell::new(0.0));
1529 let samples = Rc::new(RefCell::new(Vec::new()));
1530 let window = cx.open_window(size(px(100.), px(100.)), {
1531 let target = target.clone();
1532 let samples = samples.clone();
1533 move |_, _| TestView {
1534 target,
1535 duration,
1536 samples,
1537 }
1538 });
1539 cx.run_until_parked();
1540 Self {
1541 window,
1542 target,
1543 samples,
1544 }
1545 }
1546
1547 fn render(&self, cx: &mut TestAppContext, target: f32) -> f32 {
1548 self.target.set(target);
1549 self.window
1550 .update(cx, |_, window, _| window.refresh())
1551 .unwrap();
1552 cx.run_until_parked();
1553 *self.samples.borrow().last().unwrap()
1554 }
1555
1556 fn pending_frame(&self, cx: &mut TestAppContext) -> usize {
1557 self.window
1558 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
1559 .unwrap()
1560 }
1561 }
1562
1563 #[gpui::test]
1564 fn a_zero_duration_target_change_is_immediate(cx: &mut TestAppContext) {
1565 let fixture = Fixture::open(cx, Duration::ZERO);
1566 assert_eq!(fixture.render(cx, 1.0), 1.0);
1567 }
1568
1569 #[gpui::test]
1570 fn a_changed_target_transitions_over_time(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, 10.0), 5.0);
1577 }
1578
1579 #[gpui::test]
1580 fn requested_animation_frames_resample_without_manual_refresh(cx: &mut TestAppContext) {
1581 let duration = Duration::from_millis(100);
1582 let fixture = Fixture::open(cx, duration);
1583 assert_eq!(fixture.render(cx, 10.0), 0.0);
1584
1585 cx.executor().advance_clock(Duration::from_millis(50));
1586 assert_eq!(fixture.pending_frame(cx), 1);
1587 cx.run_until_parked();
1588
1589 assert_eq!(*fixture.samples.borrow().last().unwrap(), 5.0);
1590 }
1591
1592 #[gpui::test]
1593 fn reversing_uses_the_current_sample_and_shortens_the_return(cx: &mut TestAppContext) {
1594 let duration = Duration::from_millis(100);
1595 let fixture = Fixture::open(cx, duration);
1596 assert_eq!(fixture.render(cx, 10.0), 0.0);
1597
1598 cx.executor().advance_clock(Duration::from_millis(50));
1599 assert_eq!(fixture.render(cx, 0.0), 5.0);
1600 cx.executor().advance_clock(Duration::from_millis(25));
1601 assert_eq!(fixture.render(cx, 0.0), 2.5);
1602 }
1603
1604 #[gpui::test]
1605 fn delay_holds_the_previous_value_before_interpolation(cx: &mut TestAppContext) {
1606 let target = Rc::new(Cell::new(0.0));
1607 let samples = Rc::new(RefCell::new(Vec::new()));
1608 let window = cx.open_window(size(px(100.), px(100.)), {
1609 let target = target.clone();
1610 let samples = samples.clone();
1611 move |_, _| DelayedView { target, samples }
1612 });
1613 cx.run_until_parked();
1614
1615 target.set(10.0);
1616 window.update(cx, |_, window, _| window.refresh()).unwrap();
1617 cx.run_until_parked();
1618 assert_eq!(*samples.borrow().last().unwrap(), 0.0);
1619
1620 cx.executor().advance_clock(Duration::from_millis(50));
1621 window.update(cx, |_, window, _| window.refresh()).unwrap();
1622 cx.run_until_parked();
1623 assert_eq!(*samples.borrow().last().unwrap(), 0.0);
1624
1625 cx.executor().advance_clock(Duration::from_millis(50));
1626 window.update(cx, |_, window, _| window.refresh()).unwrap();
1627 cx.run_until_parked();
1628 assert_eq!(*samples.borrow().last().unwrap(), 5.0);
1629 }
1630
1631 #[gpui::test]
1632 fn a_completed_transition_stops_requesting_frames(cx: &mut TestAppContext) {
1633 let duration = Duration::from_millis(100);
1634 let fixture = Fixture::open(cx, duration);
1635 fixture.render(cx, 1.0);
1636 assert_eq!(fixture.pending_frame(cx), 1);
1637
1638 cx.executor().advance_clock(duration);
1639 assert_eq!(fixture.render(cx, 1.0), 1.0);
1640 fixture.pending_frame(cx);
1641 cx.run_until_parked();
1642 assert_eq!(fixture.pending_frame(cx), 0);
1643 }
1644
1645 #[gpui::test]
1646 fn reduced_motion_adopts_the_target_without_requesting_a_frame(cx: &mut TestAppContext) {
1647 cx.update(|cx| cx.set_reduce_motion(true));
1648 let duration = Duration::from_millis(100);
1649 let fixture = Fixture::open(cx, duration);
1650 assert_eq!(fixture.render(cx, 1.0), 1.0);
1651 assert_eq!(fixture.pending_frame(cx), 0);
1652 }
1653
1654 struct SpringView {
1655 target: Rc<Cell<f32>>,
1656 policy: Rc<Cell<Spring>>,
1657 samples: Rc<RefCell<Vec<f32>>>,
1658 }
1659
1660 impl Render for SpringView {
1661 fn render(
1662 &mut self,
1663 window: &mut Window,
1664 cx: &mut gpui::Context<Self>,
1665 ) -> impl IntoElement {
1666 self.samples.borrow_mut().push(spring(
1667 ("spring-test", "value"),
1668 self.target.get(),
1669 self.policy.get(),
1670 window,
1671 cx,
1672 ));
1673 Empty
1674 }
1675 }
1676
1677 struct SpringFixture {
1678 window: WindowHandle<SpringView>,
1679 target: Rc<Cell<f32>>,
1680 policy: Rc<Cell<Spring>>,
1681 samples: Rc<RefCell<Vec<f32>>>,
1682 }
1683
1684 impl SpringFixture {
1685 fn open(cx: &mut TestAppContext, policy: Spring) -> Self {
1686 let target = Rc::new(Cell::new(0.0));
1687 let policy = Rc::new(Cell::new(policy));
1688 let samples = Rc::new(RefCell::new(Vec::new()));
1689 let window = cx.open_window(size(px(100.), px(100.)), {
1690 let target = target.clone();
1691 let policy = policy.clone();
1692 let samples = samples.clone();
1693 move |_, _| SpringView {
1694 target,
1695 policy,
1696 samples,
1697 }
1698 });
1699 cx.run_until_parked();
1700 Self {
1701 window,
1702 target,
1703 policy,
1704 samples,
1705 }
1706 }
1707
1708 fn render(&self, cx: &mut TestAppContext, target: f32) -> f32 {
1709 self.target.set(target);
1710 self.window
1711 .update(cx, |_, window, _| window.refresh())
1712 .unwrap();
1713 cx.run_until_parked();
1714 *self.samples.borrow().last().unwrap()
1715 }
1716
1717 fn advance(&self, cx: &mut TestAppContext, millis: u64, target: f32) -> f32 {
1718 cx.executor().advance_clock(Duration::from_millis(millis));
1719 self.render(cx, target)
1720 }
1721
1722 fn pending_frame(&self, cx: &mut TestAppContext) -> usize {
1723 self.window
1724 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
1725 .unwrap()
1726 }
1727 }
1728
1729 #[gpui::test]
1730 fn a_spring_adopts_its_first_target_immediately(cx: &mut TestAppContext) {
1731 let fixture = SpringFixture::open(cx, Spring::new(Duration::from_millis(300)));
1732 assert_eq!(*fixture.samples.borrow().first().unwrap(), 0.0);
1733 }
1734
1735 #[gpui::test]
1736 fn a_spring_travels_toward_its_target_over_time(cx: &mut TestAppContext) {
1737 let fixture = SpringFixture::open(cx, Spring::new(Duration::from_millis(300)));
1738 assert_eq!(fixture.render(cx, 1.0), 0.0);
1739
1740 let early = fixture.advance(cx, 50, 1.0);
1741 let late = fixture.advance(cx, 50, 1.0);
1742 assert!(
1743 0.0 < early && early < late && late < 1.0,
1744 "expected monotonic approach, got {early} then {late}"
1745 );
1746 }
1747
1748 #[gpui::test]
1749 fn a_reversed_spring_keeps_its_momentum_before_turning_around(cx: &mut TestAppContext) {
1750 let fixture = SpringFixture::open(cx, Spring::new(Duration::from_millis(300)));
1751 fixture.render(cx, 1.0);
1752 let reversed_at = fixture.advance(cx, 100, 1.0);
1753
1754 assert_eq!(fixture.render(cx, 0.0), reversed_at);
1757
1758 let next = fixture.advance(cx, 16, 0.0);
1759 assert!(
1760 next > reversed_at,
1761 "expected the spring to carry its velocity past {reversed_at}, got {next}"
1762 );
1763
1764 assert_eq!(fixture.advance(cx, 1_000, 0.0), 0.0);
1765 }
1766
1767 #[gpui::test]
1768 fn a_bouncy_spring_overshoots_its_target(cx: &mut TestAppContext) {
1769 let fixture = SpringFixture::open(
1770 cx,
1771 Spring::new(Duration::from_millis(350)).with_damping(0.7),
1772 );
1773 fixture.render(cx, 1.0);
1774 for _ in 0..30 {
1775 fixture.advance(cx, 16, 1.0);
1776 }
1777
1778 let peak = fixture
1779 .samples
1780 .borrow()
1781 .iter()
1782 .copied()
1783 .fold(f32::MIN, f32::max);
1784 assert!(peak > 1.0, "expected an overshoot past 1.0, got {peak}");
1785 }
1786
1787 #[gpui::test]
1788 fn a_settled_spring_stops_requesting_frames(cx: &mut TestAppContext) {
1789 let fixture = SpringFixture::open(cx, Spring::new(Duration::from_millis(300)));
1790 fixture.render(cx, 1.0);
1791 assert_eq!(fixture.pending_frame(cx), 1);
1792
1793 assert_eq!(fixture.advance(cx, 2_000, 1.0), 1.0);
1794 fixture.pending_frame(cx);
1795 cx.run_until_parked();
1796 assert_eq!(fixture.pending_frame(cx), 0);
1797 }
1798
1799 #[gpui::test]
1800 fn a_spring_that_is_not_travelling_adopts_its_target_on_the_spot(cx: &mut TestAppContext) {
1801 let travelling = Spring::new(Duration::from_millis(300));
1802 let fixture = SpringFixture::open(cx, travelling.with_travel(false));
1803
1804 assert_eq!(fixture.render(cx, 1.0), 1.0);
1805 assert_eq!(fixture.pending_frame(cx), 0);
1806 assert_eq!(fixture.advance(cx, 100, 5.0), 5.0);
1807
1808 fixture.policy.set(travelling);
1811 assert_eq!(fixture.render(cx, 6.0), 5.0);
1812 let next = fixture.advance(cx, 50, 6.0);
1813 assert!(
1814 5.0 < next && next < 6.0,
1815 "expected travel to resume from 5.0, got {next}"
1816 );
1817 }
1818
1819 #[gpui::test]
1820 fn a_zero_response_spring_resolves_instead_of_dividing_by_its_period(cx: &mut TestAppContext) {
1821 let fixture = SpringFixture::open(cx, Spring::new(Duration::ZERO));
1822 assert_eq!(fixture.render(cx, 1.0), 1.0);
1823 assert_eq!(fixture.pending_frame(cx), 0);
1824 }
1825
1826 #[test]
1827 fn spring_rejects_non_finite_or_negative_physical_parameters() {
1828 let spring = Spring::new(Duration::from_millis(300));
1829
1830 assert_eq!(
1831 spring.try_with_damping(f32::NAN).unwrap_err(),
1832 SpringError::InvalidDamping
1833 );
1834 assert_eq!(
1835 spring.try_with_damping(-0.1).unwrap_err(),
1836 SpringError::InvalidDamping
1837 );
1838 assert_eq!(
1839 spring.try_with_epsilon(f32::INFINITY).unwrap_err(),
1840 SpringError::InvalidEpsilon
1841 );
1842 assert_eq!(
1843 spring.try_with_epsilon(-0.1).unwrap_err(),
1844 SpringError::InvalidEpsilon
1845 );
1846 }
1847
1848 #[test]
1849 fn spring_reports_its_unit_specific_settling_tolerance() {
1850 let normalized = Spring::new(Duration::from_millis(180));
1851 let pixels = Spring::new(Duration::from_millis(180)).with_epsilon(0.1);
1852
1853 assert!(normalized.epsilon() < 0.01);
1854 assert_eq!(pixels.epsilon(), 0.1);
1855 }
1856
1857 #[gpui::test]
1858 fn reduced_motion_adopts_the_spring_target_without_requesting_a_frame(cx: &mut TestAppContext) {
1859 cx.update(|cx| cx.set_reduce_motion(true));
1860 let fixture = SpringFixture::open(
1861 cx,
1862 Spring::new(Duration::from_millis(350)).with_damping(0.7),
1863 );
1864 assert_eq!(fixture.render(cx, 1.0), 1.0);
1865 assert_eq!(fixture.pending_frame(cx), 0);
1866 }
1867}