1#[cfg(not(target_family = "wasm"))]
2use std::time::Instant;
3use std::{cell::RefCell, 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, Copy, Debug, Default, Eq, PartialEq)]
108pub enum ReducedMotion {
109 #[default]
111 Finish,
112 Continue,
114}
115
116#[derive(Clone)]
122pub struct Transition {
123 duration: Duration,
124 delay: SignedDuration,
125 easing: Easing,
126 reduced_motion: ReducedMotion,
127}
128
129impl Transition {
130 pub fn new(duration: Duration) -> Self {
131 Self {
132 duration,
133 delay: SignedDuration::ZERO,
134 easing: Easing::Custom(Rc::new(ease_out_cubic)),
135 reduced_motion: ReducedMotion::Finish,
136 }
137 }
138
139 pub fn reduced_motion(mut self, behavior: ReducedMotion) -> Self {
146 self.reduced_motion = behavior;
147 self
148 }
149
150 pub fn delay(mut self, delay: impl Into<SignedDuration>) -> Self {
151 self.delay = delay.into();
152 self
153 }
154
155 pub fn ease(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
156 self.easing = Easing::Custom(Rc::new(easing));
157 self
158 }
159
160 pub fn easing(mut self, easing: Easing) -> Self {
161 self.easing = easing;
162 self
163 }
164
165 fn sample(&self, progress: f32) -> f32 {
166 self.easing.sample(progress)
167 }
168
169 pub(crate) fn curve(&self) -> &Easing {
172 &self.easing
173 }
174
175 fn progress(&self, elapsed: Duration, duration: Duration) -> (f32, MotionStatus) {
176 let Some(active_elapsed) = self.delay.active_elapsed(elapsed) else {
177 return (0.0, MotionStatus::Delayed);
178 };
179 if duration.is_zero() || active_elapsed >= duration {
180 return (1.0, MotionStatus::Finished);
181 }
182 (
183 active_elapsed.as_secs_f32() / duration.as_secs_f32(),
184 MotionStatus::Running,
185 )
186 }
187
188 fn finishes_after(&self) -> Duration {
191 match self.delay {
192 SignedDuration::Positive(delay) => delay.saturating_add(self.duration),
193 SignedDuration::Negative(delay) => self.duration.saturating_sub(delay),
194 }
195 }
196}
197
198impl From<Duration> for SignedDuration {
199 fn from(duration: Duration) -> Self {
200 Self::positive(duration)
201 }
202}
203
204#[derive(Clone, Debug, Eq, Hash, PartialEq)]
206pub struct TransitionId(ElementId);
207
208impl From<ElementId> for TransitionId {
209 fn from(id: ElementId) -> Self {
210 Self(id)
211 }
212}
213
214impl From<&'static str> for TransitionId {
215 fn from(id: &'static str) -> Self {
216 Self(id.into())
217 }
218}
219
220impl From<String> for TransitionId {
221 fn from(id: String) -> Self {
222 Self(id.into())
223 }
224}
225
226impl From<SharedString> for TransitionId {
227 fn from(id: SharedString) -> Self {
228 Self(id.into())
229 }
230}
231
232impl From<usize> for TransitionId {
233 fn from(id: usize) -> Self {
234 Self(id.into())
235 }
236}
237
238impl From<i32> for TransitionId {
239 fn from(id: i32) -> Self {
240 Self(id.into())
241 }
242}
243
244impl From<TransitionId> for ElementId {
245 fn from(id: TransitionId) -> Self {
246 ElementId::NamedChild(id.0.into(), "__base-transition-state".into())
247 }
248}
249
250impl<I, C> From<(I, C)> for TransitionId
251where
252 I: Into<ElementId>,
253 C: Into<SharedString>,
254{
255 fn from((id, channel): (I, C)) -> Self {
256 Self(ElementId::NamedChild(id.into().into(), channel.into()))
257 }
258}
259
260#[derive(Clone)]
261struct ValueTransition<T> {
262 from: T,
263 reversing_adjusted_start: T,
266 policy: Transition,
269 target: T,
270 started_at: Instant,
271 reversing_factor: f32,
272 duration: Duration,
273}
274
275#[derive(Clone, Copy, Debug, Eq, PartialEq)]
276pub enum MotionStatus {
277 Idle,
278 Delayed,
279 Running,
280 Finished,
281}
282
283#[derive(Clone, Copy, Debug, PartialEq)]
284pub struct MotionValue<T> {
285 pub value: T,
286 pub status: MotionStatus,
287}
288
289#[derive(Clone)]
304pub struct TransitionState<T> {
305 state: Rc<RefCell<Option<ValueTransition<T>>>>,
306}
307
308impl<T> Default for TransitionState<T> {
309 fn default() -> Self {
310 Self {
311 state: Rc::new(RefCell::new(None)),
312 }
313 }
314}
315
316impl<T: Interpolate + PartialEq + 'static> TransitionState<T> {
317 pub fn sample(
324 &self,
325 target: T,
326 policy: Transition,
327 window: &mut Window,
328 cx: &mut App,
329 ) -> MotionValue<T> {
330 let now = cx.background_executor().now();
331 let mut state = self.state.borrow_mut();
332 let state = state.get_or_insert_with(|| ValueTransition {
333 from: target.clone(),
334 reversing_adjusted_start: target.clone(),
335 policy: policy.clone(),
336 target: target.clone(),
337 started_at: now,
338 reversing_factor: 1.0,
339 duration: policy.duration,
340 });
341 let snapshot = state.clone();
342 let sampled = sample_transition(
343 &snapshot,
344 target,
345 policy,
346 now,
347 cx.reduce_motion(),
348 |update| update(state),
349 );
350 if matches!(
351 sampled.status,
352 MotionStatus::Delayed | MotionStatus::Running
353 ) {
354 window.request_animation_frame();
355 }
356 sampled
357 }
358}
359
360pub fn transition<T>(
369 id: impl Into<TransitionId>,
370 target: T,
371 policy: Transition,
372 window: &mut Window,
373 cx: &mut App,
374) -> T
375where
376 T: Interpolate + PartialEq + 'static,
377{
378 transition_with_status(id, target, policy, window, cx).value
379}
380
381pub fn transition_with_status<T>(
382 id: impl Into<TransitionId>,
383 target: T,
384 policy: Transition,
385 window: &mut Window,
386 cx: &mut App,
387) -> MotionValue<T>
388where
389 T: Interpolate + PartialEq + 'static,
390{
391 let id: ElementId = id.into().into();
392 let now = cx.background_executor().now();
393 let state = window.use_keyed_state(id, cx, |_, _| ValueTransition {
394 from: target.clone(),
395 reversing_adjusted_start: target.clone(),
396 policy: policy.clone(),
397 target: target.clone(),
398 started_at: now,
399 reversing_factor: 1.0,
400 duration: policy.duration,
401 });
402
403 let snapshot = state.read(cx).clone();
404 let reduced = cx.reduce_motion();
405 let sampled = sample_transition(&snapshot, target, policy, now, reduced, |update| {
406 state.update(cx, |state, _| update(state));
407 });
408 if matches!(
409 sampled.status,
410 MotionStatus::Delayed | MotionStatus::Running
411 ) {
412 window.request_animation_frame();
413 }
414 sampled
415}
416
417fn sample_transition<T>(
418 snapshot: &ValueTransition<T>,
419 target: T,
420 policy: Transition,
421 now: Instant,
422 reduced_motion: bool,
423 mut update: impl FnMut(&mut dyn FnMut(&mut ValueTransition<T>)),
424) -> MotionValue<T>
425where
426 T: Interpolate + PartialEq + 'static,
427{
428 if (reduced_motion && policy.reduced_motion == ReducedMotion::Finish)
429 || policy.duration.is_zero()
430 {
431 if snapshot.from != target || snapshot.target != target {
432 update(&mut |state| {
433 state.from = target.clone();
434 state.reversing_adjusted_start = target.clone();
435 state.policy = policy.clone();
436 state.target = target.clone();
437 state.started_at = now;
438 state.reversing_factor = 1.0;
439 state.duration = policy.duration;
440 });
441 }
442 return MotionValue {
443 value: target,
444 status: MotionStatus::Finished,
445 };
446 }
447
448 let elapsed = now.saturating_duration_since(snapshot.started_at);
449 let (progress, status) = snapshot.policy.progress(elapsed, snapshot.duration);
450 let sampled = snapshot
451 .from
452 .interpolate(&snapshot.target, snapshot.policy.sample(progress));
453
454 let (value, status) = if snapshot.target != target {
455 let reversing = matches!(status, MotionStatus::Running | MotionStatus::Delayed)
458 && target == snapshot.reversing_adjusted_start;
459 let reversing_factor = if reversing {
460 (snapshot.policy.sample(progress) * snapshot.reversing_factor
461 + (1.0 - snapshot.reversing_factor))
462 .abs()
463 .clamp(0.0, 1.0)
464 } else {
465 1.0
466 };
467 let duration = policy.duration.mul_f64(f64::from(reversing_factor));
468 let mut active_policy = policy.clone();
470 if reversing && let SignedDuration::Negative(delay) = policy.delay {
471 active_policy.delay =
472 SignedDuration::negative(delay.mul_f64(f64::from(reversing_factor)));
473 }
474 update(&mut |state| {
475 state.reversing_adjusted_start = if reversing {
476 snapshot.target.clone()
477 } else {
478 sampled.clone()
479 };
480 state.policy = active_policy.clone();
481 state.from = sampled.clone();
482 state.target = target.clone();
483 state.started_at = now;
484 state.reversing_factor = reversing_factor;
485 state.duration = duration;
486 });
487 let (initial_progress, initial_status) = active_policy.progress(Duration::ZERO, duration);
488 (
489 sampled.interpolate(&target, active_policy.sample(initial_progress)),
490 initial_status,
491 )
492 } else {
493 (
494 sampled,
495 if snapshot.from == snapshot.target {
496 MotionStatus::Idle
497 } else {
498 status
499 },
500 )
501 };
502 MotionValue { value, status }
503}
504
505#[derive(Clone, Copy)]
506struct KeyframePlayback {
507 started_at: Instant,
508}
509
510pub fn animate_keyframes<T>(
517 id: impl Into<TransitionId>,
518 keyframes: &Keyframes<T>,
519 timing: Timing,
520 window: &mut Window,
521 cx: &mut App,
522) -> MotionValue<T>
523where
524 T: Interpolate + 'static,
525{
526 let id: TransitionId = id.into();
527 let id = ElementId::NamedChild(ElementId::from(id).into(), "__keyframes".into());
528 let now = cx.background_executor().now();
529 let state = window.use_keyed_state(id, cx, |_, _| KeyframePlayback { started_at: now });
530 let started_at = state.read(cx).started_at;
531
532 if cx.reduce_motion() {
533 return MotionValue {
534 value: keyframes.sample(1.0),
535 status: MotionStatus::Finished,
536 };
537 }
538
539 let sample = timing.sample(now.saturating_duration_since(started_at));
540 let status = match sample.phase {
541 MotionPhase::Before => MotionStatus::Delayed,
542 MotionPhase::Active => MotionStatus::Running,
543 MotionPhase::After => MotionStatus::Finished,
544 };
545 if matches!(status, MotionStatus::Delayed | MotionStatus::Running) {
546 window.request_animation_frame();
547 }
548 MotionValue {
549 value: keyframes.sample(sample.directed_progress),
550 status,
551 }
552}
553
554#[derive(Clone, Copy, Debug)]
563pub struct Spring {
564 response: Duration,
565 damping: f32,
566 epsilon: f32,
567 travel: bool,
568}
569
570#[derive(Clone, Copy, Debug, Eq, PartialEq)]
572pub enum SpringError {
573 InvalidDamping,
574 InvalidEpsilon,
575}
576
577impl std::fmt::Display for SpringError {
578 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
579 match self {
580 Self::InvalidDamping => f.write_str("spring damping must be finite and non-negative"),
581 Self::InvalidEpsilon => {
582 f.write_str("spring epsilon must be finite and greater than zero")
583 }
584 }
585 }
586}
587
588impl std::error::Error for SpringError {}
589
590impl Spring {
591 pub const fn new(response: Duration) -> Self {
606 Self {
607 response,
608 damping: 1.0,
609 epsilon: DEFAULT_SPRING_EPSILON,
610 travel: true,
611 }
612 }
613
614 pub const fn with_damping(self, ratio: f32) -> Self {
629 match self.try_with_damping(ratio) {
630 Ok(spring) => spring,
631 Err(_) => panic!("spring damping must be finite and non-negative"),
632 }
633 }
634
635 pub const fn try_with_damping(mut self, ratio: f32) -> Result<Self, SpringError> {
637 if !ratio.is_finite() || ratio < 0.0 {
638 return Err(SpringError::InvalidDamping);
639 }
640 self.damping = ratio;
641 Ok(self)
642 }
643
644 pub const fn with_travel(mut self, travel: bool) -> Self {
657 self.travel = travel;
658 self
659 }
660
661 pub const fn with_epsilon(self, epsilon: f32) -> Self {
673 match self.try_with_epsilon(epsilon) {
674 Ok(spring) => spring,
675 Err(_) => panic!("spring epsilon must be finite and greater than zero"),
676 }
677 }
678
679 pub const fn try_with_epsilon(mut self, epsilon: f32) -> Result<Self, SpringError> {
681 if !epsilon.is_finite() || epsilon <= 0.0 {
682 return Err(SpringError::InvalidEpsilon);
683 }
684 self.epsilon = epsilon;
685 Ok(self)
686 }
687
688 pub const fn epsilon(self) -> f32 {
690 self.epsilon
691 }
692
693 fn config(&self) -> SpringConfig {
700 let frequency = std::f32::consts::TAU / self.response.as_secs_f32();
701 SpringConfig::new(frequency * frequency, 2.0 * self.damping * frequency, 1.0)
702 }
703}
704
705#[derive(Clone, Copy)]
706struct SpringTransition {
707 state: SpringState,
708 target: f32,
709 updated_at: Instant,
710}
711
712pub fn spring<T>(
722 id: impl Into<TransitionId>,
723 target: T,
724 policy: Spring,
725 window: &mut Window,
726 cx: &mut App,
727) -> T::Output
728where
729 T: SpringTarget,
730{
731 let id: ElementId = id.into().into();
732 let now = cx.background_executor().now();
733 let target_position = target.target();
734 let state = window.use_keyed_state(id, cx, |_, _| SpringTransition {
735 state: SpringState {
736 position: target_position,
737 velocity: 0.0,
738 },
739 target: target_position,
740 updated_at: now,
741 });
742
743 let snapshot = *state.read(cx);
744 let at_rest_on_target =
745 snapshot.state.position == target_position && snapshot.state.velocity == 0.0;
746
747 if at_rest_on_target {
758 return target.resolve(target_position);
759 }
760
761 let settle = |state: &mut SpringTransition| {
762 state.state = SpringState {
763 position: target_position,
764 velocity: 0.0,
765 };
766 state.target = target_position;
767 state.updated_at = now;
768 };
769
770 if cx.reduce_motion() || !policy.travel || policy.response.is_zero() {
771 state.update(cx, |state, _| settle(state));
772 return target.resolve(target_position);
773 }
774
775 let elapsed = now
778 .saturating_duration_since(snapshot.updated_at)
779 .as_secs_f32();
780 let config = policy.config();
781 let stepped = config.step(snapshot.state, snapshot.target, elapsed);
782
783 if config.is_settled(stepped, target_position, policy.epsilon) {
784 state.update(cx, |state, _| settle(state));
785 return target.resolve(target_position);
786 }
787
788 state.update(cx, |state, _| {
789 state.state = stepped;
790 state.target = target_position;
791 state.updated_at = now;
792 });
793 window.request_animation_frame();
794 target.resolve(stepped.position)
795}
796
797#[cfg(test)]
798mod css_timing_tests {
799 use super::{
800 Easing, IterationCount, LinearStop, MotionPhase, PlaybackDirection, SignedDuration,
801 StepPosition, Timing,
802 };
803 use std::{num::NonZeroU32, sync::Arc, time::Duration};
804
805 #[test]
806 fn css_keyword_easing_matches_published_reference_samples() {
807 for (easing, samples) in [
808 (Easing::Ease, [(0.2, 0.295), (0.5, 0.802), (0.8, 0.976)]),
809 (Easing::EaseIn, [(0.2, 0.062), (0.5, 0.315), (0.8, 0.692)]),
810 (Easing::EaseOut, [(0.2, 0.308), (0.5, 0.685), (0.8, 0.938)]),
811 (Easing::EaseInOut, [(0.2, 0.082), (0.5, 0.5), (0.8, 0.918)]),
812 ] {
813 for (progress, expected) in samples {
814 let actual = easing.sample(progress);
815 assert!(
816 (actual - expected).abs() < 0.002,
817 "{easing:?}({progress}) = {actual}, expected {expected}"
818 );
819 }
820 }
821 }
822
823 #[test]
824 fn cubic_bezier_easing_keeps_anticipation_and_overshoot() {
825 let anticipate = Easing::cubic_bezier(1.0, -0.4, 0.35, 0.95).unwrap();
826 let overshoot = Easing::cubic_bezier(0.175, 0.885, 0.32, 1.275).unwrap();
827
828 assert!(anticipate.sample(0.2) < 0.0);
829 assert!(overshoot.sample(0.8) > 1.0);
830 for easing in [anticipate, overshoot] {
831 assert_eq!(easing.sample(-1.0), 0.0);
832 assert_eq!(easing.sample(2.0), 1.0);
833 }
834 }
835
836 #[test]
837 fn step_easing_observes_css_jump_positions() {
838 let start = Easing::steps(4, StepPosition::JumpStart).unwrap();
839 let end = Easing::steps(4, StepPosition::JumpEnd).unwrap();
840
841 assert_eq!(start.sample(0.0), 0.25);
842 assert_eq!(start.sample(0.24), 0.25);
843 assert_eq!(start.sample(0.25), 0.5);
844 assert_eq!(end.sample(0.0), 0.0);
845 assert_eq!(end.sample(0.24), 0.0);
846 assert_eq!(end.sample(0.25), 0.25);
847 assert!(Easing::steps(0, StepPosition::JumpEnd).is_err());
848
849 let none = Easing::steps(4, StepPosition::JumpNone).unwrap();
850 let both = Easing::steps(4, StepPosition::JumpBoth).unwrap();
851 assert_eq!(none.sample(0.0), 0.0);
852 assert!((none.sample(0.5) - 2.0 / 3.0).abs() < f32::EPSILON);
853 assert_eq!(none.sample(1.0), 1.0);
854 assert_eq!(both.sample(0.0), 0.2);
855 assert_eq!(both.sample(1.0), 1.0);
856 assert!(Easing::steps(1, StepPosition::JumpNone).is_err());
857 }
858
859 #[test]
860 fn linear_stops_fill_omitted_positions_before_sampling() {
861 let easing = Easing::linear_stops([
862 LinearStop::at(0.0, 0.0),
863 LinearStop::new(0.2),
864 LinearStop::new(0.8),
865 LinearStop::at(1.0, 1.0),
866 ])
867 .unwrap();
868
869 assert!((easing.sample(1.0 / 3.0) - 0.2).abs() < 1e-6);
870 assert!((easing.sample(0.5) - 0.5).abs() < 1e-6);
871 assert!(
872 Easing::linear_stops([LinearStop::at(0.0, 0.8), LinearStop::at(1.0, 0.2)]).is_err()
873 );
874 }
875
876 #[test]
877 fn curves_built_without_stops_or_jumps_sample_linearly() {
878 let no_stops = Easing::LinearStops(Arc::from([]));
880 let no_jumps = Easing::Steps {
881 count: NonZeroU32::MIN,
882 position: StepPosition::JumpNone,
883 };
884
885 for (progress, expected) in [(-1.0, 0.0), (0.25, 0.25), (2.0, 1.0)] {
886 assert_eq!(
887 no_stops.sample(progress),
888 expected,
889 "no stops at {progress}"
890 );
891 assert_eq!(
892 no_jumps.sample(progress),
893 expected,
894 "no jumps at {progress}"
895 );
896 }
897 }
898
899 #[test]
900 fn negative_delay_starts_inside_the_active_interval() {
901 let timing = Timing::new(Duration::from_millis(100))
902 .delay(SignedDuration::negative(Duration::from_millis(25)));
903 let sample = timing.sample(Duration::ZERO);
904
905 assert_eq!(sample.phase, MotionPhase::Active);
906 assert!((sample.directed_progress - 0.25).abs() < f32::EPSILON);
907 assert!(sample.active);
908 assert!(!sample.finished);
909 }
910
911 #[test]
912 fn alternate_direction_reverses_odd_iterations() {
913 let timing = Timing::new(Duration::from_millis(100))
914 .iterations(IterationCount::Finite(2))
915 .direction(PlaybackDirection::Alternate)
916 .ease(Easing::Linear);
917
918 let first = timing.sample(Duration::from_millis(25));
919 let second = timing.sample(Duration::from_millis(125));
920 let finished = timing.sample(Duration::from_millis(200));
921
922 assert_eq!(first.iteration, 0);
923 assert_eq!(first.directed_progress, 0.25);
924 assert_eq!(second.iteration, 1);
925 assert_eq!(second.directed_progress, 0.75);
926 assert_eq!(finished.phase, MotionPhase::After);
927 assert_eq!(finished.directed_progress, 0.0);
928 assert!(finished.finished);
929 }
930}
931
932#[cfg(test)]
933mod motion_track_tests {
934 use super::{
935 Discrete, Easing, Interpolate as _, Keyframe, KeyframeError, Keyframes, MotionTransform,
936 Stagger, StaggerOrigin,
937 };
938 use gpui::{Bounds, Point, Size, point, px, size};
939 use std::time::Duration;
940
941 #[test]
942 fn keyframes_validate_offsets_and_sample_each_segments_easing() {
943 assert!(matches!(
944 Keyframes::try_new([Keyframe::new(0.2, 0.0_f32), Keyframe::new(1.0, 1.0_f32),]),
945 Err(KeyframeError::MissingEndpoint)
946 ));
947 assert!(matches!(
948 Keyframes::try_new([
949 Keyframe::new(0.0, 0.0_f32),
950 Keyframe::new(0.8, 1.0_f32),
951 Keyframe::new(0.7, 2.0_f32),
952 Keyframe::new(1.0, 3.0_f32),
953 ]),
954 Err(KeyframeError::OffsetsNotMonotonic)
955 ));
956
957 let track = Keyframes::try_new([
958 Keyframe::new(0.0, 0.0_f32)
959 .ease(Easing::steps(2, super::StepPosition::JumpEnd).unwrap()),
960 Keyframe::new(0.5, 10.0_f32).ease(Easing::Linear),
961 Keyframe::new(1.0, 20.0_f32),
962 ])
963 .unwrap();
964
965 assert_eq!(track.sample(0.2), 0.0);
966 assert_eq!(track.sample(0.3), 5.0);
967 assert_eq!(track.sample(0.75), 15.0);
968 assert_eq!(track.sample(1.0), 20.0);
969 }
970
971 #[test]
972 fn discrete_values_switch_only_at_the_requested_progress() {
973 let value = Discrete::new("old", "new").switch_at(0.75).unwrap();
974 assert_eq!(value.sample(0.749), "old");
975 assert_eq!(value.sample(0.75), "new");
976 assert!(Discrete::new(0, 1).switch_at(f32::NAN).is_err());
977 }
978
979 #[test]
980 fn stagger_origins_produce_stable_delays_without_allocating_a_schedule() {
981 let interval = Duration::from_millis(20);
982 let first = Stagger::new(interval, StaggerOrigin::First);
983 let last = Stagger::new(interval, StaggerOrigin::Last);
984 let center = Stagger::new(interval, StaggerOrigin::Center);
985
986 assert_eq!(first.delay(3, 5), Duration::from_millis(60));
987 assert_eq!(last.delay(3, 5), Duration::from_millis(20));
988 assert_eq!(center.delay(2, 5), Duration::ZERO);
989 assert_eq!(center.delay(0, 5), Duration::from_millis(40));
990 assert_eq!(first.delay(7, 0), Duration::ZERO);
991 }
992
993 #[test]
994 fn common_gpui_geometry_interpolates_channel_by_channel() {
995 let from_size = size(px(10.0), px(20.0));
996 let to_size = size(px(30.0), px(60.0));
997 assert_eq!(
998 from_size.interpolate(&to_size, 0.25),
999 size(px(15.0), px(30.0))
1000 );
1001
1002 let from = Bounds::new(point(px(0.0), px(10.0)), from_size);
1003 let to = Bounds::new(point(px(40.0), px(50.0)), to_size);
1004 assert_eq!(
1005 from.interpolate(&to, 0.5),
1006 Bounds::new(point(px(20.0), px(30.0)), size(px(20.0), px(40.0)))
1007 );
1008
1009 let _: Point<gpui::Pixels> = from.origin;
1010 let _: Size<gpui::Pixels> = from.size;
1011
1012 let transform = MotionTransform::identity().interpolate(
1013 &MotionTransform {
1014 translation: point(px(20.0), px(40.0)),
1015 scale: point(2.0, 0.5),
1016 rotation_radians: std::f32::consts::PI,
1017 opacity: 0.0,
1018 },
1019 0.5,
1020 );
1021 assert_eq!(transform.translation, point(px(10.0), px(20.0)));
1022 assert_eq!(transform.scale, point(1.5, 0.75));
1023 assert_eq!(transform.rotation_radians, std::f32::consts::FRAC_PI_2);
1024 assert_eq!(transform.opacity, 0.5);
1025 }
1026}
1027
1028#[cfg(test)]
1029mod tests {
1030 use std::{
1031 cell::{Cell, RefCell},
1032 rc::Rc,
1033 time::Duration,
1034 };
1035
1036 use gpui::{
1037 Empty, IntoElement, Render, TestAppContext, VisualTestContext, WindowHandle, px, size,
1038 };
1039
1040 use super::*;
1041
1042 struct StatusView {
1043 target: Rc<Cell<f32>>,
1044 policy: Transition,
1045 samples: Rc<RefCell<Vec<MotionValue<f32>>>>,
1046 }
1047
1048 impl Render for StatusView {
1049 fn render(
1050 &mut self,
1051 window: &mut Window,
1052 cx: &mut gpui::Context<Self>,
1053 ) -> impl IntoElement {
1054 self.samples.borrow_mut().push(transition_with_status(
1055 ("status-test", "value"),
1056 self.target.get(),
1057 self.policy.clone(),
1058 window,
1059 cx,
1060 ));
1061 Empty
1062 }
1063 }
1064
1065 struct StatusFixture {
1066 window: WindowHandle<StatusView>,
1067 target: Rc<Cell<f32>>,
1068 samples: Rc<RefCell<Vec<MotionValue<f32>>>>,
1069 }
1070
1071 impl StatusFixture {
1072 fn open(cx: &mut TestAppContext, policy: Transition) -> Self {
1073 let target = Rc::new(Cell::new(0.0));
1074 let samples = Rc::new(RefCell::new(Vec::new()));
1075 let window = cx.open_window(size(px(100.), px(100.)), {
1076 let target = target.clone();
1077 let samples = samples.clone();
1078 move |_, _| StatusView {
1079 target,
1080 policy,
1081 samples,
1082 }
1083 });
1084 cx.run_until_parked();
1085 Self {
1086 window,
1087 target,
1088 samples,
1089 }
1090 }
1091
1092 fn render(&self, cx: &mut TestAppContext, target: f32) -> MotionValue<f32> {
1093 self.target.set(target);
1094 self.window
1095 .update(cx, |_, window, _| window.refresh())
1096 .unwrap();
1097 cx.run_until_parked();
1098 *self.samples.borrow().last().unwrap()
1099 }
1100 }
1101
1102 #[gpui::test]
1103 fn status_transition_reports_delay_running_and_finished(cx: &mut TestAppContext) {
1104 let fixture = StatusFixture::open(
1105 cx,
1106 Transition::new(Duration::from_millis(100)).delay(Duration::from_millis(20)),
1107 );
1108 assert_eq!(fixture.render(cx, 1.0).status, MotionStatus::Delayed);
1109
1110 cx.executor().advance_clock(Duration::from_millis(20));
1111 assert_eq!(fixture.render(cx, 1.0).status, MotionStatus::Running);
1112 cx.executor().advance_clock(Duration::from_millis(100));
1113 assert_eq!(fixture.render(cx, 1.0).status, MotionStatus::Finished);
1114 }
1115
1116 #[gpui::test]
1117 fn negative_delay_samples_a_target_change_inside_its_interval(cx: &mut TestAppContext) {
1118 let fixture = StatusFixture::open(
1119 cx,
1120 Transition::new(Duration::from_millis(100))
1121 .delay(SignedDuration::negative(Duration::from_millis(25)))
1122 .ease(|t| t),
1123 );
1124 let sample = fixture.render(cx, 1.0);
1125 assert_eq!(sample.status, MotionStatus::Running);
1126 assert_eq!(sample.value, 0.25);
1127 }
1128
1129 #[gpui::test]
1130 fn a_direct_reversal_shortens_the_return_transition(cx: &mut TestAppContext) {
1131 let fixture =
1132 StatusFixture::open(cx, Transition::new(Duration::from_millis(100)).ease(|t| t));
1133 assert_eq!(fixture.render(cx, 1.0).value, 0.0);
1134 cx.executor().advance_clock(Duration::from_millis(50));
1135 assert_eq!(fixture.render(cx, 0.0).value, 0.5);
1136 cx.executor().advance_clock(Duration::from_millis(25));
1137 assert_eq!(fixture.render(cx, 0.0).value, 0.25);
1138 }
1139
1140 #[gpui::test]
1141 fn repeated_reversals_keep_the_logical_start(cx: &mut TestAppContext) {
1142 let fixture =
1143 StatusFixture::open(cx, Transition::new(Duration::from_millis(1000)).ease(|t| t));
1144 fixture.render(cx, 1.0);
1145 cx.executor().advance_clock(Duration::from_millis(400));
1146 assert!((fixture.render(cx, 0.0).value - 0.4).abs() < 1e-6);
1147 cx.executor().advance_clock(Duration::from_millis(100));
1148 assert!((fixture.render(cx, 1.0).value - 0.3).abs() < 1e-6);
1149 cx.executor().advance_clock(Duration::from_millis(100));
1150 assert!((fixture.render(cx, 0.0).value - 0.4).abs() < 1e-6);
1151 cx.executor().advance_clock(Duration::from_millis(401));
1152 assert_eq!(fixture.render(cx, 0.0).status, MotionStatus::Finished);
1153 assert_eq!(fixture.render(cx, 0.0).value, 0.0);
1154 fixture.render(cx, 1.0);
1155 cx.executor().advance_clock(Duration::from_millis(500));
1156 assert!((fixture.render(cx, 1.0).value - 0.5).abs() < 1e-6);
1157 }
1158
1159 #[gpui::test]
1160 fn reversal_scales_negative_delay(cx: &mut TestAppContext) {
1161 let fixture = StatusFixture::open(
1162 cx,
1163 Transition::new(Duration::from_millis(1000))
1164 .delay(SignedDuration::negative(Duration::from_millis(200)))
1165 .ease(|t| t),
1166 );
1167 assert_eq!(fixture.render(cx, 1.0).value, 0.2);
1168 cx.executor().advance_clock(Duration::from_millis(200));
1169 assert!((fixture.render(cx, 0.0).value - 0.32).abs() < 1e-6);
1171 cx.executor().advance_clock(Duration::from_millis(321));
1172 assert_eq!(fixture.render(cx, 0.0).status, MotionStatus::Finished);
1173 }
1174
1175 #[gpui::test]
1176 fn reversal_uses_absolute_shortening_for_anticipation(cx: &mut TestAppContext) {
1177 let fixture = StatusFixture::open(
1178 cx,
1179 Transition::new(Duration::from_millis(1000)).ease(|t| if t < 0.5 { -t } else { t }),
1180 );
1181 fixture.render(cx, 1.0);
1182 cx.executor().advance_clock(Duration::from_millis(250));
1183 assert_eq!(fixture.render(cx, 0.0).status, MotionStatus::Running);
1184 cx.executor().advance_clock(Duration::from_millis(250));
1185 assert_eq!(fixture.render(cx, 0.0).status, MotionStatus::Finished);
1186 }
1187
1188 #[gpui::test]
1189 fn non_reversing_retarget_uses_full_duration(cx: &mut TestAppContext) {
1190 let fixture =
1191 StatusFixture::open(cx, Transition::new(Duration::from_millis(1000)).ease(|t| t));
1192 fixture.render(cx, 1.0);
1193 cx.executor().advance_clock(Duration::from_millis(400));
1194 fixture.render(cx, 0.0);
1195 cx.executor().advance_clock(Duration::from_millis(100));
1196 fixture.render(cx, 2.0);
1197 cx.executor().advance_clock(Duration::from_millis(500));
1198 assert!((fixture.render(cx, 2.0).value - 1.15).abs() < 1e-6);
1199 }
1200
1201 #[gpui::test]
1202 fn active_policy_is_retained_until_retarget(cx: &mut TestAppContext) {
1203 let fixture =
1204 StatusFixture::open(cx, Transition::new(Duration::from_millis(1000)).ease(|t| t));
1205 fixture.render(cx, 1.0);
1206 cx.executor().advance_clock(Duration::from_millis(400));
1207 fixture
1208 .window
1209 .update(cx, |view, _, _| {
1210 view.policy = Transition::new(Duration::from_millis(1000))
1211 .ease(|t| t * t)
1212 .delay(SignedDuration::negative(Duration::from_millis(200)));
1213 })
1214 .unwrap();
1215 assert!((fixture.render(cx, 1.0).value - 0.4).abs() < 1e-6);
1216 assert!((fixture.render(cx, 2.0).value - 0.464).abs() < 1e-6);
1218 }
1219
1220 #[gpui::test]
1221 fn reversal_keeps_positive_delay(cx: &mut TestAppContext) {
1222 let fixture = StatusFixture::open(
1223 cx,
1224 Transition::new(Duration::from_millis(1000))
1225 .delay(Duration::from_millis(200))
1226 .ease(|t| t),
1227 );
1228 fixture.render(cx, 1.0);
1229 cx.executor().advance_clock(Duration::from_millis(600));
1230 assert!((fixture.render(cx, 0.0).value - 0.4).abs() < 1e-6);
1231 cx.executor().advance_clock(Duration::from_millis(100));
1232 assert_eq!(fixture.render(cx, 0.0).status, MotionStatus::Delayed);
1233 cx.executor().advance_clock(Duration::from_millis(501));
1234 assert_eq!(fixture.render(cx, 0.0).status, MotionStatus::Finished);
1235 }
1236
1237 #[gpui::test]
1238 fn effects_opt_in_keeps_timing_under_reduced_motion(cx: &mut TestAppContext) {
1239 let fixture = StatusFixture::open(
1240 cx,
1241 Transition::new(Duration::from_millis(1000))
1242 .ease(|t| t)
1243 .reduced_motion(ReducedMotion::Continue),
1244 );
1245 fixture.render(cx, 1.0);
1246 cx.executor().advance_clock(Duration::from_millis(200));
1247 cx.update(|cx| cx.set_reduce_motion(true));
1248 assert!((fixture.render(cx, 1.0).value - 0.2).abs() < 1e-6);
1249 cx.update(|cx| assert!(cx.reduce_motion()));
1250 cx.executor().advance_clock(Duration::from_millis(800));
1251 assert_eq!(fixture.render(cx, 1.0).status, MotionStatus::Finished);
1252 fixture.render(cx, 0.0);
1253 cx.executor().advance_clock(Duration::from_millis(500));
1254 assert!((fixture.render(cx, 0.0).value - 0.5).abs() < 1e-6);
1255 cx.update(|cx| cx.set_reduce_motion(false));
1256 assert!((fixture.render(cx, 0.0).value - 0.5).abs() < 1e-6);
1257 }
1258
1259 #[gpui::test]
1260 fn current_reduced_motion_policy_and_zero_duration_take_effect(cx: &mut TestAppContext) {
1261 cx.update(|cx| cx.set_reduce_motion(true));
1262 let fixture = StatusFixture::open(
1263 cx,
1264 Transition::new(Duration::from_millis(1000))
1265 .ease(|t| t)
1266 .reduced_motion(ReducedMotion::Continue),
1267 );
1268 fixture.render(cx, 1.0);
1269 cx.executor().advance_clock(Duration::from_millis(200));
1270 fixture
1271 .window
1272 .update(cx, |view, _, _| {
1273 view.policy = view.policy.clone().reduced_motion(ReducedMotion::Finish);
1274 })
1275 .unwrap();
1276 assert_eq!(fixture.render(cx, 1.0).value, 1.0);
1277 fixture
1278 .window
1279 .update(cx, |view, _, _| {
1280 view.policy = view.policy.clone().reduced_motion(ReducedMotion::Continue);
1281 })
1282 .unwrap();
1283 assert_eq!(fixture.render(cx, 0.0).value, 1.0);
1284 cx.executor().advance_clock(Duration::from_millis(200));
1285 assert!((fixture.render(cx, 0.0).value - 0.8).abs() < 1e-6);
1286 fixture
1287 .window
1288 .update(cx, |view, _, _| {
1289 view.policy =
1290 Transition::new(Duration::ZERO).reduced_motion(ReducedMotion::Continue);
1291 })
1292 .unwrap();
1293 assert_eq!(fixture.render(cx, 0.0).value, 0.0);
1294 }
1295
1296 struct PairView {
1297 target: f32,
1298 policy: Transition,
1299 state: TransitionState<f32>,
1300 samples: Rc<RefCell<Vec<(MotionValue<f32>, MotionValue<f32>)>>>,
1301 }
1302
1303 impl Render for PairView {
1304 fn render(
1305 &mut self,
1306 window: &mut Window,
1307 cx: &mut gpui::Context<Self>,
1308 ) -> impl IntoElement {
1309 let keyed = transition_with_status(
1310 ("pair-test", "value"),
1311 self.target,
1312 self.policy.clone(),
1313 window,
1314 cx,
1315 );
1316 let owned = self
1317 .state
1318 .sample(self.target, self.policy.clone(), window, cx);
1319 self.samples.borrow_mut().push((keyed, owned));
1320 Empty
1321 }
1322 }
1323
1324 #[gpui::test]
1325 fn transition_state_matches_keyed_transition_retarget_reverse_and_delay(
1326 cx: &mut TestAppContext,
1327 ) {
1328 for policy in [
1329 Transition::new(Duration::from_millis(100)).easing(Easing::Linear),
1330 Transition::new(Duration::from_millis(100))
1331 .easing(Easing::Ease)
1332 .delay(SignedDuration::negative(Duration::from_millis(10))),
1333 ] {
1334 let samples = Rc::new(RefCell::new(Vec::new()));
1335 let (view, cx) = cx.add_window_view(|_, _| PairView {
1336 target: 0.,
1337 policy,
1338 state: TransitionState::default(),
1339 samples: samples.clone(),
1340 });
1341 for (millis, target) in [(0, 1.), (40, 1.), (0, 0.), (20, 0.), (0, 1.), (100, 1.)] {
1342 cx.executor().advance_clock(Duration::from_millis(millis));
1343 cx.update(|window, cx| {
1344 view.update(cx, |view, _| view.target = target);
1345 window.refresh();
1346 window.draw(cx).clear(cx);
1347 });
1348 let (expected, actual) = *samples.borrow().last().unwrap();
1349 assert_eq!(actual.status, expected.status);
1350 assert!((actual.value - expected.value).abs() < 0.00001);
1351 }
1352 }
1353 }
1354
1355 struct PersistentTransitionView {
1356 state: TransitionState<f32>,
1357 policy: Transition,
1358 mounted: bool,
1359 target: f32,
1360 sampled: Rc<Cell<Option<MotionValue<f32>>>>,
1361 }
1362
1363 impl Render for PersistentTransitionView {
1364 fn render(
1365 &mut self,
1366 window: &mut Window,
1367 cx: &mut gpui::Context<Self>,
1368 ) -> impl IntoElement {
1369 if self.mounted {
1370 self.sampled.set(Some(self.state.sample(
1371 self.target,
1372 self.policy.clone(),
1373 window,
1374 cx,
1375 )));
1376 }
1377 Empty
1378 }
1379 }
1380
1381 struct Persistent {
1382 view: gpui::Entity<PersistentTransitionView>,
1383 sampled: Rc<Cell<Option<MotionValue<f32>>>>,
1384 }
1385
1386 impl Persistent {
1387 fn open(cx: &mut TestAppContext, policy: Transition) -> (Self, &mut VisualTestContext) {
1388 let sampled = Rc::new(Cell::new(None));
1389 let (view, cx) = cx.add_window_view(|_, _| PersistentTransitionView {
1390 state: TransitionState::default(),
1391 policy,
1392 mounted: true,
1393 target: 0.,
1394 sampled: sampled.clone(),
1395 });
1396 (Self { view, sampled }, cx)
1397 }
1398
1399 fn draw(
1402 &self,
1403 cx: &mut VisualTestContext,
1404 update: impl FnOnce(&mut PersistentTransitionView),
1405 ) -> (Option<MotionValue<f32>>, usize) {
1406 self.sampled.set(None);
1407 cx.update(|window, cx| {
1408 window.simulate_next_frame(cx);
1409 self.view.update(cx, |view, _| update(view));
1410 window.refresh();
1411 window.draw(cx).clear(cx);
1412 (self.sampled.get(), window.simulate_next_frame(cx))
1413 })
1414 }
1415 }
1416
1417 #[gpui::test]
1418 fn transition_state_requests_frames_only_while_delayed_or_running(cx: &mut TestAppContext) {
1419 let (fixture, cx) = Persistent::open(
1420 cx,
1421 Transition::new(Duration::from_millis(100)).delay(Duration::from_millis(20)),
1422 );
1423 let (adopted, frames) = fixture.draw(cx, |_| {});
1424 assert_eq!(adopted.unwrap().status, MotionStatus::Idle);
1425 assert_eq!(frames, 0, "the first sample adopts its target");
1426
1427 let (delayed, frames) = fixture.draw(cx, |view| view.target = 1.);
1428 assert_eq!(delayed.unwrap().status, MotionStatus::Delayed);
1429 assert!(frames > 0, "a delayed transition requests the next frame");
1430
1431 cx.executor().advance_clock(Duration::from_millis(70));
1432 let (running, frames) = fixture.draw(cx, |_| {});
1433 assert_eq!(running.unwrap().status, MotionStatus::Running);
1434 assert!(frames > 0, "a running transition requests the next frame");
1435
1436 cx.executor().advance_clock(Duration::from_millis(50));
1437 let (finished, frames) = fixture.draw(cx, |_| {});
1438 assert_eq!(finished.unwrap().status, MotionStatus::Finished);
1439 assert_eq!(frames, 0, "a settled transition requests no frame");
1440 }
1441
1442 #[gpui::test]
1443 fn transition_state_survives_unmount_and_reverses_on_remount(cx: &mut TestAppContext) {
1444 let (fixture, cx) = Persistent::open(
1445 cx,
1446 Transition::new(Duration::from_millis(100)).easing(Easing::Linear),
1447 );
1448 fixture.draw(cx, |view| view.target = 1.);
1449 cx.executor().advance_clock(Duration::from_millis(40));
1450 let (sample, _) = fixture.draw(cx, |_| {});
1451 assert!((sample.unwrap().value - 0.4).abs() < 0.00001);
1452
1453 let (sample, frames) = fixture.draw(cx, |view| view.mounted = false);
1454 assert_eq!(sample, None);
1455 assert_eq!(frames, 0, "an unmounted state requests no frames");
1456
1457 cx.executor().advance_clock(Duration::from_millis(30));
1460 fixture.draw(cx, |view| view.target = 0.);
1461 let (sample, frames) = fixture.draw(cx, |view| view.mounted = true);
1462 let sample = sample.unwrap();
1463 assert_eq!(sample.status, MotionStatus::Running);
1464 assert!((sample.value - 0.7).abs() < 0.00001);
1465 assert!(frames > 0);
1466
1467 cx.executor().advance_clock(Duration::from_millis(35));
1468 let (sample, _) = fixture.draw(cx, |_| {});
1469 assert!(
1470 (sample.unwrap().value - 0.35).abs() < 0.00001,
1471 "the 70ms shortened reversal continues after remount"
1472 );
1473 cx.executor().advance_clock(Duration::from_millis(35));
1474 let (sample, frames) = fixture.draw(cx, |_| {});
1475 assert_eq!(
1476 sample.unwrap(),
1477 MotionValue {
1478 value: 0.,
1479 status: MotionStatus::Finished
1480 }
1481 );
1482 assert_eq!(frames, 0);
1483 }
1484
1485 struct KeyframeView {
1486 track: Keyframes<f32>,
1487 timing: Timing,
1488 samples: Rc<RefCell<Vec<MotionValue<f32>>>>,
1489 }
1490
1491 impl Render for KeyframeView {
1492 fn render(
1493 &mut self,
1494 window: &mut Window,
1495 cx: &mut gpui::Context<Self>,
1496 ) -> impl IntoElement {
1497 self.samples.borrow_mut().push(animate_keyframes(
1498 "keyframe-test",
1499 &self.track,
1500 self.timing.clone(),
1501 window,
1502 cx,
1503 ));
1504 Empty
1505 }
1506 }
1507
1508 #[gpui::test]
1509 fn keyed_keyframes_follow_timing_and_stop_after_completion(cx: &mut TestAppContext) {
1510 let samples = Rc::new(RefCell::new(Vec::new()));
1511 let window = cx.open_window(size(px(100.), px(100.)), {
1512 let samples = samples.clone();
1513 move |_, _| KeyframeView {
1514 track: Keyframes::try_new([Keyframe::new(0.0, 0.0), Keyframe::new(1.0, 10.0)])
1515 .unwrap(),
1516 timing: Timing::new(Duration::from_millis(100)),
1517 samples,
1518 }
1519 });
1520 cx.run_until_parked();
1521 assert_eq!(samples.borrow().last().unwrap().value, 0.0);
1522 assert_eq!(
1523 samples.borrow().last().unwrap().status,
1524 MotionStatus::Running
1525 );
1526
1527 cx.executor().advance_clock(Duration::from_millis(50));
1528 assert_eq!(
1529 window
1530 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
1531 .unwrap(),
1532 1
1533 );
1534 cx.run_until_parked();
1535 assert_eq!(samples.borrow().last().unwrap().value, 5.0);
1536
1537 cx.executor().advance_clock(Duration::from_millis(50));
1538 window.update(cx, |_, window, _| window.refresh()).unwrap();
1539 cx.run_until_parked();
1540 assert_eq!(
1541 samples.borrow().last().unwrap().status,
1542 MotionStatus::Finished
1543 );
1544 window
1545 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
1546 .unwrap();
1547 cx.run_until_parked();
1548 assert_eq!(
1549 window
1550 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
1551 .unwrap(),
1552 0
1553 );
1554 }
1555
1556 struct PresenceView {
1557 present: Rc<Cell<bool>>,
1558 samples: Rc<RefCell<Vec<PresenceSample>>>,
1559 }
1560
1561 impl Render for PresenceView {
1562 fn render(
1563 &mut self,
1564 window: &mut Window,
1565 cx: &mut gpui::Context<Self>,
1566 ) -> impl IntoElement {
1567 self.samples.borrow_mut().push(
1568 Presence::new("presence-test", self.present.get())
1569 .transition(Transition::new(Duration::from_millis(100)).ease(|t| t))
1570 .sample(window, cx),
1571 );
1572 Empty
1573 }
1574 }
1575
1576 struct PresenceFixture {
1577 window: WindowHandle<PresenceView>,
1578 present: Rc<Cell<bool>>,
1579 samples: Rc<RefCell<Vec<PresenceSample>>>,
1580 }
1581
1582 impl PresenceFixture {
1583 fn open(cx: &mut TestAppContext, initially_present: bool) -> Self {
1584 let present = Rc::new(Cell::new(initially_present));
1585 let samples = Rc::new(RefCell::new(Vec::new()));
1586 let window = cx.open_window(size(px(100.), px(100.)), {
1587 let present = present.clone();
1588 let samples = samples.clone();
1589 move |_, _| PresenceView { present, samples }
1590 });
1591 cx.run_until_parked();
1592 Self {
1593 window,
1594 present,
1595 samples,
1596 }
1597 }
1598
1599 fn render(&self, cx: &mut TestAppContext, present: bool) -> PresenceSample {
1600 self.present.set(present);
1601 self.window
1602 .update(cx, |_, window, _| window.refresh())
1603 .unwrap();
1604 cx.run_until_parked();
1605 *self.samples.borrow().last().unwrap()
1606 }
1607 }
1608
1609 #[gpui::test]
1610 fn presence_enters_exits_and_only_unmounts_after_exit(cx: &mut TestAppContext) {
1611 let fixture = PresenceFixture::open(cx, true);
1612 let entering = *fixture.samples.borrow().last().unwrap();
1613 assert_eq!(entering.phase, PresencePhase::Entering);
1614 assert_eq!(entering.progress, 0.0);
1615 assert!(entering.should_render());
1616
1617 cx.executor().advance_clock(Duration::from_millis(100));
1618 let present = fixture.render(cx, true);
1619 assert_eq!(present.phase, PresencePhase::Present);
1620 assert_eq!(present.progress, 1.0);
1621
1622 let exiting = fixture.render(cx, false);
1623 assert_eq!(exiting.phase, PresencePhase::Exiting);
1624 assert_eq!(exiting.progress, 1.0);
1625 assert!(exiting.should_render());
1626
1627 cx.executor().advance_clock(Duration::from_millis(100));
1628 let absent = fixture.render(cx, false);
1629 assert_eq!(absent.phase, PresencePhase::Absent);
1630 assert_eq!(absent.progress, 0.0);
1631 assert!(!absent.should_render());
1632 }
1633
1634 #[gpui::test]
1635 fn presence_reentry_reverses_from_the_exit_sample(cx: &mut TestAppContext) {
1636 let fixture = PresenceFixture::open(cx, true);
1637 cx.executor().advance_clock(Duration::from_millis(100));
1638 fixture.render(cx, true);
1639 fixture.render(cx, false);
1640 cx.executor().advance_clock(Duration::from_millis(40));
1641 let reentering = fixture.render(cx, true);
1642
1643 assert_eq!(reentering.phase, PresencePhase::Entering);
1644 assert_eq!(reentering.progress, 0.6);
1645 }
1646
1647 #[gpui::test]
1648 fn reduced_motion_resolves_presence_without_a_pending_frame(cx: &mut TestAppContext) {
1649 cx.update(|cx| cx.set_reduce_motion(true));
1650 let fixture = PresenceFixture::open(cx, true);
1651 assert_eq!(
1652 fixture.samples.borrow().last().unwrap().phase,
1653 PresencePhase::Present
1654 );
1655 assert_eq!(
1656 fixture
1657 .window
1658 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
1659 .unwrap(),
1660 0
1661 );
1662 assert_eq!(fixture.render(cx, false).phase, PresencePhase::Absent);
1663 }
1664
1665 struct SequenceView {
1666 steps: Rc<RefCell<Vec<SequenceStep<f32>>>>,
1667 samples: Rc<RefCell<Vec<SequenceSample<f32>>>>,
1668 }
1669
1670 impl Render for SequenceView {
1671 fn render(
1672 &mut self,
1673 window: &mut Window,
1674 cx: &mut gpui::Context<Self>,
1675 ) -> impl IntoElement {
1676 self.samples.borrow_mut().push(
1677 Sequence::new("sequence-test", 0.0)
1678 .with_steps(self.steps.borrow().iter().cloned())
1679 .sample(window, cx),
1680 );
1681 Empty
1682 }
1683 }
1684
1685 struct SequenceFixture {
1686 window: WindowHandle<SequenceView>,
1687 steps: Rc<RefCell<Vec<SequenceStep<f32>>>>,
1688 samples: Rc<RefCell<Vec<SequenceSample<f32>>>>,
1689 }
1690
1691 impl SequenceFixture {
1692 fn open(cx: &mut TestAppContext, steps: Vec<SequenceStep<f32>>) -> Self {
1693 let steps = Rc::new(RefCell::new(steps));
1694 let samples = Rc::new(RefCell::new(Vec::new()));
1695 let window = cx.open_window(size(px(100.), px(100.)), {
1696 let steps = steps.clone();
1697 let samples = samples.clone();
1698 move |_, _| SequenceView { steps, samples }
1699 });
1700 cx.run_until_parked();
1701 Self {
1702 window,
1703 steps,
1704 samples,
1705 }
1706 }
1707
1708 fn linear(millis: u64) -> Transition {
1709 Transition::new(Duration::from_millis(millis)).ease(|t| t)
1710 }
1711
1712 fn last(&self) -> SequenceSample<f32> {
1713 *self.samples.borrow().last().unwrap()
1714 }
1715
1716 fn render(&self, cx: &mut TestAppContext) -> SequenceSample<f32> {
1717 self.window
1718 .update(cx, |_, window, _| window.refresh())
1719 .unwrap();
1720 cx.run_until_parked();
1721 self.last()
1722 }
1723
1724 fn advance(&self, cx: &mut TestAppContext, millis: u64) -> SequenceSample<f32> {
1725 cx.executor().advance_clock(Duration::from_millis(millis));
1726 self.render(cx)
1727 }
1728
1729 fn pending_frame(&self, cx: &mut TestAppContext) -> usize {
1730 self.window
1731 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
1732 .unwrap()
1733 }
1734 }
1735
1736 fn assert_sample(sample: SequenceSample<f32>, value: f32, step: usize, status: MotionStatus) {
1737 assert_eq!(
1738 (*sample.value(), sample.step(), sample.status()),
1739 (value, step, status)
1740 );
1741 }
1742
1743 #[gpui::test]
1744 fn sequence_steps_advance_in_order_at_their_boundaries(cx: &mut TestAppContext) {
1745 let fixture = SequenceFixture::open(
1746 cx,
1747 vec![
1748 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1749 SequenceStep::new(
1750 20.0,
1751 SequenceFixture::linear(100).delay(Duration::from_millis(50)),
1752 ),
1753 ],
1754 );
1755 assert_sample(fixture.last(), 0.0, 0, MotionStatus::Running);
1756 assert_sample(fixture.advance(cx, 50), 5.0, 0, MotionStatus::Running);
1757
1758 assert_sample(fixture.advance(cx, 50), 10.0, 1, MotionStatus::Delayed);
1761 assert_sample(fixture.advance(cx, 50), 10.0, 1, MotionStatus::Running);
1762 assert_sample(fixture.advance(cx, 50), 15.0, 1, MotionStatus::Running);
1763 assert_sample(fixture.advance(cx, 50), 20.0, 1, MotionStatus::Finished);
1764 }
1765
1766 #[gpui::test]
1767 fn a_sequence_starts_its_next_step_where_the_previous_ended_not_at_the_frame(
1768 cx: &mut TestAppContext,
1769 ) {
1770 let fixture = SequenceFixture::open(
1771 cx,
1772 vec![
1773 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1774 SequenceStep::new(20.0, SequenceFixture::linear(100)),
1775 ],
1776 );
1777 fixture.advance(cx, 50);
1780 assert_sample(fixture.advance(cx, 125), 17.5, 1, MotionStatus::Running);
1781 }
1782
1783 #[gpui::test]
1784 fn zero_duration_steps_complete_within_the_frame_that_reaches_them(cx: &mut TestAppContext) {
1785 let fixture = SequenceFixture::open(
1786 cx,
1787 vec![
1788 SequenceStep::new(5.0, SequenceFixture::linear(0)),
1789 SequenceStep::new(6.0, SequenceFixture::linear(0)),
1790 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1791 ],
1792 );
1793 assert_sample(fixture.last(), 6.0, 2, MotionStatus::Running);
1794 assert_sample(fixture.advance(cx, 50), 8.0, 2, MotionStatus::Running);
1795 }
1796
1797 #[gpui::test]
1798 fn a_sequence_reports_finished_once_and_then_stops_requesting_frames(cx: &mut TestAppContext) {
1799 let fixture = SequenceFixture::open(
1800 cx,
1801 vec![
1802 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1803 SequenceStep::new(20.0, SequenceFixture::linear(100)),
1804 ],
1805 );
1806 assert_eq!(fixture.pending_frame(cx), 1);
1807 cx.run_until_parked();
1808
1809 cx.executor().advance_clock(Duration::from_millis(100));
1810 assert_eq!(fixture.pending_frame(cx), 1);
1811 cx.run_until_parked();
1812 assert_eq!(fixture.last().status(), MotionStatus::Running);
1813 assert!(
1814 fixture
1815 .samples
1816 .borrow()
1817 .iter()
1818 .all(|sample| !sample.is_finished()),
1819 "the first step's end must not read as the sequence finishing"
1820 );
1821
1822 assert_sample(fixture.advance(cx, 100), 20.0, 1, MotionStatus::Finished);
1823 fixture.pending_frame(cx);
1824 cx.run_until_parked();
1825 assert_eq!(fixture.pending_frame(cx), 0);
1826 assert_sample(fixture.advance(cx, 1_000), 20.0, 1, MotionStatus::Finished);
1827 }
1828
1829 #[gpui::test]
1830 fn reduced_motion_adopts_a_sequences_last_target_without_requesting_a_frame(
1831 cx: &mut TestAppContext,
1832 ) {
1833 cx.update(|cx| cx.set_reduce_motion(true));
1834 let fixture = SequenceFixture::open(
1835 cx,
1836 vec![
1837 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1838 SequenceStep::new(20.0, SequenceFixture::linear(100)),
1839 ],
1840 );
1841 assert_sample(fixture.last(), 20.0, 1, MotionStatus::Finished);
1842 assert_eq!(fixture.pending_frame(cx), 0);
1843 }
1844
1845 #[gpui::test]
1846 fn a_changed_step_target_restarts_the_sequence_from_the_sampled_value(cx: &mut TestAppContext) {
1847 let fixture = SequenceFixture::open(
1848 cx,
1849 vec![
1850 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1851 SequenceStep::new(20.0, SequenceFixture::linear(100)),
1852 ],
1853 );
1854 fixture.advance(cx, 150);
1855 assert_sample(fixture.last(), 15.0, 1, MotionStatus::Running);
1856
1857 *fixture.steps.borrow_mut() = vec![
1858 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1859 SequenceStep::new(35.0, SequenceFixture::linear(100)),
1860 ];
1861 assert_sample(fixture.render(cx), 15.0, 0, MotionStatus::Running);
1862 assert_sample(fixture.advance(cx, 50), 12.5, 0, MotionStatus::Running);
1863 assert_sample(fixture.advance(cx, 100), 22.5, 1, MotionStatus::Running);
1864
1865 *fixture.steps.borrow_mut() = vec![SequenceStep::new(0.0, SequenceFixture::linear(100))];
1867 assert_sample(fixture.render(cx), 22.5, 0, MotionStatus::Running);
1868 assert_sample(fixture.advance(cx, 100), 0.0, 0, MotionStatus::Finished);
1869 }
1870
1871 #[gpui::test]
1872 fn a_change_to_a_step_not_being_played_does_not_restart_the_sequence(cx: &mut TestAppContext) {
1873 let fixture = SequenceFixture::open(
1874 cx,
1875 vec![
1876 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1877 SequenceStep::new(20.0, SequenceFixture::linear(100)),
1878 ],
1879 );
1880 fixture.advance(cx, 50);
1881 *fixture.steps.borrow_mut() = vec![
1882 SequenceStep::new(10.0, SequenceFixture::linear(100)),
1883 SequenceStep::new(30.0, SequenceFixture::linear(100)),
1884 ];
1885 assert_sample(fixture.render(cx), 5.0, 0, MotionStatus::Running);
1886 assert_sample(fixture.advance(cx, 100), 20.0, 1, MotionStatus::Running);
1887 }
1888
1889 struct SingleStepView {
1890 armed: Rc<Cell<bool>>,
1891 samples: Rc<RefCell<Vec<(SequenceSample<f32>, MotionValue<f32>)>>>,
1892 }
1893
1894 impl Render for SingleStepView {
1895 fn render(
1896 &mut self,
1897 window: &mut Window,
1898 cx: &mut gpui::Context<Self>,
1899 ) -> impl IntoElement {
1900 let policy = Transition::new(Duration::from_millis(100))
1901 .delay(Duration::from_millis(20))
1902 .easing(Easing::EaseInOut);
1903 if !self.armed.get() {
1907 transition_with_status("plain", 0.0, policy, window, cx);
1908 return Empty;
1909 }
1910 let sequence = Sequence::new("single-step", 0.0)
1911 .with_step(10.0, policy.clone())
1912 .sample(window, cx);
1913 let plain = transition_with_status("plain", 10.0, policy, window, cx);
1914 self.samples.borrow_mut().push((sequence, plain));
1915 Empty
1916 }
1917 }
1918
1919 #[gpui::test]
1920 fn a_single_step_sequence_matches_a_plain_transition(cx: &mut TestAppContext) {
1921 let armed = Rc::new(Cell::new(false));
1922 let samples = Rc::new(RefCell::new(Vec::new()));
1923 let window = cx.open_window(size(px(100.), px(100.)), {
1924 let armed = armed.clone();
1925 let samples = samples.clone();
1926 move |_, _| SingleStepView { armed, samples }
1927 });
1928 cx.run_until_parked();
1929 armed.set(true);
1930 for millis in [0, 10, 30, 70, 120] {
1931 cx.executor().advance_clock(Duration::from_millis(millis));
1932 window.update(cx, |_, window, _| window.refresh()).unwrap();
1933 cx.run_until_parked();
1934 let (sequence, plain) = *samples.borrow().last().unwrap();
1935 assert_eq!(*sequence.value(), plain.value, "value after {millis} ms");
1936 assert_eq!(sequence.status(), plain.status, "status after {millis} ms");
1937 assert_eq!(sequence.step(), 0);
1938 }
1939 }
1940
1941 #[gpui::test]
1942 fn an_empty_sequence_idles_at_its_starting_value(cx: &mut TestAppContext) {
1943 let fixture = SequenceFixture::open(cx, Vec::new());
1944 assert_sample(fixture.last(), 0.0, 0, MotionStatus::Idle);
1945 assert_eq!(fixture.pending_frame(cx), 0);
1946 }
1947
1948 #[test]
1949 fn transition_ids_accept_element_like_scalars_and_named_channels() {
1950 assert_eq!(
1951 TransitionId::from("opacity"),
1952 TransitionId::from(ElementId::from("opacity"))
1953 );
1954 assert_ne!(
1955 TransitionId::from(("terms", "fill")),
1956 TransitionId::from(("terms", "mark-opacity"))
1957 );
1958 let _: TransitionId = 7usize.into();
1959 let _: TransitionId = 7i32.into();
1960 }
1961
1962 struct TestView {
1963 target: Rc<Cell<f32>>,
1964 duration: Duration,
1965 samples: Rc<RefCell<Vec<f32>>>,
1966 }
1967
1968 impl Render for TestView {
1969 fn render(
1970 &mut self,
1971 window: &mut Window,
1972 cx: &mut gpui::Context<Self>,
1973 ) -> impl IntoElement {
1974 self.samples.borrow_mut().push(transition(
1975 ("test", "value"),
1976 self.target.get(),
1977 Transition::new(self.duration).ease(|t| t),
1978 window,
1979 cx,
1980 ));
1981 Empty
1982 }
1983 }
1984
1985 struct DelayedView {
1986 target: Rc<Cell<f32>>,
1987 samples: Rc<RefCell<Vec<f32>>>,
1988 }
1989
1990 impl Render for DelayedView {
1991 fn render(
1992 &mut self,
1993 window: &mut Window,
1994 cx: &mut gpui::Context<Self>,
1995 ) -> impl IntoElement {
1996 self.samples.borrow_mut().push(transition(
1997 ("delayed-test", "value"),
1998 self.target.get(),
1999 Transition::new(Duration::from_millis(100))
2000 .delay(Duration::from_millis(50))
2001 .ease(|t| t),
2002 window,
2003 cx,
2004 ));
2005 Empty
2006 }
2007 }
2008
2009 struct Fixture {
2010 window: WindowHandle<TestView>,
2011 target: Rc<Cell<f32>>,
2012 samples: Rc<RefCell<Vec<f32>>>,
2013 }
2014
2015 impl Fixture {
2016 fn open(cx: &mut TestAppContext, duration: Duration) -> Self {
2017 let target = Rc::new(Cell::new(0.0));
2018 let samples = Rc::new(RefCell::new(Vec::new()));
2019 let window = cx.open_window(size(px(100.), px(100.)), {
2020 let target = target.clone();
2021 let samples = samples.clone();
2022 move |_, _| TestView {
2023 target,
2024 duration,
2025 samples,
2026 }
2027 });
2028 cx.run_until_parked();
2029 Self {
2030 window,
2031 target,
2032 samples,
2033 }
2034 }
2035
2036 fn render(&self, cx: &mut TestAppContext, target: f32) -> f32 {
2037 self.target.set(target);
2038 self.window
2039 .update(cx, |_, window, _| window.refresh())
2040 .unwrap();
2041 cx.run_until_parked();
2042 *self.samples.borrow().last().unwrap()
2043 }
2044
2045 fn pending_frame(&self, cx: &mut TestAppContext) -> usize {
2046 self.window
2047 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
2048 .unwrap()
2049 }
2050 }
2051
2052 #[gpui::test]
2053 fn a_zero_duration_target_change_is_immediate(cx: &mut TestAppContext) {
2054 let fixture = Fixture::open(cx, Duration::ZERO);
2055 assert_eq!(fixture.render(cx, 1.0), 1.0);
2056 }
2057
2058 #[gpui::test]
2059 fn a_changed_target_transitions_over_time(cx: &mut TestAppContext) {
2060 let duration = Duration::from_millis(100);
2061 let fixture = Fixture::open(cx, duration);
2062 assert_eq!(fixture.render(cx, 10.0), 0.0);
2063
2064 cx.executor().advance_clock(Duration::from_millis(50));
2065 assert_eq!(fixture.render(cx, 10.0), 5.0);
2066 }
2067
2068 #[gpui::test]
2069 fn requested_animation_frames_resample_without_manual_refresh(cx: &mut TestAppContext) {
2070 let duration = Duration::from_millis(100);
2071 let fixture = Fixture::open(cx, duration);
2072 assert_eq!(fixture.render(cx, 10.0), 0.0);
2073
2074 cx.executor().advance_clock(Duration::from_millis(50));
2075 assert_eq!(fixture.pending_frame(cx), 1);
2076 cx.run_until_parked();
2077
2078 assert_eq!(*fixture.samples.borrow().last().unwrap(), 5.0);
2079 }
2080
2081 #[gpui::test]
2082 fn reversing_uses_the_current_sample_and_shortens_the_return(cx: &mut TestAppContext) {
2083 let duration = Duration::from_millis(100);
2084 let fixture = Fixture::open(cx, duration);
2085 assert_eq!(fixture.render(cx, 10.0), 0.0);
2086
2087 cx.executor().advance_clock(Duration::from_millis(50));
2088 assert_eq!(fixture.render(cx, 0.0), 5.0);
2089 cx.executor().advance_clock(Duration::from_millis(25));
2090 assert_eq!(fixture.render(cx, 0.0), 2.5);
2091 }
2092
2093 #[gpui::test]
2094 fn delay_holds_the_previous_value_before_interpolation(cx: &mut TestAppContext) {
2095 let target = Rc::new(Cell::new(0.0));
2096 let samples = Rc::new(RefCell::new(Vec::new()));
2097 let window = cx.open_window(size(px(100.), px(100.)), {
2098 let target = target.clone();
2099 let samples = samples.clone();
2100 move |_, _| DelayedView { target, samples }
2101 });
2102 cx.run_until_parked();
2103
2104 target.set(10.0);
2105 window.update(cx, |_, window, _| window.refresh()).unwrap();
2106 cx.run_until_parked();
2107 assert_eq!(*samples.borrow().last().unwrap(), 0.0);
2108
2109 cx.executor().advance_clock(Duration::from_millis(50));
2110 window.update(cx, |_, window, _| window.refresh()).unwrap();
2111 cx.run_until_parked();
2112 assert_eq!(*samples.borrow().last().unwrap(), 0.0);
2113
2114 cx.executor().advance_clock(Duration::from_millis(50));
2115 window.update(cx, |_, window, _| window.refresh()).unwrap();
2116 cx.run_until_parked();
2117 assert_eq!(*samples.borrow().last().unwrap(), 5.0);
2118 }
2119
2120 #[gpui::test]
2121 fn a_completed_transition_stops_requesting_frames(cx: &mut TestAppContext) {
2122 let duration = Duration::from_millis(100);
2123 let fixture = Fixture::open(cx, duration);
2124 fixture.render(cx, 1.0);
2125 assert_eq!(fixture.pending_frame(cx), 1);
2126
2127 cx.executor().advance_clock(duration);
2128 assert_eq!(fixture.render(cx, 1.0), 1.0);
2129 fixture.pending_frame(cx);
2130 cx.run_until_parked();
2131 assert_eq!(fixture.pending_frame(cx), 0);
2132 }
2133
2134 #[gpui::test]
2135 fn reduced_motion_adopts_the_target_without_requesting_a_frame(cx: &mut TestAppContext) {
2136 cx.update(|cx| cx.set_reduce_motion(true));
2137 let duration = Duration::from_millis(100);
2138 let fixture = Fixture::open(cx, duration);
2139 assert_eq!(fixture.render(cx, 1.0), 1.0);
2140 assert_eq!(fixture.pending_frame(cx), 0);
2141 }
2142
2143 struct SpringView {
2144 target: Rc<Cell<f32>>,
2145 policy: Rc<Cell<Spring>>,
2146 samples: Rc<RefCell<Vec<f32>>>,
2147 }
2148
2149 impl Render for SpringView {
2150 fn render(
2151 &mut self,
2152 window: &mut Window,
2153 cx: &mut gpui::Context<Self>,
2154 ) -> impl IntoElement {
2155 self.samples.borrow_mut().push(spring(
2156 ("spring-test", "value"),
2157 self.target.get(),
2158 self.policy.get(),
2159 window,
2160 cx,
2161 ));
2162 Empty
2163 }
2164 }
2165
2166 struct SpringFixture {
2167 window: WindowHandle<SpringView>,
2168 target: Rc<Cell<f32>>,
2169 policy: Rc<Cell<Spring>>,
2170 samples: Rc<RefCell<Vec<f32>>>,
2171 }
2172
2173 impl SpringFixture {
2174 fn open(cx: &mut TestAppContext, policy: Spring) -> Self {
2175 let target = Rc::new(Cell::new(0.0));
2176 let policy = Rc::new(Cell::new(policy));
2177 let samples = Rc::new(RefCell::new(Vec::new()));
2178 let window = cx.open_window(size(px(100.), px(100.)), {
2179 let target = target.clone();
2180 let policy = policy.clone();
2181 let samples = samples.clone();
2182 move |_, _| SpringView {
2183 target,
2184 policy,
2185 samples,
2186 }
2187 });
2188 cx.run_until_parked();
2189 Self {
2190 window,
2191 target,
2192 policy,
2193 samples,
2194 }
2195 }
2196
2197 fn render(&self, cx: &mut TestAppContext, target: f32) -> f32 {
2198 self.target.set(target);
2199 self.window
2200 .update(cx, |_, window, _| window.refresh())
2201 .unwrap();
2202 cx.run_until_parked();
2203 *self.samples.borrow().last().unwrap()
2204 }
2205
2206 fn advance(&self, cx: &mut TestAppContext, millis: u64, target: f32) -> f32 {
2207 cx.executor().advance_clock(Duration::from_millis(millis));
2208 self.render(cx, target)
2209 }
2210
2211 fn pending_frame(&self, cx: &mut TestAppContext) -> usize {
2212 self.window
2213 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
2214 .unwrap()
2215 }
2216 }
2217
2218 #[gpui::test]
2219 fn a_spring_adopts_its_first_target_immediately(cx: &mut TestAppContext) {
2220 let fixture = SpringFixture::open(cx, Spring::new(Duration::from_millis(300)));
2221 assert_eq!(*fixture.samples.borrow().first().unwrap(), 0.0);
2222 }
2223
2224 #[gpui::test]
2225 fn a_spring_travels_toward_its_target_over_time(cx: &mut TestAppContext) {
2226 let fixture = SpringFixture::open(cx, Spring::new(Duration::from_millis(300)));
2227 assert_eq!(fixture.render(cx, 1.0), 0.0);
2228
2229 let early = fixture.advance(cx, 50, 1.0);
2230 let late = fixture.advance(cx, 50, 1.0);
2231 assert!(
2232 0.0 < early && early < late && late < 1.0,
2233 "expected monotonic approach, got {early} then {late}"
2234 );
2235 }
2236
2237 #[gpui::test]
2238 fn a_reversed_spring_keeps_its_momentum_before_turning_around(cx: &mut TestAppContext) {
2239 let fixture = SpringFixture::open(cx, Spring::new(Duration::from_millis(300)));
2240 fixture.render(cx, 1.0);
2241 let reversed_at = fixture.advance(cx, 100, 1.0);
2242
2243 assert_eq!(fixture.render(cx, 0.0), reversed_at);
2246
2247 let next = fixture.advance(cx, 16, 0.0);
2248 assert!(
2249 next > reversed_at,
2250 "expected the spring to carry its velocity past {reversed_at}, got {next}"
2251 );
2252
2253 assert_eq!(fixture.advance(cx, 1_000, 0.0), 0.0);
2254 }
2255
2256 #[gpui::test]
2257 fn a_bouncy_spring_overshoots_its_target(cx: &mut TestAppContext) {
2258 let fixture = SpringFixture::open(
2259 cx,
2260 Spring::new(Duration::from_millis(350)).with_damping(0.7),
2261 );
2262 fixture.render(cx, 1.0);
2263 for _ in 0..30 {
2264 fixture.advance(cx, 16, 1.0);
2265 }
2266
2267 let peak = fixture
2268 .samples
2269 .borrow()
2270 .iter()
2271 .copied()
2272 .fold(f32::MIN, f32::max);
2273 assert!(peak > 1.0, "expected an overshoot past 1.0, got {peak}");
2274 }
2275
2276 #[gpui::test]
2277 fn a_settled_spring_stops_requesting_frames(cx: &mut TestAppContext) {
2278 let fixture = SpringFixture::open(cx, Spring::new(Duration::from_millis(300)));
2279 fixture.render(cx, 1.0);
2280 assert_eq!(fixture.pending_frame(cx), 1);
2281
2282 assert_eq!(fixture.advance(cx, 2_000, 1.0), 1.0);
2283 fixture.pending_frame(cx);
2284 cx.run_until_parked();
2285 assert_eq!(fixture.pending_frame(cx), 0);
2286 }
2287
2288 #[gpui::test]
2289 fn a_spring_that_is_not_travelling_adopts_its_target_on_the_spot(cx: &mut TestAppContext) {
2290 let travelling = Spring::new(Duration::from_millis(300));
2291 let fixture = SpringFixture::open(cx, travelling.with_travel(false));
2292
2293 assert_eq!(fixture.render(cx, 1.0), 1.0);
2294 assert_eq!(fixture.pending_frame(cx), 0);
2295 assert_eq!(fixture.advance(cx, 100, 5.0), 5.0);
2296
2297 fixture.policy.set(travelling);
2300 assert_eq!(fixture.render(cx, 6.0), 5.0);
2301 let next = fixture.advance(cx, 50, 6.0);
2302 assert!(
2303 5.0 < next && next < 6.0,
2304 "expected travel to resume from 5.0, got {next}"
2305 );
2306 }
2307
2308 #[gpui::test]
2309 fn a_zero_response_spring_resolves_instead_of_dividing_by_its_period(cx: &mut TestAppContext) {
2310 let fixture = SpringFixture::open(cx, Spring::new(Duration::ZERO));
2311 assert_eq!(fixture.render(cx, 1.0), 1.0);
2312 assert_eq!(fixture.pending_frame(cx), 0);
2313 }
2314
2315 #[test]
2316 fn spring_rejects_non_finite_or_negative_physical_parameters() {
2317 let spring = Spring::new(Duration::from_millis(300));
2318
2319 assert_eq!(
2320 spring.try_with_damping(f32::NAN).unwrap_err(),
2321 SpringError::InvalidDamping
2322 );
2323 assert_eq!(
2324 spring.try_with_damping(-0.1).unwrap_err(),
2325 SpringError::InvalidDamping
2326 );
2327 assert_eq!(
2328 spring.try_with_epsilon(f32::INFINITY).unwrap_err(),
2329 SpringError::InvalidEpsilon
2330 );
2331 assert_eq!(
2332 spring.try_with_epsilon(-0.1).unwrap_err(),
2333 SpringError::InvalidEpsilon
2334 );
2335 }
2336
2337 #[test]
2338 fn spring_reports_its_unit_specific_settling_tolerance() {
2339 let normalized = Spring::new(Duration::from_millis(180));
2340 let pixels = Spring::new(Duration::from_millis(180)).with_epsilon(0.1);
2341
2342 assert!(normalized.epsilon() < 0.01);
2343 assert_eq!(pixels.epsilon(), 0.1);
2344 }
2345
2346 #[gpui::test]
2347 fn reduced_motion_adopts_the_spring_target_without_requesting_a_frame(cx: &mut TestAppContext) {
2348 cx.update(|cx| cx.set_reduce_motion(true));
2349 let fixture = SpringFixture::open(
2350 cx,
2351 Spring::new(Duration::from_millis(350)).with_damping(0.7),
2352 );
2353 assert_eq!(fixture.render(cx, 1.0), 1.0);
2354 assert_eq!(fixture.pending_frame(cx), 0);
2355 }
2356}