Skip to main content

gpui_kit/motion/
sequence.rs

1//! Motion that runs one step after another, rather than all at once.
2
3use std::time::Duration;
4
5use super::MotionSpec;
6
7/// A chain of specifications, each starting when the one before it has
8/// finished.
9///
10/// [`MotionSpec::after`] already composes two, and a chain of them is a
11/// perfectly good way to write "and then". This exists for what that shape
12/// cannot answer: it keeps the steps, so a caller can ask where step three
13/// starts or drive the whole chain from one clock, and it reports the total —
14/// which [`Presence`](super::Presence), a caller holding an element on screen,
15/// or anything else waiting for the group to be over cannot otherwise know
16/// without adding the durations up by hand.
17///
18/// Each step keeps its own delay, counted from the end of the previous step,
19/// so a gap between two motions is written where the gap is.
20#[derive(Debug, Clone, Default, PartialEq)]
21pub struct Sequence {
22    /// Steps as given, with their delays still relative to the step before.
23    steps: Vec<MotionSpec>,
24}
25
26impl Sequence {
27    pub fn new(steps: impl IntoIterator<Item = MotionSpec>) -> Self {
28        Self {
29            steps: steps.into_iter().collect(),
30        }
31    }
32
33    /// Adds a step that begins when everything already in the sequence has
34    /// finished.
35    pub fn then(mut self, spec: MotionSpec) -> Self {
36        self.steps.push(spec);
37        self
38    }
39
40    pub fn len(&self) -> usize {
41        self.steps.len()
42    }
43
44    pub fn is_empty(&self) -> bool {
45        self.steps.is_empty()
46    }
47
48    /// When step `index` starts, measured from the start of the sequence.
49    /// Its own delay is part of the step, not part of the wait.
50    pub fn start(&self, index: usize) -> Duration {
51        self.steps
52            .iter()
53            .take(index.min(self.steps.len()))
54            .map(|spec| spec.total())
55            .sum()
56    }
57
58    /// Step `index` with its delay measured from the start of the sequence,
59    /// so it can be handed to anything that runs a single specification.
60    pub fn step(&self, index: usize) -> Option<MotionSpec> {
61        let spec = *self.steps.get(index)?;
62        Some(spec.with_delay(spec.delay_ms + self.start(index).as_millis() as u64))
63    }
64
65    pub fn steps(&self) -> impl Iterator<Item = MotionSpec> + '_ {
66        (0..self.steps.len()).filter_map(|index| self.step(index))
67    }
68
69    /// How long the whole sequence lasts.
70    pub fn total(&self) -> Duration {
71        self.steps.iter().map(|spec| spec.total()).sum()
72    }
73
74    /// Where step `index` has got to when the sequence as a whole is `raw`
75    /// through, so one clock over [`Sequence::total`] drives every step.
76    ///
77    /// A step that has not started reports 0 and a step that has finished
78    /// reports 1, which is what a caller painting all of them at once needs:
79    /// the steps that are over stay where they landed.
80    pub fn progress(&self, index: usize, raw: f32) -> f32 {
81        let Some(spec) = self.steps.get(index).copied() else {
82            return 1.0;
83        };
84        let elapsed = self.total().mul_f32(raw.clamp(0.0, 1.0));
85        let local = elapsed.saturating_sub(self.start(index));
86        let span = spec.total();
87        if span.is_zero() {
88            return 1.0;
89        }
90        spec.progress((local.as_secs_f32() / span.as_secs_f32()).min(1.0))
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::motion::CubicBezier;
98
99    fn linear(ms: u64) -> MotionSpec {
100        MotionSpec::new(ms, CubicBezier::new(0.0, 0.0, 1.0, 1.0))
101    }
102
103    fn sequence() -> Sequence {
104        Sequence::new([linear(200)]).then(linear(100).with_delay(50))
105    }
106
107    #[test]
108    fn a_step_starts_when_the_one_before_it_ends() {
109        let sequence = sequence();
110        assert_eq!(sequence.start(0), Duration::ZERO);
111        assert_eq!(sequence.start(1), Duration::from_millis(200));
112        assert_eq!(sequence.step(1).expect("two steps").delay_ms, 250);
113    }
114
115    #[test]
116    fn the_total_is_the_sum_of_the_steps() {
117        assert_eq!(sequence().total(), Duration::from_millis(350));
118        assert_eq!(Sequence::default().total(), Duration::ZERO);
119    }
120
121    #[test]
122    fn an_empty_sequence_has_no_steps_to_report() {
123        let empty = Sequence::default();
124        assert!(empty.is_empty());
125        assert_eq!(empty.len(), 0);
126        assert!(empty.step(0).is_none());
127    }
128
129    #[test]
130    fn a_step_holds_at_both_ends_of_its_own_span() {
131        let sequence = sequence();
132        // The second step has not begun while the first is still running.
133        assert_eq!(sequence.progress(1, 0.5), 0.0);
134        // And the first stays where it landed once it is over.
135        assert_eq!(sequence.progress(0, 1.0), 1.0);
136        assert!((sequence.progress(0, 200.0 / 350.0) - 1.0).abs() < 0.01);
137    }
138
139    #[test]
140    fn the_steps_carry_the_offsets_the_sequence_gave_them() {
141        let delays: Vec<u64> = sequence().steps().map(|spec| spec.delay_ms).collect();
142        assert_eq!(delays, vec![0, 250]);
143    }
144}