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