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