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