Skip to main content

gpui/
motion.rs

1use std::{
2    num::NonZeroU32,
3    ops::{Deref, DerefMut, Sub},
4    rc::Rc,
5    time::Duration,
6};
7
8use crate::spring::DEFAULT_SPRING_EPSILON;
9use crate::{SpringAnimation, SpringConfig, SpringState, SpringTarget};
10
11/// Creates a [`Duration`](std::time::Duration) from whole seconds.
12///
13/// This is `Duration::from_secs` as a `const fn`, which keeps motion declarations short.
14///
15/// # Examples
16///
17/// ```
18/// use std::time::Duration;
19///
20/// use gpui::secs;
21///
22/// assert_eq!(secs(2), Duration::from_secs(2));
23/// ```
24pub const fn secs(seconds: u64) -> Duration {
25    Duration::from_secs(seconds)
26}
27
28/// Creates a [`Duration`](std::time::Duration) from whole milliseconds.
29///
30/// This is `Duration::from_millis` as a `const fn`. Pass the result to [`Motion::new`] or
31/// [`Animation::new`](crate::Animation::new).
32///
33/// # Examples
34///
35/// ```
36/// use std::time::Duration;
37///
38/// use gpui::millis;
39///
40/// assert_eq!(millis(250), Duration::from_millis(250));
41/// ```
42pub const fn millis(milliseconds: u64) -> Duration {
43    Duration::from_millis(milliseconds)
44}
45
46/// A motion's presentation progress, normally between zero and one.
47///
48/// [`Motion::sample`] returns progress after the easing function has run.
49/// An easing curve may overshoot, so a sample can fall outside `0.0..=1.0`. Use
50/// [`Progress::clamped`] when an API needs that range. `Progress` describes where the presentation
51/// is, not whether the run has finished; use [`MotionSample::is_complete`] for that.
52///
53/// # Examples
54///
55/// ```
56/// use gpui::Progress;
57///
58/// assert_eq!(Progress::clamped(1.5), Progress::END);
59/// assert_eq!(Progress::clamped(-0.25), Progress::START);
60/// assert_eq!(Progress::clamped(0.5).get(), 0.5);
61/// ```
62#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
63pub struct Progress(f32);
64
65impl Progress {
66    /// Zero progress: the origin of a motion.
67    pub const START: Self = Self(0.0);
68
69    /// One progress: the target of a motion.
70    pub const END: Self = Self(1.0);
71
72    /// Returns `value` limited to `0.0..=1.0`.
73    ///
74    /// # Panics
75    ///
76    /// Panics if `value` is NaN.
77    pub fn clamped(value: f32) -> Self {
78        assert!(!value.is_nan(), "progress must not be NaN");
79        Self(value.clamp(Self::START.0, Self::END.0))
80    }
81
82    /// Returns the progress as a float. It can lie outside `0.0..=1.0` when it comes from an
83    /// easing curve that overshoots.
84    pub const fn get(self) -> f32 {
85        self.0
86    }
87
88    /// Returns whether the progress is exactly one.
89    ///
90    /// This doesn't mean the whole run has finished. Use [`MotionSample::is_complete`] for that.
91    pub const fn is_at_end(self) -> bool {
92        self.0 == Self::END.0
93    }
94
95    pub(crate) fn reversed(self) -> Self {
96        Self::eased(Self::END.0 - self.0)
97    }
98
99    fn eased(value: f32) -> Self {
100        assert!(value.is_finite(), "easing must return a finite value");
101        Self(value)
102    }
103}
104
105/// Adds motion constructors to [`Duration`](std::time::Duration), including
106/// `duration.with_easing(...)`.
107///
108/// Import this trait to call its methods. Each method forwards to the matching [`Motion`]
109/// method on a single forward pass.
110///
111/// # Examples
112///
113/// ```
114/// use std::time::Duration;
115///
116/// use gpui::{MotionDurationExt, ease_out_quint};
117///
118/// let motion = Duration::from_millis(300).with_easing(ease_out_quint());
119/// assert_eq!(motion.forward_pass().duration(), Duration::from_millis(300));
120/// ```
121pub trait MotionDurationExt {
122    /// Creates a one-pass motion of this duration with the given easing function.
123    fn with_easing(self, easing: impl Fn(f32) -> f32 + 'static) -> Motion;
124
125    /// Creates a motion of this duration whose easing is a spring sampled over that duration.
126    /// See [`Motion::with_spring`].
127    fn with_spring(self, config: SpringConfig) -> Motion;
128}
129
130impl MotionDurationExt for Duration {
131    fn with_easing(self, easing: impl Fn(f32) -> f32 + 'static) -> Motion {
132        Motion::new(self).with_easing(easing)
133    }
134
135    fn with_spring(self, config: SpringConfig) -> Motion {
136        Motion::new(self).with_spring(config)
137    }
138}
139
140/// The former name of [`MotionDurationExt`].
141pub use MotionDurationExt as DurationWithEasing;
142
143/// An easing function that maps linear pass time to eased progress.
144///
145/// Each [`MotionPass`] and [`Motion`] owns one. Clones share the same function. The default is
146/// [`linear`](crate::linear).
147///
148/// # Examples
149///
150/// ```
151/// use gpui::{Easing, Progress};
152///
153/// let square = Easing::new(|t| t * t);
154/// assert_eq!(square.sample(Progress::clamped(0.5)).get(), 0.25);
155/// assert_eq!(Easing::default().sample(Progress::clamped(0.5)).get(), 0.5);
156/// ```
157#[derive(Clone)]
158pub struct Easing(Rc<dyn Fn(f32) -> f32>);
159
160impl Easing {
161    /// Creates an easing from `easing`, which maps `0.0..=1.0` to an eased value.
162    ///
163    /// The result is not clamped, so the function may overshoot. It must return finite values.
164    pub fn new(easing: impl Fn(f32) -> f32 + 'static) -> Self {
165        Self(Rc::new(easing))
166    }
167
168    /// Returns the eased value of `progress`.
169    ///
170    /// # Panics
171    ///
172    /// Panics if the easing function returns NaN or an infinite value.
173    pub fn sample(&self, progress: Progress) -> Progress {
174        Progress::eased((self.0)(progress.get()))
175    }
176}
177
178impl Default for Easing {
179    fn default() -> Self {
180        Self::new(crate::linear)
181    }
182}
183
184/// One directional pass of a [`Motion`]: a duration together with its easing curve.
185///
186/// A one-way motion needs one pass, which [`Motion::new`] creates for you. Use a `MotionPass` when
187/// the reverse leg of an alternating motion needs its own duration or easing, via
188/// [`Motion::with_reverse_pass`]. Local pass time advances from zero to one; the easing maps that
189/// time to presentation progress and may overshoot or be non-monotonic.
190///
191/// # Examples
192///
193/// ```
194/// use gpui::{Motion, MotionPass, millis};
195///
196/// let motion = Motion::new(millis(500)).with_reverse_pass(MotionPass::new(millis(1000)));
197/// assert_eq!(motion.forward_pass().duration(), millis(500));
198/// assert_eq!(motion.reverse_pass().unwrap().duration(), millis(1000));
199/// ```
200#[derive(Clone)]
201pub struct MotionPass {
202    duration: Duration,
203    easing: Easing,
204}
205
206impl MotionPass {
207    /// Creates a linear pass of `duration`. A zero duration is valid.
208    pub fn new(duration: Duration) -> Self {
209        Self {
210            duration,
211            easing: Easing::default(),
212        }
213    }
214
215    /// Returns this pass with `easing` as its curve.
216    pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
217        self.easing = Easing::new(easing);
218        self
219    }
220
221    /// Returns the length of this pass.
222    ///
223    /// See [`Motion`] for how a zero-length pass resolves.
224    pub fn duration(&self) -> Duration {
225        self.duration
226    }
227
228    /// Returns the easing curve of this pass.
229    pub fn easing(&self) -> &Easing {
230        &self.easing
231    }
232
233    fn duration_nanos(&self) -> u128 {
234        self.duration.as_nanos()
235    }
236
237    fn local_time(&self, elapsed_nanos: u128) -> Progress {
238        let duration_nanos = self.duration_nanos();
239        debug_assert!(duration_nanos > 0);
240        debug_assert!(elapsed_nanos < duration_nanos);
241
242        let linear = elapsed_nanos as f64 / duration_nanos as f64;
243        Progress::clamped(linear as f32)
244    }
245}
246
247impl From<Duration> for MotionPass {
248    fn from(duration: Duration) -> Self {
249        Self::new(duration)
250    }
251}
252
253/// How many iterations a [`Motion`] plays before it completes.
254///
255/// An iteration is one run through the motion's passes: a single forward pass, or a forward
256/// and a reverse pass when the motion alternates. Set it with [`Motion::iterations`] or
257/// [`Motion::repeat_forever`].
258#[derive(Clone, Copy, Debug, Eq, PartialEq)]
259pub enum Iterations {
260    /// A nonzero number of iterations, including the first.
261    Finite(NonZeroU32),
262    /// Repeat without end, as [`Motion::repeat_forever`] does.
263    Forever,
264}
265
266// Derive(Default) cannot select a data-bearing variant.
267impl Default for Iterations {
268    fn default() -> Self {
269        Self::Finite(NonZeroU32::MIN)
270    }
271}
272
273/// The configured direction of a motion's iterations.
274///
275/// The default is [`Direction::Forward`]. [`Direction::Alternate`] and
276/// [`Direction::AlternateReverse`] differ only in which way the first iteration travels.
277#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
278pub enum Direction {
279    /// Every iteration travels from origin to target.
280    #[default]
281    Forward,
282    /// Every iteration travels from target to origin.
283    Reverse,
284    /// The first iteration travels forward, then the directions alternate.
285    Alternate,
286    /// The first iteration travels in reverse, then the directions alternate.
287    AlternateReverse,
288}
289
290/// The direction of the pass that a sample falls in.
291#[derive(Clone, Copy, Debug, Eq, PartialEq)]
292pub enum PassDirection {
293    /// Origin to target.
294    Forward,
295    /// Target to origin.
296    Reverse,
297}
298
299/// The iteration count and configured direction of a run.
300///
301/// Read it with [`Motion::playback`]. Replace it all at once with [`Motion::with_playback`].
302#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
303pub struct Playback {
304    iterations: Iterations,
305    direction: Direction,
306}
307
308impl Playback {
309    /// Creates a playback policy from `iterations` and `direction`.
310    pub const fn new(iterations: Iterations, direction: Direction) -> Self {
311        Self {
312            iterations,
313            direction,
314        }
315    }
316
317    /// Returns the configured iteration policy.
318    pub const fn iterations(self) -> Iterations {
319        self.iterations
320    }
321
322    /// Returns the configured direction, which may alternate between passes.
323    pub const fn direction(self) -> Direction {
324        self.direction
325    }
326}
327
328/// The total length of a motion, including its delay.
329///
330/// See [`Motion::checked_extent`].
331#[derive(Clone, Copy, Debug, Eq, PartialEq)]
332pub enum MotionExtent {
333    /// A finite length that fits in [`Duration`](std::time::Duration).
334    Finite(Duration),
335    /// The motion has no end.
336    Infinite,
337}
338
339/// Where a sampled motion is in its lifecycle.
340#[derive(Clone, Copy, Debug, Eq, PartialEq)]
341pub enum MotionPhase {
342    /// The initial delay has not yet elapsed.
343    Delayed,
344    /// The motion is playing.
345    Playing,
346    /// Every finite iteration has elapsed, or a spring has reached its settling cutoff.
347    Completed,
348    /// An infinite motion whose passes all have zero length. Its presentation never changes, so
349    /// it requests no frames.
350    Inactive,
351}
352
353impl Direction {
354    fn pass(self, iteration: u128) -> PassDirection {
355        match self {
356            Self::Forward => PassDirection::Forward,
357            Self::Reverse => PassDirection::Reverse,
358            Self::Alternate => {
359                if iteration.is_multiple_of(2) {
360                    PassDirection::Forward
361                } else {
362                    PassDirection::Reverse
363                }
364            }
365            Self::AlternateReverse => {
366                if iteration.is_multiple_of(2) {
367                    PassDirection::Reverse
368                } else {
369                    PassDirection::Forward
370                }
371            }
372        }
373    }
374}
375
376/// The state of a [`Motion`] at one moment, returned by [`Motion::sample`].
377///
378/// Use `progress` to draw the frame and `is_active` to decide whether to request another one.
379/// Whether the whole run has finished is a separate question, answered by [`is_complete`](Self::is_complete).
380#[derive(Clone, Copy, Debug, PartialEq)]
381pub struct MotionSample {
382    /// Eased presentation progress. It may overshoot zero through one.
383    pub progress: Progress,
384
385    /// Normalized time within the current pass, before easing. For a settling spring this is
386    /// the elapsed time divided by the estimated settling time.
387    pub local_time: Progress,
388
389    /// Zero-based index of the current iteration. A spring reports zero. A completed finite
390    /// run reports its last iteration.
391    pub iteration: u128,
392
393    /// The direction of the pass this sample is in. It is the resolved direction, not the
394    /// configured [`Direction`]. A spring reports [`PassDirection::Forward`].
395    pub direction: PassDirection,
396
397    /// The lifecycle state of the run.
398    pub phase: MotionPhase,
399
400    /// Whether another sample can produce a different value. This is true in
401    /// [`MotionPhase::Delayed`] and [`MotionPhase::Playing`], and false in
402    /// [`MotionPhase::Completed`] and [`MotionPhase::Inactive`].
403    pub is_active: bool,
404}
405
406impl MotionSample {
407    /// Returns whether the whole finite run has completed.
408    ///
409    /// A completed run may rest at either end of its presentation, so check
410    /// [`is_at_end`](Self::is_at_end) separately if you need the endpoint.
411    pub const fn is_complete(self) -> bool {
412        matches!(self.phase, MotionPhase::Completed)
413    }
414
415    /// Returns whether the eased presentation is exactly at one.
416    pub const fn is_at_end(self) -> bool {
417        self.progress.is_at_end()
418    }
419}
420
421/// The timing of a duration-based [`Motion`]: its passes, delay and playback.
422#[derive(Clone)]
423pub struct DurationDescription {
424    forward: MotionPass,
425    reverse: Option<MotionPass>,
426    delay: Duration,
427    playback: Playback,
428}
429
430/// The timing of a spring-driven [`Motion`]: its physical parameters and settling tolerance.
431#[derive(Clone, Copy, Debug, PartialEq)]
432pub struct SpringDescription {
433    /// The spring's physical parameters.
434    pub(crate) config: SpringConfig,
435
436    /// The distance and velocity threshold for settling.
437    pub(crate) epsilon: f32,
438
439    settle_after: Duration,
440}
441
442/// A deterministic, time-sampled motion from a start value to a target.
443///
444/// `Motion` describes how a value moves over time, and it doesn't hold the value. It is either
445/// duration-based (the default) or spring-based, depending on `Description`. Call
446/// [`Motion::sample`] with the time since the run started to get the progress and the
447/// lifecycle state at that moment.
448///
449/// A duration motion has one forward [`MotionPass`], an optional reverse pass, an initial
450/// delay, and a [`Playback`] policy. The default is one forward iteration with no delay. The
451/// delay applies once, when a run begins.
452///
453/// [`Motion::iterations`] counts total iterations, including the first. Restarting motion
454/// begins each iteration with local time at zero. [`Motion::alternate`] starts forward, then
455/// uses a backward leg on every second iteration. [`Direction::AlternateReverse`] starts
456/// backward instead. With conventional easing endpoints, forward-first alternating motion rests
457/// at its target after an odd count and at its origin after an even count. Reverse-first
458/// alternating motion has the opposite endpoints. Custom easing endpoints may settle elsewhere,
459/// including outside zero through one.
460///
461/// A backward leg uses the configured reverse pass, or reuses the forward pass's duration and
462/// easing if no reverse pass is configured. Reverse local time still advances from zero to one
463/// and is eased in that direction. The resulting presentation progress is `1 - eased`. For
464/// example, reverse ease-in starts the backward leg slowly and accelerates toward the origin.
465///
466/// Local pass time is normalized. Eased presentation may overshoot, as in upstream spring
467/// easing, while pass selection remains deterministic. Finite zero-duration motion resolves
468/// immediately once its initial delay is satisfied, with the configured direction and iteration
469/// parity determining the final presentation. A zero-length leg in otherwise nonzero alternating
470/// motion is instantaneous. Indefinite motion whose applicable passes all have zero duration
471/// becomes inactive instead of requesting animation frames forever.
472///
473/// # Examples
474///
475/// Ease a one-pass motion. At a quarter of the way through, `ease_in_out` gives `0.125`.
476///
477/// ```
478/// use gpui::{Motion, ease_in_out, millis};
479///
480/// let motion = Motion::new(millis(200)).with_easing(ease_in_out);
481/// assert_eq!(motion.sample(millis(50)).progress.get(), 0.125);
482/// assert!(motion.sample(millis(200)).is_at_end());
483/// ```
484///
485/// Restart the pass three times. Each iteration begins again from zero.
486///
487/// ```
488/// use gpui::{Motion, millis};
489///
490/// let motion = Motion::new(millis(1000)).iterations(3);
491/// let sample = motion.sample(millis(2500));
492/// assert_eq!(sample.iteration, 2);
493/// assert_eq!(sample.local_time.get(), 0.5);
494/// assert!(motion.sample(millis(3000)).is_complete());
495/// ```
496///
497/// Alternate the direction. After an even number of iterations the motion rests at its origin.
498///
499/// ```
500/// use gpui::{Motion, Progress, millis};
501///
502/// let motion = Motion::new(millis(1000)).iterations(2).alternate();
503/// assert_eq!(motion.sample(millis(1500)).progress.get(), 0.5);
504/// assert_eq!(motion.sample(millis(2000)).progress, Progress::START);
505/// ```
506#[derive(Clone, Debug)]
507pub struct Motion<Description = DurationDescription> {
508    description: Description,
509}
510
511impl<Description> Deref for Motion<Description> {
512    type Target = Description;
513
514    fn deref(&self) -> &Self::Target {
515        &self.description
516    }
517}
518
519impl<Description> DerefMut for Motion<Description> {
520    fn deref_mut(&mut self) -> &mut Self::Target {
521        &mut self.description
522    }
523}
524
525impl Motion<DurationDescription> {
526    /// Creates a one-pass linear motion of `duration`, with no delay, run once.
527    pub fn new(duration: Duration) -> Self {
528        Self {
529            description: DurationDescription {
530                forward: MotionPass::new(duration),
531                reverse: None,
532                delay: Duration::ZERO,
533                playback: Playback::default(),
534            },
535        }
536    }
537
538    /// Sets the easing of the forward pass.
539    ///
540    /// An alternating motion without an explicit reverse pass uses this easing for its backward
541    /// legs too.
542    pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
543        self.description.forward.easing = Easing::new(easing);
544        self
545    }
546
547    /// Replaces the easing with a spring sampled over this motion's duration.
548    ///
549    /// The spring starts at rest and reaches its target at the end of the duration. It doesn't
550    /// settle on its own and it doesn't keep velocity across retargets. For a spring that settles
551    /// and keeps its velocity, use [`Motion::spring`].
552    pub fn with_spring(self, config: SpringConfig) -> Self {
553        let duration = self.forward.duration.as_secs_f32();
554        let initial_state = SpringState {
555            position: 0.0,
556            velocity: 0.0,
557        };
558
559        self.with_easing(move |progress| {
560            if progress <= 0.0 {
561                0.0
562            } else if progress >= 1.0 {
563                1.0
564            } else {
565                config
566                    .step(initial_state, 1.0, progress * duration)
567                    .position
568            }
569        })
570    }
571
572    /// Replaces the forward pass, including its duration and easing.
573    pub fn with_forward_pass(mut self, pass: MotionPass) -> Self {
574        self.forward = pass;
575        self
576    }
577
578    /// Sets the pass used for backward legs.
579    ///
580    /// This doesn't infer direction from changes to an [`Animated`](crate::Animated) target. A
581    /// newly retargeted run begins with its configured direction. Without an explicit reverse
582    /// pass, backward legs reuse the forward pass's duration and easing.
583    ///
584    /// Reverse local time advances from zero to one and is eased normally. Its presentation is
585    /// then mapped as `1 - eased`, so a reverse ease-in begins slowly and accelerates toward the
586    /// origin.
587    pub fn with_reverse_pass(mut self, pass: MotionPass) -> Self {
588        self.reverse = Some(pass);
589        self
590    }
591
592    /// Delays the start of the run by `delay`.
593    ///
594    /// The delay applies once, before the first iteration. Restarted iterations don't wait again.
595    /// During the delay, the progress is the start value of the run.
596    pub fn with_delay(mut self, delay: Duration) -> Self {
597        self.delay = delay;
598        self
599    }
600
601    /// Sets the total number of iterations, including the first.
602    ///
603    /// The default is one. Each iteration restarts local timing. In forward-first alternating
604    /// motion, odd-numbered iterations are forward and even-numbered iterations are backward.
605    /// With conventional easing endpoints, counts of one and three rest at the target, and counts
606    /// of two and four rest at the origin.
607    ///
608    /// # Panics
609    ///
610    /// Panics if `iterations` is zero.
611    pub fn iterations(mut self, iterations: u32) -> Self {
612        self.playback.iterations = Iterations::Finite(
613            NonZeroU32::new(iterations).expect("motion iterations must be at least 1"),
614        );
615        self
616    }
617
618    /// Repeats this motion without end.
619    ///
620    /// Each iteration restarts from the start unless the motion alternates. A motion whose
621    /// applicable passes all have zero duration becomes [`MotionPhase::Inactive`], resting at a
622    /// deterministic presentation.
623    pub fn repeat_forever(mut self) -> Self {
624        self.playback.iterations = Iterations::Forever;
625        self
626    }
627
628    /// Alternates the direction of successive iterations.
629    ///
630    /// Odd-numbered iterations use the forward pass. Even-numbered iterations use the reverse
631    /// pass, or the forward pass when none is configured. This is the same as
632    /// `direction(Direction::Alternate)`.
633    pub fn alternate(mut self) -> Self {
634        self.playback.direction = Direction::Alternate;
635        self
636    }
637
638    /// Sets the direction of successive iterations.
639    pub fn direction(mut self, direction: Direction) -> Self {
640        self.playback.direction = direction;
641        self
642    }
643
644    /// Replaces the whole playback policy: iterations and direction.
645    pub fn with_playback(mut self, playback: Playback) -> Self {
646        self.playback = playback;
647        self
648    }
649
650    /// Returns the forward pass.
651    pub fn forward_pass(&self) -> &MotionPass {
652        &self.forward
653    }
654
655    /// Returns the reverse pass, or `None` when backward legs reuse the forward pass.
656    pub fn reverse_pass(&self) -> Option<&MotionPass> {
657        self.reverse.as_ref()
658    }
659
660    /// Returns the delay applied before the first iteration.
661    pub fn delay(&self) -> Duration {
662        self.delay
663    }
664
665    /// Returns the iteration count and direction of this motion.
666    pub const fn playback(&self) -> Playback {
667        self.description.playback
668    }
669
670    fn reverse_pass_or_forward(&self) -> &MotionPass {
671        self.reverse.as_ref().unwrap_or(&self.forward)
672    }
673
674    fn finite_duration_nanos(&self, iterations: NonZeroU32) -> Option<u128> {
675        let iterations = u128::from(iterations.get());
676        let forward = self.forward.duration_nanos();
677        let reverse = self.reverse_pass_or_forward().duration_nanos();
678        match self.playback.direction {
679            Direction::Forward => forward.checked_mul(iterations),
680            Direction::Reverse => reverse.checked_mul(iterations),
681            Direction::Alternate => (forward.checked_add(reverse)?)
682                .checked_mul(iterations / 2)?
683                .checked_add(forward.checked_mul(iterations % 2)?),
684            Direction::AlternateReverse => (forward.checked_add(reverse)?)
685                .checked_mul(iterations / 2)?
686                .checked_add(reverse.checked_mul(iterations % 2)?),
687        }
688    }
689
690    /// Returns the total length of the run, including the delay.
691    ///
692    /// Returns [`MotionExtent::Infinite`] for [`Motion::repeat_forever`]. Returns `None` only when
693    /// a finite total is too large for [`Duration`](std::time::Duration).
694    ///
695    /// # Examples
696    ///
697    /// ```
698    /// use gpui::{Motion, MotionExtent, millis};
699    ///
700    /// let motion = Motion::new(millis(300)).with_delay(millis(200)).iterations(3);
701    /// assert_eq!(motion.checked_extent(), Some(MotionExtent::Finite(millis(1100))));
702    /// ```
703    pub fn checked_extent(&self) -> Option<MotionExtent> {
704        let Iterations::Finite(iterations) = self.playback.iterations else {
705            return Some(MotionExtent::Infinite);
706        };
707        let nanos = self
708            .delay
709            .as_nanos()
710            .checked_add(self.finite_duration_nanos(iterations)?)?;
711        let seconds = u64::try_from(nanos / 1_000_000_000).ok()?;
712        let subsec_nanos = (nanos % 1_000_000_000) as u32;
713        Some(MotionExtent::Finite(Duration::new(seconds, subsec_nanos)))
714    }
715
716    fn pass(&self, direction: PassDirection) -> &MotionPass {
717        match direction {
718            PassDirection::Forward => &self.forward,
719            PassDirection::Reverse => self.reverse_pass_or_forward(),
720        }
721    }
722
723    fn active_pass(&self, elapsed_nanos: u128) -> Option<(&MotionPass, u128, PassDirection, u128)> {
724        let forward_duration = self.forward.duration_nanos();
725        let reverse_duration = self.reverse_pass_or_forward().duration_nanos();
726        match self.playback.direction {
727            Direction::Forward | Direction::Reverse => {
728                let direction = self.playback.direction.pass(0);
729                let pass = self.pass(direction);
730                let duration = pass.duration_nanos();
731                if duration == 0 {
732                    return None;
733                }
734                let iteration = elapsed_nanos / duration;
735                let local = elapsed_nanos % duration;
736                Some((pass, local, direction, iteration))
737            }
738            Direction::Alternate | Direction::AlternateReverse => {
739                let cycle = forward_duration + reverse_duration;
740                if cycle == 0 {
741                    return None;
742                }
743                let first = self.playback.direction.pass(0);
744                let first_duration = self.pass(first).duration_nanos();
745                let cycle_index = elapsed_nanos / cycle;
746                let in_cycle = elapsed_nanos % cycle;
747                let iteration = cycle_index * 2;
748                if in_cycle < first_duration {
749                    Some((self.pass(first), in_cycle, first, iteration))
750                } else {
751                    let second = self.playback.direction.pass(1);
752                    Some((
753                        self.pass(second),
754                        in_cycle - first_duration,
755                        second,
756                        iteration + 1,
757                    ))
758                }
759            }
760        }
761    }
762
763    fn start_progress(&self) -> Progress {
764        match self.playback.direction.pass(0) {
765            PassDirection::Forward => Progress::START,
766            PassDirection::Reverse => Progress::END,
767        }
768    }
769
770    fn terminal_sample(
771        &self,
772        direction: PassDirection,
773        iteration: u128,
774        phase: MotionPhase,
775        zero_duration: bool,
776    ) -> MotionSample {
777        let progress = if zero_duration {
778            match direction {
779                PassDirection::Forward => Progress::END,
780                PassDirection::Reverse => Progress::START,
781            }
782        } else {
783            self.presentation(self.pass(direction), Progress::END, direction)
784        };
785        MotionSample {
786            progress,
787            local_time: Progress::END,
788            iteration,
789            direction,
790            phase,
791            is_active: false,
792        }
793    }
794
795    fn presentation(
796        &self,
797        pass: &MotionPass,
798        local_time: Progress,
799        direction: PassDirection,
800    ) -> Progress {
801        let eased = pass.easing.sample(local_time);
802        if direction == PassDirection::Reverse {
803            eased.reversed()
804        } else {
805            eased
806        }
807    }
808
809    /// Samples this motion at `elapsed`, measured from the start of the run.
810    ///
811    /// Before the delay ends, the phase is [`MotionPhase::Delayed`]. After the last iteration of
812    /// a finite run, it is [`MotionPhase::Completed`] at the final progress. Pass selection,
813    /// iteration parity and completion use integer nanosecond arithmetic. Only the fraction
814    /// within the selected pass is converted to a float for easing.
815    pub fn sample(&self, elapsed: Duration) -> MotionSample {
816        if elapsed < self.delay {
817            return MotionSample {
818                progress: self.start_progress(),
819                local_time: Progress::START,
820                iteration: 0,
821                direction: self.playback.direction.pass(0),
822                phase: MotionPhase::Delayed,
823                is_active: true,
824            };
825        }
826
827        let elapsed_nanos = (elapsed - self.delay).as_nanos();
828        if let Iterations::Finite(iterations) = self.playback.iterations {
829            let total_duration = self
830                .finite_duration_nanos(iterations)
831                .expect("finite Motion extent fits in u128 nanoseconds");
832            if elapsed_nanos >= total_duration {
833                let last = u128::from(iterations.get() - 1);
834                return self.terminal_sample(
835                    self.playback.direction.pass(last),
836                    last,
837                    MotionPhase::Completed,
838                    total_duration == 0,
839                );
840            }
841        }
842
843        let Some((pass, elapsed_in_pass, direction, iteration)) = self.active_pass(elapsed_nanos)
844        else {
845            let direction = self.playback.direction.pass(0);
846            return MotionSample {
847                progress: self.start_progress(),
848                local_time: Progress::START,
849                iteration: 0,
850                direction,
851                phase: MotionPhase::Inactive,
852                is_active: false,
853            };
854        };
855
856        // A restarting pass presents its endpoint at the exact boundary while
857        // the run itself remains active. Its next sample begins the new pass.
858        if matches!(
859            self.playback.direction,
860            Direction::Forward | Direction::Reverse
861        ) && elapsed_nanos > 0
862            && elapsed_in_pass == 0
863        {
864            return MotionSample {
865                progress: self.presentation(pass, Progress::END, direction),
866                local_time: Progress::END,
867                iteration: iteration - 1,
868                direction,
869                phase: MotionPhase::Playing,
870                is_active: true,
871            };
872        }
873
874        let local_time = pass.local_time(elapsed_in_pass);
875
876        MotionSample {
877            progress: self.presentation(pass, local_time, direction),
878            local_time,
879            iteration,
880            direction,
881            phase: MotionPhase::Playing,
882            is_active: true,
883        }
884    }
885
886    /// Samples this motion at `now`, given the time `started_at` when the run began.
887    ///
888    /// `Time` can be any type that subtracts to a [`Duration`](std::time::Duration), such as
889    /// [`std::time::Instant`] or the executor's clock.
890    pub fn sample_at<Time>(&self, started_at: Time, now: Time) -> MotionSample
891    where
892        Time: Sub<Time, Output = Duration>,
893    {
894        self.sample(now - started_at)
895    }
896
897    pub(crate) fn resting_progress(&self) -> Progress {
898        match self.playback.iterations {
899            Iterations::Finite(iterations) => {
900                let last = u128::from(iterations.get() - 1);
901                match self.playback.direction.pass(last) {
902                    PassDirection::Forward => Progress::END,
903                    PassDirection::Reverse => Progress::START,
904                }
905            }
906            Iterations::Forever => self.start_progress(),
907        }
908    }
909}
910
911impl Default for Motion<DurationDescription> {
912    fn default() -> Self {
913        Self::new(Duration::ZERO)
914    }
915}
916
917impl From<Duration> for Motion<DurationDescription> {
918    fn from(duration: Duration) -> Self {
919        Self::new(duration)
920    }
921}
922
923/// The former name of [`Motion`].
924#[deprecated(note = "use Motion")]
925pub type MotionInfo = Motion;
926
927impl Motion<SpringDescription> {
928    /// Creates a spring that runs until it settles.
929    ///
930    /// Give the spring a target with [`Motion::to`]. The settling tolerance defaults to `0.001`.
931    /// Change it with [`Motion::with_epsilon`].
932    pub fn spring(config: SpringConfig) -> Self {
933        let epsilon = DEFAULT_SPRING_EPSILON;
934
935        Self {
936            description: SpringDescription {
937                config,
938                epsilon,
939                settle_after: config.settle_time(SpringState::default(), 1.0, epsilon),
940            },
941        }
942    }
943
944    /// Returns the physical parameters of this spring.
945    pub fn config(&self) -> SpringConfig {
946        self.config
947    }
948
949    /// Returns the settling tolerance of this spring.
950    pub fn epsilon(&self) -> f32 {
951        self.epsilon
952    }
953
954    /// Sets how close the spring must be to its target before it counts as settled.
955    ///
956    /// The tolerance applies to position, and to velocity scaled by the spring's natural frequency.
957    /// Changing it also recomputes the settling time, which is when [`Motion::sample`] reports
958    /// [`MotionPhase::Completed`].
959    pub fn with_epsilon(mut self, epsilon: f32) -> Self {
960        self.description.epsilon = epsilon;
961        self.description.settle_after =
962            self.config
963                .settle_time(SpringState::default(), 1.0, epsilon);
964        self
965    }
966
967    /// Samples this spring at `elapsed`, starting from rest at zero progress.
968    ///
969    /// Progress is the spring's position, which overshoots when the spring is underdamped. From
970    /// the settling time onward the sample is [`MotionPhase::Completed`] at [`Progress::END`],
971    /// and it is no longer active.
972    ///
973    /// # Examples
974    ///
975    /// ```
976    /// use gpui::{Motion, Progress, millis, secs, spring};
977    ///
978    /// let motion = Motion::spring(spring(100., 10., 1.));
979    /// assert!(motion.sample(millis(0)).is_active);
980    /// let settled = motion.sample(secs(10));
981    /// assert!(settled.is_complete());
982    /// assert_eq!(settled.progress, Progress::END);
983    /// ```
984    pub fn sample(&self, elapsed: Duration) -> MotionSample {
985        if elapsed >= self.settle_after {
986            return MotionSample {
987                progress: Progress::END,
988                local_time: Progress::END,
989                iteration: 0,
990                direction: PassDirection::Forward,
991                phase: MotionPhase::Completed,
992                is_active: false,
993            };
994        }
995
996        let state = self
997            .config
998            .step(SpringState::default(), 1.0, elapsed.as_secs_f32());
999
1000        MotionSample {
1001            progress: Progress::eased(state.position),
1002            local_time: Progress::clamped(
1003                (elapsed.as_secs_f64() / self.settle_after.as_secs_f64()) as f32,
1004            ),
1005            iteration: 0,
1006            direction: PassDirection::Forward,
1007            phase: MotionPhase::Playing,
1008            is_active: true,
1009        }
1010    }
1011
1012    /// Targets `target` with this spring.
1013    ///
1014    /// The result is a [`SpringAnimation`] for [`AnimationExt::with_spring`](crate::AnimationExt::with_spring).
1015    pub fn to<T: SpringTarget>(self, target: T) -> SpringAnimation<T> {
1016        SpringAnimation {
1017            motion: self,
1018            target,
1019            initial: None,
1020            playback: crate::SpringPlayback::Running,
1021        }
1022    }
1023}
1024
1025impl From<SpringConfig> for Motion<SpringDescription> {
1026    fn from(config: SpringConfig) -> Self {
1027        Self::spring(config)
1028    }
1029}
1030
1031/// A duration or spring [`Motion`] behind one type, so that an API can accept either.
1032///
1033/// Each spring sample starts from rest, so retargeting an animated value resets its velocity.
1034#[derive(Clone)]
1035pub enum AnyMotion {
1036    /// Motion that runs for a fixed duration.
1037    Duration(Motion<DurationDescription>),
1038
1039    /// Motion that runs until its spring settles.
1040    Spring(Motion<SpringDescription>),
1041}
1042
1043impl AnyMotion {
1044    pub(crate) fn starts_in_reverse(&self) -> bool {
1045        matches!(self, Self::Duration(motion) if motion.playback.direction.pass(0) == PassDirection::Reverse)
1046    }
1047
1048    /// Samples this motion at `elapsed`. See [`Motion::sample`].
1049    pub fn sample(&self, elapsed: Duration) -> MotionSample {
1050        match self {
1051            Self::Duration(motion) => motion.sample(elapsed),
1052            Self::Spring(motion) => motion.sample(elapsed),
1053        }
1054    }
1055
1056    /// Samples this motion at `now`, given the time `started_at` when the run began. See
1057    /// [`Motion::sample_at`].
1058    pub fn sample_at<Time>(&self, started_at: Time, now: Time) -> MotionSample
1059    where
1060        Time: Sub<Time, Output = Duration>,
1061    {
1062        self.sample(now - started_at)
1063    }
1064}
1065
1066impl From<Motion<DurationDescription>> for AnyMotion {
1067    fn from(motion: Motion<DurationDescription>) -> Self {
1068        Self::Duration(motion)
1069    }
1070}
1071
1072impl From<Motion<SpringDescription>> for AnyMotion {
1073    fn from(motion: Motion<SpringDescription>) -> Self {
1074        Self::Spring(motion)
1075    }
1076}
1077
1078impl From<Duration> for AnyMotion {
1079    fn from(duration: Duration) -> Self {
1080        Self::Duration(duration.into())
1081    }
1082}
1083
1084impl From<SpringConfig> for AnyMotion {
1085    fn from(config: SpringConfig) -> Self {
1086        Self::Spring(config.into())
1087    }
1088}
1089
1090impl From<&Motion<DurationDescription>> for AnyMotion {
1091    fn from(motion: &Motion<DurationDescription>) -> Self {
1092        Self::Duration(motion.clone())
1093    }
1094}
1095
1096impl From<&Motion<SpringDescription>> for AnyMotion {
1097    fn from(motion: &Motion<SpringDescription>) -> Self {
1098        Self::Spring(motion.clone())
1099    }
1100}
1101
1102impl From<&AnyMotion> for AnyMotion {
1103    fn from(motion: &AnyMotion) -> Self {
1104        motion.clone()
1105    }
1106}
1107
1108#[cfg(test)]
1109mod tests {
1110    use super::*;
1111
1112    fn progress(value: f32) -> Progress {
1113        Progress::clamped(value)
1114    }
1115
1116    fn assert_sample(motion: &Motion, elapsed: Duration, value: f32, is_active: bool) {
1117        let sample = motion.sample(elapsed);
1118        assert_eq!(sample.progress, progress(value));
1119        assert_eq!(sample.is_active, is_active);
1120    }
1121
1122    #[test]
1123    fn creates_durations() {
1124        assert_eq!(secs(2), Duration::from_secs(2));
1125        assert_eq!(millis(250), Duration::from_millis(250));
1126    }
1127
1128    #[test]
1129    fn samples_default_one_shot_and_custom_easing() {
1130        let motion = Duration::from_secs(2).with_easing(|progress| progress * progress);
1131        assert_eq!(motion.forward_pass().duration(), Duration::from_secs(2));
1132        assert!(motion.reverse_pass().is_none());
1133        assert_eq!(motion.delay(), Duration::ZERO);
1134        assert_eq!(motion.playback(), Playback::default());
1135
1136        assert_sample(&motion, Duration::ZERO, 0.0, true);
1137        assert_sample(&motion, Duration::from_secs(1), 0.25, true);
1138        assert_sample(&motion, Duration::from_secs(3), 1.0, false);
1139        assert_eq!(
1140            motion
1141                .sample_at(Duration::from_secs(3), Duration::from_secs(5))
1142                .progress,
1143            Progress::END
1144        );
1145
1146        assert_eq!(Progress::clamped(-1.0), Progress::START);
1147        assert_eq!(Progress::clamped(2.0), Progress::END);
1148    }
1149
1150    #[test]
1151    fn delay_applies_once_at_the_start_of_a_run() {
1152        let motion = Motion::new(Duration::from_secs(1))
1153            .with_delay(Duration::from_millis(100))
1154            .iterations(3);
1155
1156        assert_sample(&motion, Duration::ZERO, 0.0, true);
1157        assert_sample(&motion, Duration::from_millis(99), 0.0, true);
1158        assert_sample(&motion, Duration::from_millis(100), 0.0, true);
1159        assert_sample(&motion, Duration::from_millis(600), 0.5, true);
1160        assert_sample(&motion, Duration::from_millis(1_100), 1.0, true);
1161        assert_sample(&motion, Duration::from_millis(3_100), 1.0, false);
1162    }
1163
1164    #[test]
1165    fn restarting_iterations_have_exact_boundaries_and_stable_completion() {
1166        let motion = Motion::new(Duration::from_secs(1)).iterations(3);
1167
1168        for (milliseconds, value, active) in [
1169            (0, 0.0, true),
1170            (500, 0.5, true),
1171            (1_000, 1.0, true),
1172            (2_000, 1.0, true),
1173            (2_750, 0.75, true),
1174            (3_000, 1.0, false),
1175            (30_000, 1.0, false),
1176        ] {
1177            assert_sample(&motion, Duration::from_millis(milliseconds), value, active);
1178        }
1179    }
1180
1181    #[test]
1182    fn alternating_iterations_reverse_and_settle_by_parity() {
1183        for (iterations, expected_final) in [(1, 1.0), (2, 0.0), (3, 1.0), (4, 0.0)] {
1184            let motion = Motion::new(Duration::from_secs(1))
1185                .iterations(iterations)
1186                .alternate();
1187            assert_sample(
1188                &motion,
1189                Duration::from_secs(u64::from(iterations)),
1190                expected_final,
1191                false,
1192            );
1193            assert_sample(&motion, Duration::from_secs(100), expected_final, false);
1194        }
1195
1196        let motion = Motion::new(Duration::from_secs(1))
1197            .iterations(4)
1198            .alternate();
1199        for (milliseconds, value) in [
1200            (0, 0.0),
1201            (500, 0.5),
1202            (1_000, 1.0),
1203            (1_500, 0.5),
1204            (2_000, 0.0),
1205            (3_000, 1.0),
1206        ] {
1207            assert_sample(&motion, Duration::from_millis(milliseconds), value, true);
1208        }
1209    }
1210
1211    #[test]
1212    fn asymmetric_alternating_passes_use_local_easing() {
1213        let motion = Motion::new(Duration::from_secs(1))
1214            .with_easing(|value| value)
1215            .with_reverse_pass(
1216                MotionPass::new(Duration::from_secs(2)).with_easing(|value| value * value),
1217            )
1218            .iterations(5)
1219            .alternate();
1220
1221        assert_eq!(
1222            motion.reverse_pass().unwrap().duration(),
1223            Duration::from_secs(2)
1224        );
1225        assert_sample(&motion, Duration::from_millis(500), 0.5, true);
1226        assert_sample(&motion, Duration::from_secs(1), 1.0, true);
1227        assert_sample(&motion, Duration::from_secs(2), 0.75, true);
1228        assert_sample(&motion, Duration::from_secs(3), 0.0, true);
1229        assert_sample(&motion, Duration::from_millis(6_500), 0.5, true);
1230        assert_sample(&motion, Duration::from_secs(7), 1.0, false);
1231    }
1232
1233    #[test]
1234    fn infinite_motion_restarts_or_alternates_without_completing() {
1235        let restarting = Motion::new(Duration::from_secs(1)).repeat_forever();
1236        assert_sample(&restarting, Duration::from_millis(250), 0.25, true);
1237        assert_sample(&restarting, Duration::from_secs(1), 1.0, true);
1238        assert_sample(&restarting, Duration::from_millis(2_500), 0.5, true);
1239
1240        let alternating = Motion::new(Duration::from_secs(1))
1241            .repeat_forever()
1242            .alternate();
1243        assert_sample(&alternating, Duration::from_millis(1_250), 0.75, true);
1244        assert_sample(&alternating, Duration::from_secs(2), 0.0, true);
1245    }
1246
1247    #[test]
1248    fn zero_duration_motion_resolves_after_delay_without_spinning() {
1249        for (iterations, alternate, expected) in [
1250            (1, false, 1.0),
1251            (2, false, 1.0),
1252            (2, true, 0.0),
1253            (3, true, 1.0),
1254        ] {
1255            let mut motion = Motion::new(Duration::ZERO)
1256                .with_delay(Duration::from_millis(10))
1257                .iterations(iterations);
1258            if alternate {
1259                motion = motion.alternate();
1260            }
1261            assert_sample(&motion, Duration::from_millis(9), 0.0, true);
1262            assert_sample(&motion, Duration::from_millis(10), expected, false);
1263        }
1264
1265        let forever = Motion::new(Duration::ZERO).repeat_forever().alternate();
1266        assert_sample(&forever, Duration::ZERO, 0.0, false);
1267        assert_sample(&forever, Duration::MAX, 0.0, false);
1268    }
1269
1270    #[test]
1271    fn zero_length_passes_are_skipped_without_iteration() {
1272        let zero_forward = Motion::new(Duration::ZERO)
1273            .with_reverse_pass(MotionPass::new(Duration::from_secs(2)))
1274            .iterations(2)
1275            .alternate();
1276        assert_sample(&zero_forward, Duration::ZERO, 1.0, true);
1277        assert_sample(&zero_forward, Duration::from_secs(1), 0.5, true);
1278        assert_sample(&zero_forward, Duration::from_secs(2), 0.0, false);
1279
1280        let zero_reverse = Motion::new(Duration::from_secs(1))
1281            .with_reverse_pass(MotionPass::new(Duration::ZERO))
1282            .iterations(3)
1283            .alternate();
1284        assert_sample(&zero_reverse, Duration::from_secs(1), 0.0, true);
1285        assert_sample(&zero_reverse, Duration::from_secs(2), 1.0, false);
1286    }
1287
1288    #[test]
1289    fn nanosecond_and_large_duration_sampling_keep_exact_pass_selection() {
1290        let nanos = Motion::new(Duration::from_nanos(3))
1291            .iterations(3)
1292            .alternate();
1293        assert_sample(&nanos, Duration::from_nanos(3), 1.0, true);
1294        assert_sample(&nanos, Duration::from_nanos(6), 0.0, true);
1295        assert_sample(&nanos, Duration::from_nanos(9), 1.0, false);
1296
1297        let enormous = Motion::new(Duration::MAX).iterations(u32::MAX);
1298        assert_sample(&enormous, Duration::MAX, 1.0, true);
1299    }
1300
1301    #[test]
1302    fn playback_model_and_sample_distinguish_end_from_completion() {
1303        assert_eq!(Iterations::default(), Iterations::Finite(NonZeroU32::MIN));
1304        assert_eq!(Playback::default().direction(), Direction::Forward);
1305        let motion = Motion::new(secs(1)).iterations(3);
1306        assert_eq!(
1307            motion.playback().iterations(),
1308            Iterations::Finite(NonZeroU32::new(3).unwrap())
1309        );
1310        let boundary = motion.sample(secs(1));
1311        assert_eq!(boundary.progress, Progress::END);
1312        assert_eq!(boundary.local_time, Progress::END);
1313        assert_eq!(boundary.iteration, 0);
1314        assert!(boundary.is_at_end());
1315        assert!(!boundary.is_complete());
1316        let next = motion.sample(secs(1) + Duration::from_nanos(1));
1317        assert_eq!(next.iteration, 1);
1318        assert_eq!(next.phase, MotionPhase::Playing);
1319
1320        let alternating = Motion::new(secs(1)).iterations(2).alternate();
1321        let end = alternating.sample(secs(2));
1322        assert_eq!(end.progress, Progress::START);
1323        assert!(!end.is_at_end());
1324        assert!(end.is_complete());
1325        assert_eq!(end.iteration, 1);
1326    }
1327
1328    #[test]
1329    fn overshoot_is_not_the_presentation_endpoint() {
1330        let motion =
1331            Motion::new(secs(1)).with_easing(|progress| if progress < 1.0 { 1.2 } else { 1.0 });
1332        let overshoot = motion.sample(millis(500));
1333        assert_eq!(overshoot.progress.get(), 1.2);
1334        assert!(!overshoot.progress.is_at_end());
1335        assert!(!overshoot.is_at_end());
1336        assert!(!overshoot.is_complete());
1337        assert!(motion.sample(secs(1)).is_at_end());
1338    }
1339
1340    #[test]
1341    fn all_directions_resolve_passes_and_reverse_first_delay() {
1342        for (configured, first, second) in [
1343            (
1344                Direction::Forward,
1345                PassDirection::Forward,
1346                PassDirection::Forward,
1347            ),
1348            (
1349                Direction::Reverse,
1350                PassDirection::Reverse,
1351                PassDirection::Reverse,
1352            ),
1353            (
1354                Direction::Alternate,
1355                PassDirection::Forward,
1356                PassDirection::Reverse,
1357            ),
1358            (
1359                Direction::AlternateReverse,
1360                PassDirection::Reverse,
1361                PassDirection::Forward,
1362            ),
1363        ] {
1364            let motion = Motion::new(secs(1))
1365                .with_delay(millis(100))
1366                .iterations(2)
1367                .direction(configured);
1368            let delayed = motion.sample(Duration::ZERO);
1369            assert_eq!(delayed.phase, MotionPhase::Delayed);
1370            assert_eq!(delayed.direction, first);
1371            assert_eq!(
1372                delayed.progress,
1373                if first == PassDirection::Reverse {
1374                    Progress::END
1375                } else {
1376                    Progress::START
1377                }
1378            );
1379            let active = motion.sample(millis(600));
1380            assert_eq!(active.direction, first);
1381            assert_eq!(active.iteration, 0);
1382            assert_eq!(active.local_time, progress(0.5));
1383            let boundary = motion.sample(millis(1_100));
1384            assert_eq!(boundary.phase, MotionPhase::Playing);
1385            assert_eq!(
1386                boundary.progress,
1387                if first == PassDirection::Forward {
1388                    Progress::END
1389                } else {
1390                    Progress::START
1391                }
1392            );
1393            let later = motion.sample(millis(1_600));
1394            assert_eq!(later.direction, second);
1395            assert_eq!(later.iteration, 1);
1396            assert_eq!(later.local_time, progress(0.5));
1397            let completed = motion.sample(millis(2_100));
1398            assert_eq!(completed.direction, second);
1399            assert!(completed.is_complete());
1400        }
1401    }
1402
1403    #[test]
1404    fn reverse_pass_easing_is_distinct_from_uneased_local_time() {
1405        let motion = Motion::new(secs(1))
1406            .with_reverse_pass(MotionPass::new(secs(2)).with_easing(|t| t * t))
1407            .direction(Direction::Reverse);
1408        let sample = motion.sample(secs(1));
1409        assert_eq!(sample.local_time, progress(0.5));
1410        assert_eq!(sample.progress, progress(0.75));
1411        assert_eq!(sample.direction, PassDirection::Reverse);
1412    }
1413
1414    #[test]
1415    fn checked_extent_counts_delay_and_asymmetric_passes() {
1416        let base = Motion::new(millis(300))
1417            .with_reverse_pass(MotionPass::new(millis(700)))
1418            .with_delay(millis(200))
1419            .iterations(3);
1420        for (direction, expected) in [
1421            (Direction::Forward, 1_100),
1422            (Direction::Reverse, 2_300),
1423            (Direction::Alternate, 1_500),
1424            (Direction::AlternateReverse, 1_900),
1425        ] {
1426            assert_eq!(
1427                base.clone().direction(direction).checked_extent(),
1428                Some(MotionExtent::Finite(millis(expected)))
1429            );
1430        }
1431        for direction in [Direction::Alternate, Direction::AlternateReverse] {
1432            assert_eq!(
1433                base.clone()
1434                    .iterations(2)
1435                    .direction(direction)
1436                    .checked_extent(),
1437                Some(MotionExtent::Finite(millis(1_200)))
1438            );
1439        }
1440        assert_eq!(
1441            base.repeat_forever().checked_extent(),
1442            Some(MotionExtent::Infinite)
1443        );
1444        assert_eq!(
1445            Motion::new(Duration::MAX).iterations(2).checked_extent(),
1446            None
1447        );
1448        assert_eq!(
1449            Motion::new(Duration::MAX)
1450                .with_delay(Duration::from_nanos(1))
1451                .checked_extent(),
1452            None
1453        );
1454    }
1455
1456    #[test]
1457    fn zero_length_passes_work_in_both_reverse_first_modes() {
1458        let finite = Motion::new(Duration::ZERO)
1459            .with_reverse_pass(MotionPass::new(secs(1)))
1460            .iterations(2)
1461            .direction(Direction::AlternateReverse);
1462        assert_eq!(
1463            finite.sample(Duration::ZERO).direction,
1464            PassDirection::Reverse
1465        );
1466        assert_eq!(finite.sample(Duration::ZERO).progress, Progress::END);
1467        assert_eq!(finite.sample(secs(1)).phase, MotionPhase::Completed);
1468        let forever = Motion::new(Duration::ZERO)
1469            .with_reverse_pass(MotionPass::new(Duration::ZERO))
1470            .repeat_forever()
1471            .direction(Direction::AlternateReverse);
1472        assert_eq!(forever.sample(Duration::MAX).phase, MotionPhase::Inactive);
1473        assert_eq!(forever.sample(Duration::MAX).progress, Progress::END);
1474    }
1475
1476    #[test]
1477    fn zero_duration_combinations_terminate_or_stay_active_as_configured() {
1478        for direction in [
1479            Direction::Forward,
1480            Direction::Reverse,
1481            Direction::Alternate,
1482            Direction::AlternateReverse,
1483        ] {
1484            for (forward, reverse) in [
1485                (Duration::ZERO, Duration::ZERO),
1486                (Duration::ZERO, secs(1)),
1487                (secs(1), Duration::ZERO),
1488            ] {
1489                let motion = Motion::new(forward)
1490                    .with_reverse_pass(MotionPass::new(reverse))
1491                    .iterations(3)
1492                    .direction(direction);
1493                let MotionExtent::Finite(extent) = motion.checked_extent().unwrap() else {
1494                    panic!("finite playback must have finite extent");
1495                };
1496                assert_eq!(motion.sample(extent).phase, MotionPhase::Completed);
1497                assert!(!motion.sample(extent).is_active);
1498
1499                let forever = motion.repeat_forever();
1500                let sample = forever.sample(Duration::MAX);
1501                let applicable_duration = match direction {
1502                    Direction::Forward => forward,
1503                    Direction::Reverse => reverse,
1504                    Direction::Alternate | Direction::AlternateReverse => forward + reverse,
1505                };
1506                assert_eq!(
1507                    sample.phase == MotionPhase::Inactive,
1508                    applicable_duration.is_zero()
1509                );
1510            }
1511        }
1512
1513        let fallback = Motion::new(secs(1)).direction(Direction::Reverse);
1514        assert_eq!(fallback.sample(millis(500)).progress, progress(0.5));
1515        assert_eq!(
1516            fallback.checked_extent(),
1517            Some(MotionExtent::Finite(secs(1)))
1518        );
1519    }
1520
1521    #[test]
1522    #[should_panic(expected = "motion iterations must be at least 1")]
1523    fn zero_iterations_are_invalid() {
1524        let _ = Motion::new(Duration::from_secs(1)).iterations(0);
1525    }
1526    #[test]
1527    fn spring_motion_preserves_overshoot_and_rich_sample_metadata() {
1528        let config = SpringConfig::new(100.0, 6.0, 1.0);
1529        let sampled = Motion::new(secs(1)).with_spring(config);
1530        assert!((1..100).any(|step| { sampled.sample(millis(step * 10)).progress.get() > 1.0 }));
1531
1532        let native: AnyMotion = config.into();
1533        let loose = Motion::spring(config).with_epsilon(0.1);
1534        let cutoff = loose.settle_after;
1535        assert!(native.sample(cutoff).is_active);
1536        let completed = AnyMotion::from(loose).sample_at(Duration::ZERO, cutoff);
1537        assert!(completed.is_complete());
1538        assert_eq!(completed.progress, Progress::END);
1539        assert_eq!(completed.local_time, Progress::END);
1540        assert_eq!(completed.direction, PassDirection::Forward);
1541    }
1542
1543    #[test]
1544    fn resting_progress_uses_the_configured_endpoint() {
1545        let forward = Motion::new(secs(1)).with_easing(|_| 0.5);
1546        assert_eq!(forward.resting_progress(), Progress::END);
1547        assert_eq!(
1548            forward
1549                .clone()
1550                .direction(Direction::Reverse)
1551                .resting_progress(),
1552            Progress::START
1553        );
1554        assert_eq!(
1555            forward
1556                .repeat_forever()
1557                .direction(Direction::Reverse)
1558                .resting_progress(),
1559            Progress::END
1560        );
1561    }
1562}