Skip to main content

gpui_base/
motion.rs

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