Skip to main content

gpui_ce_motion/
animation.rs

1//! Prepared clips, keyframes, playback clocks, and explicit timeline orchestration.
2use crate::{Easing, Frame, Spring, Target, Transition, value::sample_transition};
3#[cfg(feature = "gpui")]
4use std::time::Instant;
5use std::{error::Error, fmt, sync::Arc, time::Duration};
6
7/// Invalid prepared animation, timing, or orchestration input.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum AnimationError {
11    /// At least two keyframes are required, at zero and one.
12    Keyframes,
13    /// Times must be finite, strictly increasing, and in zero through one.
14    Times,
15    /// A repeat requires positive cycle duration.
16    ZeroPeriod,
17    /// Speed must be finite.
18    Speed,
19    /// A label is duplicated or missing.
20    Label,
21    /// An infinite clip cannot define a finite sequence cursor.
22    InfiniteSequence,
23    /// A stagger index must belong to a nonempty collection.
24    Index,
25}
26
27impl fmt::Display for AnimationError {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        write!(f, "invalid animation input: {self:?}")
30    }
31}
32
33impl Error for AnimationError {}
34
35/// One timed target. A missing target holds the previous value, or current presentation at zero.
36#[derive(Clone, Copy, Debug, PartialEq)]
37pub struct Keyframe {
38    at: f64,
39    target: Option<Target>,
40    easing: Easing,
41}
42
43impl Keyframe {
44    /// A complete target at normalized progress `at`. Validated when preparing the clip.
45    pub fn new(at: f64, target: Target) -> Self {
46        Self {
47            at,
48            target: Some(target),
49            easing: Easing::default(),
50        }
51    }
52
53    /// A wildcard: current presentation at zero, otherwise hold the preceding target.
54    pub fn current(at: f64) -> Self {
55        Self {
56            at,
57            target: None,
58            easing: Easing::default(),
59        }
60    }
61
62    /// Easing for the segment ending at this keyframe.
63    pub fn easing(mut self, easing: Easing) -> Self {
64        self.easing = easing;
65
66        self
67    }
68}
69
70/// Additional cycles after the first cycle.
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub enum Repeat {
73    /// A finite number of additional cycles. Zero plays once.
74    Count(u32),
75    /// Repeat until stopped or reduced motion snaps the animation.
76    Forever,
77}
78
79impl Default for Repeat {
80    fn default() -> Self {
81        Self::Count(0)
82    }
83}
84
85/// How repeated clips travel between their endpoints.
86#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
87pub enum RepeatMode {
88    /// Restart forward from the original beginning.
89    #[default]
90    Loop,
91    /// Play the same trajectory backwards on alternate cycles.
92    Reverse,
93    /// Reverse targets on alternate cycles, retaining forward easing/physics.
94    Mirror,
95}
96
97#[derive(Clone, Debug, PartialEq)]
98struct Clip {
99    frames: Vec<Keyframe>,
100    duration: Duration,
101    spring: Option<Spring>,
102}
103
104/// A prepared, cheaply cloned clip. Construct once outside rendering; clones share keyframes.
105#[derive(Clone, Debug)]
106pub struct Animation {
107    clip: Arc<Clip>,
108    delay: Duration,
109    repeat: Repeat,
110    mode: RepeatMode,
111    repeat_delay: Duration,
112}
113
114impl Animation {
115    /// Prepares keyframes with normalized, strictly increasing times from zero to one.
116    pub fn keyframes(
117        duration: Duration,
118        frames: impl IntoIterator<Item = Keyframe>,
119    ) -> Result<Self, AnimationError> {
120        let frames: Vec<_> = frames.into_iter().collect();
121
122        if frames.len() < 2 || frames.first().unwrap().at != 0.0 || frames.last().unwrap().at != 1.0
123        {
124            return Err(AnimationError::Keyframes);
125        }
126
127        if frames
128            .iter()
129            .any(|f| !f.at.is_finite() || !(0.0..=1.0).contains(&f.at))
130            || frames.windows(2).any(|p| p[0].at >= p[1].at)
131        {
132            return Err(AnimationError::Times);
133        }
134
135        Ok(Self {
136            clip: Arc::new(Clip {
137                frames,
138                duration,
139                spring: None,
140            }),
141            delay: Duration::ZERO,
142            repeat: Repeat::default(),
143            mode: RepeatMode::Loop,
144            repeat_delay: Duration::ZERO,
145        })
146    }
147
148    /// A tween from the current presentation, with explicit duration and easing.
149    pub fn to(target: Target, duration: Duration, easing: Easing) -> Self {
150        Self::keyframes(
151            duration,
152            [
153                Keyframe::current(0.0),
154                Keyframe::new(1.0, target).easing(easing),
155            ],
156        )
157        .expect("valid two-frame clip")
158    }
159
160    /// A finite physical spring clip from current position and velocity.
161    /// `duration` is an explicit deadline: the endpoint snaps exactly at that time.
162    /// Use ordinary `Motion` springs for unconstrained, tolerance-based settling.
163    pub fn spring(target: Target, spring: Spring, duration: Duration) -> Self {
164        let mut animation = Self::to(target, duration, Easing::default());
165
166        Arc::make_mut(&mut animation.clip).spring = Some(spring);
167
168        animation
169    }
170
171    /// Holds the first presentation before playback starts.
172    pub fn delay(mut self, duration: Duration) -> Self {
173        self.delay = duration;
174
175        self
176    }
177
178    /// Sets repeat count/mode and the hold between cycles.
179    pub fn repeat(
180        mut self,
181        repeat: Repeat,
182        mode: RepeatMode,
183        delay: Duration,
184    ) -> Result<Self, AnimationError> {
185        if self.clip.duration.is_zero() && repeat != Repeat::Count(0) {
186            return Err(AnimationError::ZeroPeriod);
187        }
188
189        self.repeat = repeat;
190        self.mode = mode;
191        self.repeat_delay = delay;
192
193        Ok(self)
194    }
195
196    /// Total duration, including initial/inter-cycle holds; `None` denotes infinity.
197    pub fn duration(&self) -> Option<Duration> {
198        match self.repeat {
199            Repeat::Forever => None,
200            Repeat::Count(count) => Some(
201                self.delay
202                    .saturating_add(
203                        self.clip
204                            .duration
205                            .saturating_mul(count)
206                            .saturating_add(self.clip.duration),
207                    )
208                    .saturating_add(self.repeat_delay.saturating_mul(count)),
209            ),
210        }
211    }
212
213    fn same(&self, other: &Self) -> bool {
214        Arc::ptr_eq(&self.clip, &other.clip)
215            && self.delay == other.delay
216            && self.repeat == other.repeat
217            && self.mode == other.mode
218            && self.repeat_delay == other.repeat_delay
219    }
220
221    fn resolve(&self, time: Duration, origin: Frame, force_end: bool) -> (Frame, u64, bool) {
222        let duration = self.clip.duration.as_secs_f64();
223        let period = duration + self.repeat_delay.as_secs_f64();
224        let relative = time.saturating_sub(self.delay).as_secs_f64();
225        let done = force_end || self.duration().is_some_and(|end| time >= end);
226
227        let iteration = if done {
228            match self.repeat {
229                Repeat::Count(n) => u64::from(n),
230                Repeat::Forever => 0,
231            }
232        } else if period > 0.0 {
233            (relative / period).floor().min(u64::MAX as f64) as u64
234        } else {
235            0
236        };
237
238        let local = if done {
239            duration
240        } else {
241            (relative - iteration as f64 * period).clamp(0.0, duration)
242        };
243
244        let mut progress = if duration > 0.0 {
245            local / duration
246        } else {
247            1.0
248        };
249
250        if time < self.delay && !done {
251            progress = 0.0;
252        }
253
254        let odd = !iteration.is_multiple_of(2);
255        let reverse = odd && self.mode == RepeatMode::Reverse;
256
257        if reverse {
258            progress = 1.0 - progress;
259        }
260
261        let mut from = origin.target().inherit(
262            self.clip
263                .frames
264                .last()
265                .and_then(|f| f.target)
266                .unwrap_or_default(),
267        );
268
269        from = self.clip.frames[0].target.unwrap_or(from).inherit(from);
270        let first = from;
271
272        let final_target = self.clip.frames.iter().fold(from, |previous, frame| {
273            frame.target.unwrap_or(previous).inherit(previous)
274        });
275
276        if let Some(spring) = self.clip.spring {
277            let (start, end) = if odd && self.mode == RepeatMode::Mirror {
278                (final_target, first)
279            } else {
280                (first, final_target)
281            };
282
283            let velocity = if iteration == 0 {
284                origin_velocities(origin)
285            } else {
286                [0.0; 6]
287            };
288
289            let mut values = [0.0; 6];
290            let mut velocities = [0.0; 6];
291
292            for ix in 0..6 {
293                let (v, speed, _) = sample_transition(
294                    start.values()[ix],
295                    velocity[ix],
296                    end.values()[ix],
297                    Transition::Spring(spring),
298                    progress * duration,
299                );
300
301                values[ix] = if progress == 0.0 {
302                    start.values()[ix]
303                } else if progress == 1.0 {
304                    end.values()[ix]
305                } else {
306                    v
307                };
308                velocities[ix] = if progress == 1.0 || done || time < self.delay {
309                    0.0
310                } else if reverse {
311                    -speed
312                } else {
313                    speed
314                };
315            }
316
317            return (
318                Frame::animated(
319                    Target::from_values(values, start.mask() | end.mask() | 7),
320                    velocities,
321                    !done,
322                    false,
323                ),
324                iteration,
325                done,
326            );
327        }
328
329        let mirrored = odd && self.mode == RepeatMode::Mirror;
330        let sample_progress = if mirrored { 1.0 - progress } else { progress };
331
332        let upper = self
333            .clip
334            .frames
335            .partition_point(|frame| frame.at < sample_progress)
336            .clamp(1, self.clip.frames.len() - 1);
337
338        let mut a = first;
339
340        for frame in self.clip.frames.iter().take(upper).skip(1) {
341            a = frame.target.unwrap_or(a).inherit(a);
342        }
343
344        let mut b = self.clip.frames[upper].target.unwrap_or(a).inherit(a);
345        let lo = self.clip.frames[upper - 1].at;
346        let hi = self.clip.frames[upper].at;
347        let mut u = (sample_progress - lo) / (hi - lo);
348
349        if mirrored {
350            std::mem::swap(&mut a, &mut b);
351            u = 1.0 - u;
352        }
353
354        let span = (hi - lo) * duration;
355        let easing = self.clip.frames[upper].easing;
356        let (weight, derivative) = easing.sample(u.clamp(0.0, 1.0));
357        let av = a.values();
358        let bv = b.values();
359
360        let values = std::array::from_fn(|ix| {
361            if u <= 0.0 {
362                av[ix]
363            } else if u >= 1.0 {
364                bv[ix]
365            } else {
366                crate::value::finite(av[ix] + (bv[ix] - av[ix]) * weight)
367            }
368        });
369
370        let velocity = std::array::from_fn(|ix| {
371            if done || time < self.delay || local >= duration || span == 0.0 {
372                0.0
373            } else {
374                crate::value::finite(
375                    (bv[ix] - av[ix]) * derivative / span * if reverse { -1.0 } else { 1.0 },
376                )
377            }
378        });
379
380        (
381            Frame::animated(
382                Target::from_values(values, a.mask() | b.mask() | 7),
383                velocity,
384                !done,
385                false,
386            ),
387            iteration,
388            done,
389        )
390    }
391}
392
393fn origin_velocities(frame: Frame) -> [f64; 6] {
394    let basic = frame.velocity();
395    let layout = frame.layout_velocity();
396
397    [
398        basic[0],
399        basic[1],
400        basic[2],
401        layout[0].unwrap_or(0.0),
402        layout[1].unwrap_or(0.0),
403        layout[2].unwrap_or(0.0),
404    ]
405}
406
407/// An explicit monotonic playback clock. It performs no scheduling.
408#[derive(Clone, Debug)]
409pub struct Playback {
410    anchor: Duration,
411    offset: f64,
412    speed: f64,
413    paused: bool,
414    last: Duration,
415}
416
417impl Default for Playback {
418    fn default() -> Self {
419        Self {
420            anchor: Duration::ZERO,
421            offset: 0.0,
422            speed: 1.0,
423            paused: false,
424            last: Duration::ZERO,
425        }
426    }
427}
428
429impl Playback {
430    /// Virtual playhead at a monotonic real timestamp. Backward real timestamps are held.
431    pub fn time(&mut self, now: Duration) -> Duration {
432        let now = now.max(self.last);
433
434        self.last = now;
435        let seconds = self.offset
436            + if self.paused {
437                0.0
438            } else {
439                now.saturating_sub(self.anchor).as_secs_f64() * self.speed
440            };
441
442        Duration::try_from_secs_f64(seconds.max(0.0)).unwrap_or(Duration::MAX)
443    }
444
445    /// Freezes at the current playhead.
446    pub fn pause(&mut self, now: Duration) {
447        self.offset = self.time(now).as_secs_f64();
448        self.anchor = now.max(self.last).max(self.anchor);
449        self.paused = true;
450    }
451
452    /// Resumes from the current playhead.
453    pub fn play(&mut self, now: Duration) {
454        if self.paused {
455            self.anchor = now.max(self.last).max(self.anchor);
456            self.paused = false;
457        }
458    }
459
460    /// Sets the playhead without changing speed or paused state.
461    pub fn seek(&mut self, time: Duration, now: Duration) {
462        self.last = self.last.max(now);
463        self.offset = time.as_secs_f64();
464        self.anchor = now.max(self.last).max(self.anchor);
465    }
466
467    /// Sets finite playback speed. Zero holds; negative speed plays toward zero.
468    pub fn set_speed(&mut self, speed: f64, now: Duration) -> Result<(), AnimationError> {
469        if !speed.is_finite() {
470            return Err(AnimationError::Speed);
471        }
472
473        self.offset = self.time(now).as_secs_f64();
474        self.anchor = now.max(self.last).max(self.anchor);
475        self.speed = speed;
476
477        Ok(())
478    }
479
480    /// Whether this clock advances.
481    pub fn is_playing(&self) -> bool {
482        !self.paused && self.speed != 0.0
483    }
484}
485
486/// Retained playback for a prepared clip. New clips interrupt from current presentation.
487#[derive(Debug)]
488pub struct AnimationState {
489    clip: Option<Animation>,
490    clock: Playback,
491    origin: Frame,
492    current: Frame,
493    complete: bool,
494    force_end: bool,
495    iteration: u64,
496    #[cfg(feature = "gpui")]
497    pub(crate) epoch: Instant,
498}
499
500impl Default for AnimationState {
501    fn default() -> Self {
502        Self::new(Target::default())
503    }
504}
505
506impl AnimationState {
507    /// Seeds the presentation before the first clip. No work or timers are started.
508    pub fn new(initial: Target) -> Self {
509        Self {
510            clip: None,
511            clock: Playback::default(),
512            origin: Frame::at(initial),
513            current: Frame::at(initial),
514            complete: false,
515            force_end: false,
516            iteration: 0,
517            #[cfg(feature = "gpui")]
518            epoch: Instant::now(),
519        }
520    }
521
522    /// Samples a retained prepared clip; cloning an `Animation` preserves its identity.
523    /// Rebuilding a clip intentionally restarts it. Reduced motion finishes even infinite clips.
524    pub fn sample(&mut self, animation: &Animation, now: Duration, reduced_motion: bool) -> Frame {
525        if !self.clip.as_ref().is_some_and(|old| old.same(animation)) {
526            if let Some(old) = &self.clip {
527                let frame = old
528                    .resolve(self.clock.time(now), self.origin, self.force_end)
529                    .0;
530
531                let factor = if self.clock.is_playing() {
532                    self.clock.speed
533                } else {
534                    0.0
535                };
536
537                self.current = Frame::animated(
538                    frame.target(),
539                    origin_velocities(frame).map(|v| v * factor),
540                    frame.is_animating(),
541                    false,
542                );
543            }
544
545            self.origin = self.current;
546            let first = self.clip.is_none();
547
548            self.clip = Some(animation.clone());
549
550            if first {
551                self.clock.anchor = now.max(self.clock.last);
552                self.clock.last = self.clock.anchor;
553            } else {
554                self.clock.seek(Duration::ZERO, now);
555            }
556
557            self.complete = false;
558
559            if !first {
560                self.force_end = false;
561            }
562        }
563
564        self.force_end |= reduced_motion;
565        let time = self.clock.time(now);
566        let (frame, iteration, finished) = animation.resolve(time, self.origin, self.force_end);
567        let backward_end = self.clock.speed < 0.0 && time.is_zero();
568        let complete = finished || backward_end;
569
570        self.current = Frame::animated(
571            frame.target(),
572            origin_velocities(frame).map(|v| {
573                v * if self.clock.is_playing() {
574                    self.clock.speed
575                } else {
576                    0.0
577                }
578            }),
579            !complete && self.clock.is_playing(),
580            complete && !self.complete,
581        );
582        self.complete = complete;
583        self.iteration = iteration;
584
585        self.current
586    }
587
588    /// Pauses playback; call the host's notify method after controls change.
589    pub fn pause(&mut self, now: Duration) {
590        self.clock.pause(now);
591    }
592
593    /// Resumes playback.
594    pub fn play(&mut self, now: Duration) {
595        self.clock.play(now);
596    }
597
598    /// Stops at the current presentation. Use play to resume, or restart to reset.
599    pub fn stop(&mut self, now: Duration) {
600        self.pause(now);
601    }
602
603    /// Seeks to a virtual playhead, including initial delay and repeats.
604    pub fn seek(&mut self, time: Duration, now: Duration) {
605        self.clock.seek(time, now);
606    }
607
608    /// Changes playback speed; negative speed is supported after seeking forward.
609    pub fn set_speed(&mut self, speed: f64, now: Duration) -> Result<(), AnimationError> {
610        self.clock.set_speed(speed, now)
611    }
612
613    /// Restarts the same clip from its captured origin.
614    pub fn restart(&mut self, now: Duration) {
615        self.clock.seek(Duration::ZERO, now);
616        self.clock.play(now);
617        self.force_end = false;
618        self.complete = false;
619    }
620
621    /// Finishes immediately on the next sample, including an infinite clip.
622    pub fn finish(&mut self) {
623        self.force_end = true;
624    }
625
626    /// Current repeat iteration, zero based.
627    pub fn iteration(&self) -> u64 {
628        self.iteration
629    }
630
631    /// Whether controls permit time to advance.
632    pub fn is_playing(&self) -> bool {
633        self.clock.is_playing()
634    }
635}
636
637/// Origin of a staggered group.
638#[derive(Clone, Copy, Debug, PartialEq, Eq)]
639pub enum StaggerFrom {
640    /// First child starts first.
641    Start,
642    /// Last child starts first.
643    End,
644    /// Center children start first.
645    Center,
646}
647
648/// Computes a child's delay without allocating. Center origins support half-step offsets.
649pub fn stagger(
650    step: Duration,
651    ix: usize,
652    count: usize,
653    from: StaggerFrom,
654) -> Result<Duration, AnimationError> {
655    if count == 0 || ix >= count {
656        return Err(AnimationError::Index);
657    }
658
659    let distance = match from {
660        StaggerFrom::Start => ix as f64,
661        StaggerFrom::End => (count - 1 - ix) as f64,
662        StaggerFrom::Center => (ix as f64 - (count - 1) as f64 / 2.0).abs(),
663    };
664
665    Ok(Duration::try_from_secs_f64(step.as_secs_f64() * distance).unwrap_or(Duration::MAX))
666}
667
668#[derive(Debug)]
669struct Track {
670    at: Duration,
671    animation: Animation,
672    state: AnimationState,
673}
674
675/// A retained, allocation-free-at-sample timeline. Tracks target separate caller-owned elements.
676#[derive(Debug, Default)]
677pub struct Timeline {
678    tracks: Vec<Track>,
679    frames: Vec<Frame>,
680    clock: Playback,
681    cursor: Option<Duration>,
682    labels: Vec<(String, Duration)>,
683    active: bool,
684}
685
686impl Timeline {
687    /// Adds an absolute-positioned track. Returns its stable index.
688    pub fn at(&mut self, at: Duration, animation: Animation) -> usize {
689        let end = animation
690            .duration()
691            .map(|duration| at.saturating_add(duration));
692
693        self.cursor = end;
694        let mut state = AnimationState::default();
695
696        state.sample(&animation, Duration::ZERO, false);
697        self.frames.push(Frame::at(Target::default()));
698        self.tracks.push(Track {
699            at,
700            animation,
701            state,
702        });
703
704        self.tracks.len() - 1
705    }
706
707    /// Appends after the previously inserted track; an infinite predecessor is rejected.
708    pub fn then(&mut self, animation: Animation) -> Result<usize, AnimationError> {
709        let at = if self.tracks.is_empty() {
710            Duration::ZERO
711        } else {
712            self.cursor.ok_or(AnimationError::InfiniteSequence)?
713        };
714
715        Ok(self.at(at, animation))
716    }
717
718    /// Records a label at the current finite sequence cursor.
719    pub fn label(&mut self, name: impl Into<String>) -> Result<(), AnimationError> {
720        let name = name.into();
721
722        if self.labels.iter().any(|(n, _)| *n == name) {
723            return Err(AnimationError::Label);
724        }
725
726        let at = if self.tracks.is_empty() {
727            Duration::ZERO
728        } else {
729            self.cursor.ok_or(AnimationError::InfiniteSequence)?
730        };
731
732        self.labels.push((name, at));
733
734        Ok(())
735    }
736
737    /// Adds an overlapping track relative to a previously recorded label.
738    pub fn at_label(
739        &mut self,
740        name: &str,
741        offset: Duration,
742        animation: Animation,
743    ) -> Result<usize, AnimationError> {
744        let at = self
745            .labels
746            .iter()
747            .find(|(n, _)| n == name)
748            .ok_or(AnimationError::Label)?
749            .1;
750
751        Ok(self.at(at.saturating_add(offset), animation))
752    }
753
754    /// Starts this timeline's clock at zero at an application timestamp.
755    pub fn restart(&mut self, now: Duration) {
756        self.clock.seek(Duration::ZERO, now);
757        self.clock.play(now);
758
759        for track in &mut self.tracks {
760            track.state.restart(now);
761        }
762    }
763
764    /// Pauses the whole timeline.
765    pub fn pause(&mut self, now: Duration) {
766        self.clock.pause(now);
767    }
768
769    /// Resumes the whole timeline.
770    pub fn play(&mut self, now: Duration) {
771        self.clock.play(now);
772    }
773
774    /// Scrubs all tracks, including backward seeks.
775    pub fn seek(&mut self, time: Duration, now: Duration) {
776        self.clock.seek(time, now);
777    }
778
779    /// Changes the whole timeline playback rate, including negative playback.
780    pub fn set_speed(&mut self, speed: f64, now: Duration) -> Result<(), AnimationError> {
781        self.clock.set_speed(speed, now)
782    }
783
784    /// Samples all tracks into a retained buffer. No allocation occurs here.
785    pub fn sample(&mut self, now: Duration, reduced_motion: bool) -> &[Frame] {
786        let playhead = self.clock.time(now);
787        let playing = self.clock.is_playing() && !(self.clock.speed < 0.0 && playhead.is_zero());
788
789        self.active = false;
790
791        for (track, output) in self.tracks.iter_mut().zip(&mut self.frames) {
792            let local = playhead.saturating_sub(track.at);
793
794            track.state.seek(local, now);
795            let frame = track.state.sample(&track.animation, now, reduced_motion);
796            let pending = playhead < track.at && !reduced_motion;
797
798            self.active |= (pending || frame.is_animating()) && playing;
799            *output = Frame::animated(
800                frame.target(),
801                origin_velocities(frame).map(|v| {
802                    if pending || !playing {
803                        0.0
804                    } else {
805                        v * self.clock.speed
806                    }
807                }),
808                !pending && frame.is_animating() && playing,
809                !pending && frame.just_completed(),
810            );
811        }
812
813        &self.frames
814    }
815
816    /// Whether sampled tracks or future tracks require another frame.
817    pub fn is_animating(&self) -> bool {
818        self.active
819    }
820}
821
822impl PartialEq for Animation {
823    fn eq(&self, other: &Self) -> bool {
824        self.same(other)
825    }
826}