Skip to main content

gpui_base/motion/
sequence.rs

1use gpui::{App, ElementId, Window};
2
3use super::{Instant, Interpolate, MotionStatus, Transition, TransitionId};
4
5/// One target of a [`Sequence`] and the transition that reaches it.
6#[derive(Clone)]
7pub struct SequenceStep<T> {
8    target: T,
9    transition: Transition,
10}
11
12impl<T> SequenceStep<T> {
13    pub fn new(target: T, transition: Transition) -> Self {
14        Self { target, transition }
15    }
16
17    pub fn target(&self) -> &T {
18        &self.target
19    }
20
21    pub fn transition(&self) -> &Transition {
22        &self.transition
23    }
24}
25
26/// What a [`Sequence`] reports for the current frame.
27#[derive(Clone, Copy, Debug, PartialEq)]
28pub struct SequenceSample<T> {
29    value: T,
30    step: usize,
31    status: MotionStatus,
32}
33
34impl<T> SequenceSample<T> {
35    /// The interpolated value for this frame.
36    pub fn value(&self) -> &T {
37        &self.value
38    }
39
40    pub fn into_value(self) -> T {
41        self.value
42    }
43
44    /// The index of the step being played. A finished sequence reports its
45    /// last step; an empty one reports `0`.
46    pub fn step(&self) -> usize {
47        self.step
48    }
49
50    /// The status of the step being played. `Finished` is reported only once
51    /// the last step has completed: a step that hands over to the next one
52    /// reports that next step's `Delayed` or `Running` instead.
53    pub fn status(&self) -> MotionStatus {
54        self.status
55    }
56
57    pub fn is_finished(&self) -> bool {
58        self.status == MotionStatus::Finished
59    }
60
61    fn is_active(&self) -> bool {
62        matches!(self.status, MotionStatus::Delayed | MotionStatus::Running)
63    }
64}
65
66/// A chain of value transitions, each starting when the previous one ends.
67///
68/// A sequence begins at `from` on the frame it is first sampled and runs its
69/// steps in order. A step ends at an absolute instant — its start plus its
70/// delay and duration — and the next step starts at that same instant, so a
71/// frame that lands past a boundary samples the next step where it would have
72/// been rather than starting it late. Steps of zero duration complete within
73/// the frame that reaches them.
74///
75/// State is keyed by `id` exactly as [`super::transition`] keys its own, and
76/// the sequence plays once per key. Re-rendering with the same ID continues
77/// it; to replay, include an application-owned generation in the ID, for
78/// example `("toast-enter", generation)`.
79///
80/// A step's target and transition are captured when the step starts. Handing
81/// the step being played a different target restarts the sequence: it begins
82/// again at its first step from the value sampled at that instant, as a
83/// retargeted transition continues from its current value. Steps other than
84/// the one being played are read when the sequence reaches them and are not
85/// compared, so a change to an earlier step has no effect on its own. A
86/// sequence does not reverse; play a second sequence back to the start under
87/// its own key when that is wanted.
88///
89/// Under reduced motion the sequence adopts its last target at once, reports
90/// `Finished`, and requests no frame.
91#[derive(Clone)]
92pub struct Sequence<T> {
93    id: TransitionId,
94    from: T,
95    steps: Vec<SequenceStep<T>>,
96}
97
98impl<T> Sequence<T>
99where
100    T: Interpolate + PartialEq + 'static,
101{
102    /// Starts a sequence at `from`, with no steps yet.
103    pub fn new(id: impl Into<TransitionId>, from: T) -> Self {
104        Self {
105            id: id.into(),
106            from,
107            steps: Vec::new(),
108        }
109    }
110
111    /// Appends a step that transitions to `target` once the previous step ends.
112    pub fn with_step(mut self, target: T, transition: Transition) -> Self {
113        self.steps.push(SequenceStep::new(target, transition));
114        self
115    }
116
117    /// Appends steps built elsewhere, in order.
118    pub fn with_steps(mut self, steps: impl IntoIterator<Item = SequenceStep<T>>) -> Self {
119        self.steps.extend(steps);
120        self
121    }
122
123    pub fn from(&self) -> &T {
124        &self.from
125    }
126
127    pub fn steps(&self) -> &[SequenceStep<T>] {
128        &self.steps
129    }
130
131    /// Samples the sequence and requests a frame while a step is active.
132    pub fn sample(self, window: &mut Window, cx: &mut App) -> SequenceSample<T> {
133        let Some(last) = self.steps.last() else {
134            return SequenceSample {
135                value: self.from,
136                step: 0,
137                status: MotionStatus::Idle,
138            };
139        };
140        let last_step = self.steps.len() - 1;
141
142        let id = ElementId::NamedChild(self.id.0.into(), "__sequence".into());
143        let now = cx.background_executor().now();
144        let state = window.use_keyed_state(id, cx, |_, _| {
145            SequenceState::start(0, self.from.clone(), &self.steps, now)
146        });
147
148        if cx.reduce_motion() {
149            let settled = state.read(cx);
150            if settled.step != last_step || settled.from != last.target {
151                state.update(cx, |state, _| {
152                    *state = SequenceState::start(last_step, last.target.clone(), &self.steps, now);
153                });
154            }
155            return SequenceSample {
156                value: last.target.clone(),
157                step: last_step,
158                status: MotionStatus::Finished,
159            };
160        }
161
162        let snapshot = state.read(cx);
163        let (progress, status) = snapshot.progress(now);
164        let retargeted =
165            snapshot.step > last_step || self.steps[snapshot.step].target != snapshot.target;
166        let handing_over = status == MotionStatus::Finished && snapshot.step < last_step;
167
168        if !retargeted && !handing_over {
169            let sample = SequenceSample {
170                value: snapshot.value(progress),
171                step: snapshot.step,
172                status,
173            };
174            if sample.is_active() {
175                window.request_animation_frame();
176            }
177            return sample;
178        }
179
180        // Only a boundary or a restart writes state. A restart begins the new
181        // sequence's first step from the value the old one had reached, which
182        // is where the eye is. A hand-over starts the next step at the instant
183        // the finished one ended, not at `now`, so a frame that lands past
184        // the boundary does not start the step late.
185        let mut next = if retargeted {
186            SequenceState::start(0, snapshot.value(progress), &self.steps, now)
187        } else {
188            snapshot.hand_over(&self.steps)
189        };
190        let (value, status) = loop {
191            let (progress, status) = next.progress(now);
192            if status == MotionStatus::Finished && next.step < last_step {
193                next = next.hand_over(&self.steps);
194                continue;
195            }
196            break (next.value(progress), status);
197        };
198        let sample = SequenceSample {
199            value,
200            step: next.step,
201            status,
202        };
203        state.update(cx, |state, _| *state = next);
204        if sample.is_active() {
205            window.request_animation_frame();
206        }
207        sample
208    }
209}
210
211/// The step a sequence is playing, complete enough to sample without the
212/// caller's steps: those are only needed to hand over to the next one.
213#[derive(Clone)]
214struct SequenceState<T> {
215    step: usize,
216    from: T,
217    target: T,
218    transition: Transition,
219    started_at: Instant,
220}
221
222impl<T: Interpolate> SequenceState<T> {
223    fn start(step: usize, from: T, steps: &[SequenceStep<T>], started_at: Instant) -> Self {
224        Self {
225            step,
226            from,
227            target: steps[step].target.clone(),
228            transition: steps[step].transition.clone(),
229            started_at,
230        }
231    }
232
233    fn progress(&self, now: Instant) -> (f32, MotionStatus) {
234        self.transition.progress(
235            now.saturating_duration_since(self.started_at),
236            self.transition.duration,
237        )
238    }
239
240    fn value(&self, progress: f32) -> T {
241        self.from
242            .interpolate(&self.target, self.transition.sample(progress))
243    }
244
245    /// Starts the next step where this finished one ended. The caller has
246    /// checked that this step is finished and is not the last, so the end lies
247    /// no later than `now` and the addition cannot overflow the clock.
248    fn hand_over(&self, steps: &[SequenceStep<T>]) -> Self {
249        Self::start(
250            self.step + 1,
251            self.target.clone(),
252            steps,
253            self.started_at + self.transition.finishes_after(),
254        )
255    }
256}