Skip to main content

gpui_base/
motion.rs

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