Skip to main content

open_gpui_motion/
sequence.rs

1//! Renderer-neutral sequence plans for composing many motion tracks.
2
3use crate::{
4    MotionFrameDemand, MotionFrameReason, MotionModel, MotionRunState, MotionScalarSample,
5};
6use std::time::Duration;
7
8/// A deterministic sequence of scalar motion steps.
9///
10/// The sequence owns relative timing and frame-demand aggregation, but it does not schedule frames,
11/// mutate render state, or know about GPUI elements. Adapters sample it with elapsed time and map
12/// the returned values into their own presentation state.
13#[derive(Debug, Clone, PartialEq)]
14pub struct MotionSequence<K> {
15    steps: Vec<MotionSequenceStep<K>>,
16}
17
18impl<K> Default for MotionSequence<K> {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl<K> MotionSequence<K> {
25    /// Creates an empty sequence.
26    pub const fn new() -> Self {
27        Self { steps: Vec::new() }
28    }
29
30    /// Returns the steps in insertion order.
31    pub fn steps(&self) -> &[MotionSequenceStep<K>] {
32        &self.steps
33    }
34
35    /// Returns whether the sequence has no steps.
36    pub fn is_empty(&self) -> bool {
37        self.steps.is_empty()
38    }
39
40    /// Returns the number of steps.
41    pub fn len(&self) -> usize {
42        self.steps.len()
43    }
44
45    /// Inserts a step at an absolute sequence elapsed time.
46    pub fn insert_at(&mut self, key: K, model: MotionModel, start_at: Duration) -> &mut Self {
47        self.steps
48            .push(MotionSequenceStep::new(key, model, start_at));
49        self
50    }
51
52    /// Appends a step after the current sequence duration hint.
53    pub fn append(&mut self, key: K, model: MotionModel) -> &mut Self {
54        self.insert_at(key, model, self.duration_hint())
55    }
56
57    /// Inserts a step at the previous step's start time.
58    pub fn insert_with_previous(&mut self, key: K, model: MotionModel) -> &mut Self {
59        let start_at = self
60            .steps
61            .last()
62            .map(MotionSequenceStep::start_at)
63            .unwrap_or(Duration::ZERO);
64        self.insert_at(key, model, start_at)
65    }
66
67    /// Inserts a step after the previous step's end hint plus delay.
68    pub fn insert_after_previous(
69        &mut self,
70        key: K,
71        model: MotionModel,
72        delay: Duration,
73    ) -> &mut Self {
74        let start_at = self
75            .steps
76            .last()
77            .map(MotionSequenceStep::end_hint)
78            .unwrap_or(Duration::ZERO);
79        self.insert_at(key, model, saturating_duration_add(start_at, delay))
80    }
81
82    /// Inserts many steps with a fixed stagger from a start time.
83    pub fn insert_staggered(
84        &mut self,
85        keys: impl IntoIterator<Item = K>,
86        model: MotionModel,
87        start_at: Duration,
88        stagger: Duration,
89    ) -> &mut Self {
90        let mut next_start = start_at;
91        for key in keys {
92            self.insert_at(key, model, next_start);
93            next_start = saturating_duration_add(next_start, stagger);
94        }
95        self
96    }
97
98    /// Returns the sequence duration hint from the latest step end hint.
99    pub fn duration_hint(&self) -> Duration {
100        self.steps
101            .iter()
102            .map(MotionSequenceStep::end_hint)
103            .max()
104            .unwrap_or(Duration::ZERO)
105    }
106}
107
108impl<K: Clone> MotionSequence<K> {
109    /// Samples all steps at sequence elapsed time.
110    pub fn sample_at(&self, elapsed: Duration) -> MotionSequenceSample<K> {
111        let steps = self
112            .steps
113            .iter()
114            .map(|step| step.sample_at(elapsed))
115            .collect::<Vec<_>>();
116        let needs_frame = steps
117            .iter()
118            .any(|step| step.state().needs_frame_for_sequence());
119        let frame_demand = if needs_frame {
120            MotionFrameDemand::NeedsFrame(MotionFrameReason::UpdateRender)
121        } else {
122            MotionFrameDemand::Idle
123        };
124        MotionSequenceSample::new(steps, frame_demand)
125    }
126}
127
128/// One keyed sequence step.
129#[derive(Debug, Clone, PartialEq)]
130pub struct MotionSequenceStep<K> {
131    key: K,
132    model: MotionModel,
133    start_at: Duration,
134    duration_hint: Duration,
135}
136
137impl<K> MotionSequenceStep<K> {
138    /// Creates a sequence step at an absolute sequence elapsed time.
139    pub fn new(key: K, model: MotionModel, start_at: Duration) -> Self {
140        Self {
141            key,
142            model,
143            start_at,
144            duration_hint: model.sequence_duration_hint(),
145        }
146    }
147
148    /// Returns the stable step key.
149    pub const fn key(&self) -> &K {
150        &self.key
151    }
152
153    /// Returns the step motion model.
154    pub const fn model(&self) -> MotionModel {
155        self.model
156    }
157
158    /// Returns the absolute sequence time at which this step starts.
159    pub const fn start_at(&self) -> Duration {
160        self.start_at
161    }
162
163    /// Returns the duration hint used for timeline composition.
164    pub const fn duration_hint(&self) -> Duration {
165        self.duration_hint
166    }
167
168    /// Returns the hinted end time for this step.
169    pub fn end_hint(&self) -> Duration {
170        saturating_duration_add(self.start_at, self.duration_hint)
171    }
172}
173
174impl<K: Clone> MotionSequenceStep<K> {
175    /// Samples this step at sequence elapsed time.
176    pub fn sample_at(&self, elapsed: Duration) -> MotionSequenceStepSample<K> {
177        if elapsed < self.start_at {
178            return MotionSequenceStepSample::pending(self.key.clone());
179        }
180
181        let local_elapsed = elapsed.saturating_sub(self.start_at);
182        let sample = self
183            .model
184            .sample_scalar_elapsed(0.0, 1.0, 0.0, local_elapsed);
185        MotionSequenceStepSample::from_scalar(self.key.clone(), sample)
186    }
187}
188
189/// Sequence-level state for one sampled step.
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191pub enum MotionSequenceStepState {
192    /// The step has not reached its start time yet.
193    Pending,
194    /// The step is active and should keep requesting frames.
195    Active,
196    /// The step completed immediately due to policy or reduced motion.
197    Immediate,
198    /// The step reached its final state.
199    Completed,
200    /// The step was cancelled before reaching its final state.
201    Cancelled,
202}
203
204impl MotionSequenceStepState {
205    /// Returns whether this state keeps the sequence frame demand active.
206    pub const fn needs_frame_for_sequence(self) -> bool {
207        matches!(self, Self::Pending | Self::Active)
208    }
209
210    /// Returns whether this state is terminal.
211    pub const fn is_terminal(self) -> bool {
212        !self.needs_frame_for_sequence()
213    }
214
215    /// Returns whether this state reached the final semantic state.
216    pub const fn reached_final_state(self) -> bool {
217        matches!(self, Self::Immediate | Self::Completed)
218    }
219
220    fn from_run_state(state: MotionRunState) -> Self {
221        match state {
222            MotionRunState::Immediate => Self::Immediate,
223            MotionRunState::Active => Self::Active,
224            MotionRunState::Completed => Self::Completed,
225            MotionRunState::Cancelled => Self::Cancelled,
226        }
227    }
228}
229
230/// Sample for one keyed sequence step.
231#[derive(Debug, Clone, PartialEq)]
232pub struct MotionSequenceStepSample<K> {
233    key: K,
234    state: MotionSequenceStepState,
235    elapsed: Duration,
236    value: f32,
237    velocity: f32,
238    target: f32,
239}
240
241impl<K> MotionSequenceStepSample<K> {
242    fn pending(key: K) -> Self {
243        Self {
244            key,
245            state: MotionSequenceStepState::Pending,
246            elapsed: Duration::ZERO,
247            value: 0.0,
248            velocity: 0.0,
249            target: 1.0,
250        }
251    }
252
253    fn from_scalar(key: K, sample: MotionScalarSample) -> Self {
254        Self {
255            key,
256            state: MotionSequenceStepState::from_run_state(sample.state()),
257            elapsed: sample.elapsed(),
258            value: sample.value(),
259            velocity: sample.velocity(),
260            target: sample.target(),
261        }
262    }
263
264    /// Returns the stable step key.
265    pub const fn key(&self) -> &K {
266        &self.key
267    }
268
269    /// Returns the sequence-level step state.
270    pub const fn state(&self) -> MotionSequenceStepState {
271        self.state
272    }
273
274    /// Returns local elapsed time since the step started.
275    pub const fn elapsed(&self) -> Duration {
276        self.elapsed
277    }
278
279    /// Returns sampled scalar progress.
280    pub const fn value(&self) -> f32 {
281        self.value
282    }
283
284    /// Returns sampled scalar velocity.
285    pub const fn velocity(&self) -> f32 {
286        self.velocity
287    }
288
289    /// Returns the scalar target.
290    pub const fn target(&self) -> f32 {
291        self.target
292    }
293
294    /// Returns whether this step keeps requesting frames.
295    pub const fn needs_frame(&self) -> bool {
296        self.state.needs_frame_for_sequence()
297    }
298}
299
300/// Sample for a full motion sequence.
301#[derive(Debug, Clone, PartialEq)]
302pub struct MotionSequenceSample<K> {
303    steps: Vec<MotionSequenceStepSample<K>>,
304    frame_demand: MotionFrameDemand,
305}
306
307impl<K> MotionSequenceSample<K> {
308    /// Creates a sequence sample.
309    pub fn new(steps: Vec<MotionSequenceStepSample<K>>, frame_demand: MotionFrameDemand) -> Self {
310        Self {
311            steps,
312            frame_demand,
313        }
314    }
315
316    /// Returns sampled steps in sequence insertion order.
317    pub fn steps(&self) -> &[MotionSequenceStepSample<K>] {
318        &self.steps
319    }
320
321    /// Returns the aggregated frame demand for this sample.
322    pub const fn frame_demand(&self) -> MotionFrameDemand {
323        self.frame_demand
324    }
325
326    /// Returns whether every step is terminal.
327    pub const fn complete(&self) -> bool {
328        !self.frame_demand.needs_frame()
329    }
330}
331
332impl<K: PartialEq> MotionSequenceSample<K> {
333    /// Returns the sample for a key.
334    pub fn step(&self, key: &K) -> Option<&MotionSequenceStepSample<K>> {
335        self.steps.iter().find(|step| step.key() == key)
336    }
337}
338
339fn saturating_duration_add(left: Duration, right: Duration) -> Duration {
340    left.checked_add(right).unwrap_or(Duration::MAX)
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346    use crate::{
347        MotionDuration, MotionEasing, MotionPreference, MotionSpec, MotionSpringPreset,
348        MotionSpringSpec,
349    };
350
351    fn linear_model(duration: Duration) -> MotionModel {
352        MotionModel::timeline(MotionSpec::new(
353            MotionPreference::Animated,
354            MotionDuration::Custom(duration),
355            MotionEasing::Linear,
356        ))
357    }
358
359    #[test]
360    fn sequence_positions_append_with_previous_and_after_previous() {
361        let model = linear_model(Duration::from_millis(100));
362        let mut sequence = MotionSequence::new();
363
364        sequence
365            .append("first", model)
366            .insert_with_previous("parallel", model)
367            .insert_after_previous("delayed", model, Duration::from_millis(20));
368
369        assert_eq!(sequence.steps()[0].start_at(), Duration::ZERO);
370        assert_eq!(sequence.steps()[1].start_at(), Duration::ZERO);
371        assert_eq!(sequence.steps()[2].start_at(), Duration::from_millis(120));
372        assert_eq!(sequence.duration_hint(), Duration::from_millis(220));
373    }
374
375    #[test]
376    fn staggered_steps_preserve_start_offsets() {
377        let model = linear_model(Duration::from_millis(50));
378        let mut sequence = MotionSequence::new();
379
380        sequence.insert_staggered(
381            ["a", "b", "c"],
382            model,
383            Duration::from_millis(10),
384            Duration::from_millis(20),
385        );
386
387        assert_eq!(sequence.steps()[0].start_at(), Duration::from_millis(10));
388        assert_eq!(sequence.steps()[1].start_at(), Duration::from_millis(30));
389        assert_eq!(sequence.steps()[2].start_at(), Duration::from_millis(50));
390        assert_eq!(sequence.duration_hint(), Duration::from_millis(100));
391    }
392
393    #[test]
394    fn sequence_samples_pending_active_and_completed_steps() {
395        let model = linear_model(Duration::from_millis(100));
396        let mut sequence = MotionSequence::new();
397        sequence.insert_at("row", model, Duration::from_millis(50));
398
399        let pending = sequence.sample_at(Duration::ZERO);
400        let pending_step = pending.step(&"row").expect("row step");
401        assert_eq!(pending_step.state(), MotionSequenceStepState::Pending);
402        assert!(pending.frame_demand().needs_frame());
403        assert!(!pending.complete());
404
405        let active = sequence.sample_at(Duration::from_millis(100));
406        let active_step = active.step(&"row").expect("row step");
407        assert_eq!(active_step.state(), MotionSequenceStepState::Active);
408        assert_eq!(active_step.elapsed(), Duration::from_millis(50));
409        assert_eq!(active_step.value(), 0.5);
410        assert!(active.frame_demand().needs_frame());
411
412        let complete = sequence.sample_at(Duration::from_millis(160));
413        let complete_step = complete.step(&"row").expect("row step");
414        assert_eq!(complete_step.state(), MotionSequenceStepState::Completed);
415        assert_eq!(complete_step.value(), 1.0);
416        assert!(!complete.frame_demand().needs_frame());
417        assert!(complete.complete());
418    }
419
420    #[test]
421    fn reduced_motion_step_completes_without_frame_demand_at_start() {
422        let model = MotionModel::timeline(MotionSpec::committed_layout(MotionPreference::Reduced));
423        let mut sequence = MotionSequence::new();
424        sequence.insert_at("panel", model, Duration::ZERO);
425
426        let sample = sequence.sample_at(Duration::ZERO);
427        let step = sample.step(&"panel").expect("panel step");
428
429        assert_eq!(step.state(), MotionSequenceStepState::Immediate);
430        assert_eq!(step.value(), 1.0);
431        assert!(step.state().reached_final_state());
432        assert!(sample.complete());
433    }
434
435    #[test]
436    fn spring_sequence_duration_hint_uses_review_duration() {
437        let spring = MotionSpringSpec::layout(MotionPreference::Animated);
438        let model = MotionModel::spring(spring);
439        let mut sequence = MotionSequence::new();
440
441        sequence.append("spring", model).append("next", model);
442
443        assert_eq!(
444            sequence.steps()[0].duration_hint(),
445            spring.physics().review_duration()
446        );
447        assert_eq!(
448            sequence.steps()[1].start_at(),
449            spring.physics().review_duration()
450        );
451        assert_eq!(spring.preset(), Some(MotionSpringPreset::Layout));
452    }
453}