Skip to main content

gpui_ce_motion/
typed_animation.rs

1//! Typed prepared tracks over the existing per-channel playback and spring solver.
2use crate::{
3    Animation, AnimationError, AnimationState, Easing, Keyframe, Playback, Repeat, RepeatMode,
4    Spring, Target,
5};
6
7use std::{
8    any::Any,
9    fmt::Debug,
10    marker::PhantomData,
11    sync::atomic::{AtomicU64, Ordering},
12    time::Duration,
13};
14
15/// Fixed numeric representation of a typed animation value. Implementations must
16/// return finite channels and reconstruct drawable values without allocating.
17/// `compatible` identifies an unchanged property mask; keyframes cannot silently
18/// introduce/remove properties. Implement this for application-owned value types.
19pub trait MotionData<const N: usize>: Copy + Debug + PartialEq + 'static {
20    /// Numeric channels, including unclamped spring presentation where available.
21    fn channels(self) -> [f64; N];
22
23    /// Reconstructs a value using this value's property mask and sampled channels.
24    fn with_channels(self, values: [f64; N]) -> Self;
25
26    /// Whether both values declare the same properties.
27    fn compatible(self, other: Self) -> bool;
28}
29
30impl MotionData<1> for f64 {
31    fn channels(self) -> [f64; 1] {
32        [self]
33    }
34
35    fn with_channels(self, values: [f64; 1]) -> Self {
36        values[0]
37    }
38
39    fn compatible(self, _: Self) -> bool {
40        true
41    }
42}
43
44impl MotionData<6> for Target {
45    fn channels(self) -> [f64; 6] {
46        self.values()
47    }
48
49    fn with_channels(self, values: [f64; 6]) -> Self {
50        Self::from_values(values, self.mask())
51    }
52
53    fn compatible(self, other: Self) -> bool {
54        self.mask() == other.mask()
55    }
56}
57
58/// A normalized typed keyframe; `current` captures each channel's presentation.
59#[derive(Clone, Copy, Debug)]
60pub struct TypedKeyframe<T> {
61    at: f64,
62    value: Option<T>,
63    easing: Easing,
64}
65
66impl<T> TypedKeyframe<T> {
67    /// Declares a value at normalized time `at`.
68    pub fn new(at: f64, value: T) -> Self {
69        Self {
70            at,
71            value: Some(value),
72            easing: Easing::Linear,
73        }
74    }
75
76    /// Captures current presentation at the first frame; later occurrences hold.
77    pub fn current(at: f64) -> Self {
78        Self {
79            at,
80            value: None,
81            easing: Easing::Linear,
82        }
83    }
84
85    /// Easing from the preceding frame to this frame.
86    pub fn easing(mut self, easing: Easing) -> Self {
87        self.easing = easing;
88
89        self
90    }
91}
92
93fn scalar(value: f64) -> Target {
94    Target::from_values([value, 0.0, 1.0, 0.0, 0.0, 0.0], 1)
95}
96
97/// Immutable typed clip. Channels use the same solver as ordinary `Animation`;
98/// interruption preserves each spring channel's own velocity. Prepare once.
99#[derive(Clone, Debug)]
100pub struct TypedAnimation<T, const N: usize> {
101    channels: [Animation; N],
102    template: T,
103}
104
105impl<T: MotionData<N>, const N: usize> TypedAnimation<T, N> {
106    /// Validates timing and consistent masks before preparing numeric channels.
107    /// Uses `AnimationError::Keyframes` for non-finite/mismatched value channels.
108    pub fn keyframes(
109        duration: Duration,
110        frames: impl IntoIterator<Item = TypedKeyframe<T>>,
111    ) -> Result<Self, AnimationError> {
112        let frames: Vec<_> = frames.into_iter().collect();
113
114        let template = frames
115            .iter()
116            .find_map(|frame| frame.value)
117            .ok_or(AnimationError::Keyframes)?;
118
119        if N == 0
120            || frames.iter().filter_map(|frame| frame.value).any(|value| {
121                !template.compatible(value) || value.channels().iter().any(|v| !v.is_finite())
122            })
123        {
124            return Err(AnimationError::Keyframes);
125        }
126
127        let channels = (0..N)
128            .map(|i| {
129                Animation::keyframes(
130                    duration,
131                    frames.iter().map(|frame| {
132                        let key = frame.value.map_or_else(
133                            || Keyframe::current(frame.at),
134                            |value| Keyframe::new(frame.at, scalar(value.channels()[i])),
135                        );
136
137                        key.easing(frame.easing)
138                    }),
139                )
140            })
141            .collect::<Result<Vec<_>, _>>()?
142            .try_into()
143            .unwrap_or_else(|_| unreachable!());
144
145        Ok(Self { channels, template })
146    }
147
148    /// Captures the current value and tweens to a typed destination.
149    pub fn to(value: T, duration: Duration, easing: Easing) -> Result<Self, AnimationError> {
150        let values = value.channels();
151
152        if N == 0 || values.iter().any(|value| !value.is_finite()) {
153            return Err(AnimationError::Keyframes);
154        }
155
156        let channels = std::array::from_fn(|i| Animation::to(scalar(values[i]), duration, easing));
157
158        Ok(Self {
159            channels,
160            template: value,
161        })
162    }
163
164    /// Creates per-channel physical springs with a finite deadline.
165    pub fn spring(value: T, spring: Spring, deadline: Duration) -> Result<Self, AnimationError> {
166        if N == 0 || value.channels().iter().any(|v| !v.is_finite()) {
167            return Err(AnimationError::Keyframes);
168        }
169
170        let channels = std::array::from_fn(|i| {
171            Animation::spring(scalar(value.channels()[i]), spring, deadline)
172        });
173
174        Ok(Self {
175            channels,
176            template: value,
177        })
178    }
179
180    /// Adds a hold before playback.
181    pub fn delay(mut self, duration: Duration) -> Self {
182        self.channels = self.channels.map(|channel| channel.delay(duration));
183
184        self
185    }
186
187    /// Configures finite/forever repeats identically for all channels.
188    pub fn repeat(
189        mut self,
190        repeat: Repeat,
191        mode: RepeatMode,
192        delay: Duration,
193    ) -> Result<Self, AnimationError> {
194        self.channels = self
195            .channels
196            .into_iter()
197            .map(|channel| channel.repeat(repeat, mode, delay))
198            .collect::<Result<Vec<_>, _>>()?
199            .try_into()
200            .unwrap_or_else(|_| unreachable!());
201
202        Ok(self)
203    }
204
205    /// Total duration including repeats/delays; `None` denotes forever.
206    pub fn duration(&self) -> Option<Duration> {
207        self.channels[0].duration()
208    }
209}
210
211/// Sampled typed presentation and scheduling state.
212#[derive(Clone, Copy, Debug)]
213pub struct TypedFrame<T> {
214    value: T,
215    active: bool,
216    completed: bool,
217}
218
219impl<T: Copy> TypedFrame<T> {
220    /// Current drawable value.
221    pub fn value(self) -> T {
222        self.value
223    }
224
225    /// Whether another frame is required.
226    pub fn is_animating(self) -> bool {
227        self.active
228    }
229
230    /// Completion edge consumed by this sample.
231    pub fn just_completed(self) -> bool {
232        self.completed
233    }
234}
235
236/// Retained independent channel trajectories. No allocation occurs in sampling.
237#[derive(Debug)]
238pub struct TypedState<T, const N: usize> {
239    states: [AnimationState; N],
240    frame: TypedFrame<T>,
241    #[cfg(feature = "gpui")]
242    epoch: Option<std::time::Instant>,
243}
244
245impl<T: MotionData<N>, const N: usize> TypedState<T, N> {
246    /// Seeds all channels. Panics for a non-finite seed or an empty representation.
247    pub fn new(value: T) -> Self {
248        assert!(
249            N > 0 && value.channels().iter().all(|v| v.is_finite()),
250            "invalid typed seed"
251        );
252
253        Self {
254            states: std::array::from_fn(|i| AnimationState::new(scalar(value.channels()[i]))),
255            frame: TypedFrame {
256                value,
257                active: false,
258                completed: false,
259            },
260            #[cfg(feature = "gpui")]
261            epoch: None,
262        }
263    }
264
265    /// Samples at one explicit monotonic epoch. Panics if the clip's mask differs
266    /// from the seed's mask; create a new retained state for a different shape.
267    pub fn sample(
268        &mut self,
269        clip: &TypedAnimation<T, N>,
270        now: Duration,
271        reduced: bool,
272    ) -> TypedFrame<T> {
273        assert!(
274            self.frame.value.compatible(clip.template),
275            "typed property masks must match"
276        );
277        let samples = std::array::from_fn::<_, N, _>(|i| {
278            self.states[i].sample(&clip.channels[i], now, reduced)
279        });
280
281        let active = samples.iter().any(|frame| frame.is_animating());
282
283        self.frame = TypedFrame {
284            value: clip
285                .template
286                .with_channels(samples.map(|frame| frame.target().values()[0])),
287            active,
288            completed: !active && samples.iter().any(|frame| frame.just_completed()),
289        };
290
291        self.frame
292    }
293
294    /// Last sampled value.
295    pub fn value(&self) -> T {
296        self.frame.value
297    }
298
299    /// Per-channel physical velocity.
300    pub fn velocities(&self) -> [f64; N] {
301        std::array::from_fn(|i| self.states[i].current_velocity())
302    }
303
304    /// Pauses every channel.
305    pub fn pause(&mut self, now: Duration) {
306        for state in &mut self.states {
307            state.pause(now);
308        }
309    }
310
311    /// Resumes every channel.
312    pub fn play(&mut self, now: Duration) {
313        for state in &mut self.states {
314            state.play(now);
315        }
316    }
317
318    /// Seeks every channel.
319    pub fn seek(&mut self, time: Duration, now: Duration) {
320        for state in &mut self.states {
321            state.seek(time, now);
322        }
323    }
324
325    /// Restarts every channel; cancels previous completion observers.
326    pub fn restart(&mut self, now: Duration) {
327        for state in &mut self.states {
328            state.restart(now);
329        }
330    }
331
332    /// Validates and changes speed for all channels transactionally.
333    pub fn set_speed(&mut self, speed: f64, now: Duration) -> Result<(), AnimationError> {
334        if !speed.is_finite() {
335            return Err(AnimationError::Speed);
336        }
337
338        for state in &mut self.states {
339            state.set_speed(speed, now)?;
340        }
341
342        Ok(())
343    }
344
345    /// Completes all channels at the next sample, including forever clips.
346    pub fn finish(&mut self) {
347        for state in &mut self.states {
348            state.finish();
349        }
350    }
351
352    /// Observes completion/cancellation. All channels share the same clip timing.
353    pub fn completion(&mut self) -> crate::Completion {
354        self.states[0].completion()
355    }
356
357    /// Whether the last sample requires another frame.
358    pub fn is_animating(&self) -> bool {
359        self.frame.active
360    }
361
362    /// Native executor time for controls; zero before the first native frame.
363    #[cfg(feature = "gpui")]
364    pub fn now(&self, cx: &gpui::App) -> Duration {
365        self.epoch.map_or(Duration::ZERO, |epoch| {
366            cx.background_executor()
367                .now()
368                .saturating_duration_since(epoch)
369        })
370    }
371
372    /// Samples on stock CE and schedules only while active. Do not mix clocks.
373    #[cfg(feature = "gpui")]
374    pub fn frame(
375        &mut self,
376        clip: &TypedAnimation<T, N>,
377        window: &mut gpui::Window,
378        cx: &gpui::App,
379    ) -> TypedFrame<T> {
380        let instant = cx.background_executor().now();
381        let epoch = *self.epoch.get_or_insert(instant);
382
383        let frame = self.sample(
384            clip,
385            instant.saturating_duration_since(epoch),
386            cx.reduce_motion(),
387        );
388
389        if frame.active {
390            window.request_animation_frame();
391        }
392
393        frame
394    }
395}
396
397trait ErasedTrack: Debug {
398    fn sample(&mut self, time: Duration, now: Duration, speed: f64, reduced: bool) -> bool;
399
400    fn restart(&mut self);
401
402    fn as_any(&self) -> &dyn Any;
403}
404
405#[derive(Debug)]
406struct Track<T, const N: usize> {
407    clip: TypedAnimation<T, N>,
408    state: TypedState<T, N>,
409    start: Duration,
410}
411
412impl<T: MotionData<N>, const N: usize> ErasedTrack for Track<T, N> {
413    fn sample(&mut self, time: Duration, now: Duration, speed: f64, reduced: bool) -> bool {
414        self.state
415            .set_speed(speed, now)
416            .expect("validated timeline speed");
417        self.state.seek(time.saturating_sub(self.start), now);
418        self.state.sample(&self.clip, now, reduced);
419
420        (time < self.start && !reduced)
421            || !self.state.states.iter().all(|state| state.is_complete())
422    }
423
424    fn restart(&mut self) {
425        self.state.restart(Duration::ZERO);
426    }
427
428    fn as_any(&self) -> &dyn Any {
429        self
430    }
431}
432
433static NEXT_TIMELINE: AtomicU64 = AtomicU64::new(1);
434
435/// Typed track identity, scoped to the timeline that created it.
436#[derive(Debug)]
437pub struct TrackId<T, const N: usize> {
438    owner: u64,
439    index: usize,
440    marker: PhantomData<T>,
441}
442
443impl<T, const N: usize> Copy for TrackId<T, N> {}
444
445impl<T, const N: usize> Clone for TrackId<T, N> {
446    fn clone(&self) -> Self {
447        *self
448    }
449}
450
451/// A heterogeneous timeline of scalar, color, style, SVG and target tracks.
452/// Track setup allocates; samples reuse each track's state. Deferred tracks keep
453/// scheduling even before their first visible sample. Ownership is explicit.
454#[derive(Debug)]
455pub struct TypedTimeline {
456    owner: u64,
457    tracks: Vec<Box<dyn ErasedTrack>>,
458    clock: Playback,
459    cursor: Option<Duration>,
460    labels: Vec<(String, Duration)>,
461    active: bool,
462    completed: bool,
463    observers: crate::completion::CompletionSource,
464    #[cfg(feature = "gpui")]
465    epoch: Option<std::time::Instant>,
466}
467
468impl Default for TypedTimeline {
469    fn default() -> Self {
470        Self {
471            owner: NEXT_TIMELINE.fetch_add(1, Ordering::Relaxed),
472            tracks: Vec::new(),
473            clock: Playback::default(),
474            cursor: Some(Duration::ZERO),
475            labels: Vec::new(),
476            active: false,
477            completed: false,
478            observers: Default::default(),
479            #[cfg(feature = "gpui")]
480            epoch: None,
481        }
482    }
483}
484
485impl TypedTimeline {
486    /// Adds a typed track at an absolute playhead position, with its initial value.
487    pub fn at<T: MotionData<N>, const N: usize>(
488        &mut self,
489        start: Duration,
490        initial: T,
491        clip: TypedAnimation<T, N>,
492    ) -> TrackId<T, N> {
493        assert!(
494            initial.compatible(clip.template),
495            "typed property masks must match"
496        );
497        self.observers.resolve(crate::CompletionStatus::Cancelled);
498        self.cursor = clip
499            .duration()
500            .map(|duration| start.saturating_add(duration));
501        let id = TrackId {
502            owner: self.owner,
503            index: self.tracks.len(),
504            marker: PhantomData,
505        };
506
507        let mut state = TypedState::new(initial);
508
509        state.sample(&clip, Duration::ZERO, false);
510        self.tracks.push(Box::new(Track { state, clip, start }));
511        self.completed = false;
512
513        id
514    }
515
516    fn offset(base: Option<Duration>, seconds: f64) -> Result<Duration, AnimationError> {
517        let base = base.ok_or(AnimationError::Times)?;
518        let value = base.as_secs_f64() + seconds;
519
520        Duration::try_from_secs_f64(value).map_err(|_| AnimationError::Times)
521    }
522
523    /// Adds after the preceding track's end, with positive delay or negative overlap.
524    /// Fails without mutation after a forever track or for a negative/invalid time.
525    pub fn after<T: MotionData<N>, const N: usize>(
526        &mut self,
527        seconds: f64,
528        initial: T,
529        clip: TypedAnimation<T, N>,
530    ) -> Result<TrackId<T, N>, AnimationError> {
531        let at = Self::offset(self.cursor, seconds)?;
532
533        Ok(self.at(at, initial, clip))
534    }
535
536    /// Records a unique label at an absolute position.
537    pub fn label(&mut self, name: impl Into<String>, at: Duration) -> Result<(), AnimationError> {
538        let name = name.into();
539
540        if self.labels.iter().any(|(old, _)| *old == name) {
541            return Err(AnimationError::Label);
542        }
543
544        self.labels.push((name, at));
545
546        Ok(())
547    }
548
549    /// Adds at a label with a signed seconds offset; missing labels fail transactionally.
550    pub fn at_label<T: MotionData<N>, const N: usize>(
551        &mut self,
552        name: &str,
553        seconds: f64,
554        initial: T,
555        clip: TypedAnimation<T, N>,
556    ) -> Result<TrackId<T, N>, AnimationError> {
557        let at = self
558            .labels
559            .iter()
560            .find(|(label, _)| label == name)
561            .map(|(_, at)| *at)
562            .ok_or(AnimationError::Label)?;
563
564        Ok(self.at(Self::offset(Some(at), seconds)?, initial, clip))
565    }
566
567    /// Samples all tracks and dispatches completion observers at the end.
568    pub fn sample(&mut self, now: Duration, reduced: bool) {
569        let time = self.clock.time(now);
570        let mut unfinished = false;
571
572        let speed = if self.clock.is_playing() {
573            self.clock.speed()
574        } else {
575            0.0
576        };
577
578        for track in &mut self.tracks {
579            unfinished |= track.sample(time, now, speed, reduced);
580        }
581
582        if speed < 0.0 && time.is_zero() {
583            unfinished = false;
584        }
585
586        self.active = unfinished && self.clock.is_playing();
587
588        if !unfinished && !self.completed {
589            self.observers.resolve(crate::CompletionStatus::Completed);
590        }
591
592        self.completed = !unfinished;
593    }
594
595    /// Reads a sampled typed value; foreign track identities return `None`.
596    pub fn value<T: MotionData<N>, const N: usize>(&self, id: TrackId<T, N>) -> Option<T> {
597        if id.owner != self.owner {
598            return None;
599        }
600
601        self.tracks
602            .get(id.index)?
603            .as_any()
604            .downcast_ref::<Track<T, N>>()
605            .map(|track| track.state.value())
606    }
607
608    /// Pauses the shared clock.
609    pub fn pause(&mut self, now: Duration) {
610        self.clock.pause(now);
611    }
612
613    /// Resumes the shared clock.
614    pub fn play(&mut self, now: Duration) {
615        self.clock.play(now);
616    }
617
618    /// Seeks without replacing prepared tracks.
619    pub fn seek(&mut self, time: Duration, now: Duration) {
620        self.clock.seek(time, now);
621    }
622
623    /// Changes speed, including zero and reverse playback.
624    pub fn set_speed(&mut self, speed: f64, now: Duration) -> Result<(), AnimationError> {
625        self.clock.set_speed(speed, now)
626    }
627
628    /// Restarts and rearms reduced-motion completion.
629    pub fn restart(&mut self, now: Duration) {
630        self.observers.resolve(crate::CompletionStatus::Cancelled);
631        self.clock.seek(Duration::ZERO, now);
632        self.clock.play(now);
633
634        for track in &mut self.tracks {
635            track.restart();
636        }
637
638        self.completed = false;
639    }
640
641    /// Completion observer for this run. Dropping this timeline cancels observers.
642    pub fn completion(&mut self) -> crate::Completion {
643        self.observers.subscribe(self.completed)
644    }
645
646    /// Whether another sample is required, including upcoming delayed tracks.
647    pub fn is_animating(&self) -> bool {
648        self.active
649    }
650
651    /// Native executor time for controls.
652    #[cfg(feature = "gpui")]
653    pub fn now(&self, cx: &gpui::App) -> Duration {
654        self.epoch.map_or(Duration::ZERO, |epoch| {
655            cx.background_executor()
656                .now()
657                .saturating_duration_since(epoch)
658        })
659    }
660
661    /// Samples a stock CE window using its executor; do not mix with explicit time.
662    #[cfg(feature = "gpui")]
663    pub fn frame(&mut self, window: &mut gpui::Window, cx: &gpui::App) {
664        let instant = cx.background_executor().now();
665        let epoch = *self.epoch.get_or_insert(instant);
666
667        self.sample(instant.saturating_duration_since(epoch), cx.reduce_motion());
668
669        if self.active {
670            window.request_animation_frame();
671        }
672    }
673}
674
675impl<const N: usize> MotionData<N> for [f64; N] {
676    fn channels(self) -> [f64; N] {
677        self
678    }
679
680    fn with_channels(self, values: [f64; N]) -> Self {
681        values
682    }
683
684    fn compatible(self, _: Self) -> bool {
685        true
686    }
687}