Skip to main content

guise/anim/
animator.rs

1//! `Animator` — a clip with a clock you can drive.
2//!
3//! gpui's own `with_animation` is fire-and-forget: it starts when the element
4//! first lays out and there is no handle to pause, reverse, or scrub it. That
5//! is the right thing for an entrance and useless for anything a user
6//! controls, so `Animator` keeps the playback state in an entity instead.
7//!
8//! The clock is an *anchor*, not a tick: `time` is where the playhead was at
9//! `anchor`, and everything else is derived from `Instant::now()`. Nothing
10//! mutates per frame, so a paused animation costs nothing, seeking is one
11//! assignment, and sampling is pure enough to unit-test without a window.
12//!
13//! Frames come from [`Animator::frame`], which also asks the window for the
14//! next one while the clip is still running — that is the whole repaint loop.
15
16use std::time::{Duration, Instant};
17
18use gpui::{Context, EventEmitter, Task, Window};
19
20use super::{Clip, Frame};
21
22/// The ends of a playback run. There is no per-frame event: your `render`
23/// already runs every frame, and reading [`Animator::frame`] there is the
24/// same thing without the plumbing.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum AnimatorEvent {
27  /// Playback started (or restarted) from a stopped state.
28  Begin,
29  /// The playhead reached the end — or the start, when running reversed.
30  Complete,
31}
32
33/// A clip plus a playhead.
34pub struct Animator {
35  clip: Clip,
36  /// Playhead position in ms, correct as of `anchor`.
37  time: f32,
38  /// `Some` while running: the wall clock that `time` was measured at.
39  anchor: Option<Instant>,
40  speed: f32,
41  reversed: bool,
42  /// Bumped on every state change so a stale completion timer gives up.
43  epoch: usize,
44  completion: Option<Task<()>>,
45}
46
47impl EventEmitter<AnimatorEvent> for Animator {}
48
49impl Animator {
50  pub fn new(clip: impl Into<Clip>, _cx: &mut Context<Self>) -> Self {
51    Animator {
52      clip: clip.into(),
53      time: 0.0,
54      anchor: None,
55      speed: 1.0,
56      reversed: false,
57      epoch: 0,
58      completion: None,
59    }
60  }
61
62  /// Start playing as soon as it is created.
63  pub fn autoplay(mut self, cx: &mut Context<Self>) -> Self {
64    self.play(cx);
65    self
66  }
67
68  /// Run the clip backwards from the start.
69  pub fn reversed(mut self, reversed: bool) -> Self {
70    self.reversed = reversed;
71    self.time = if reversed { self.total_ms() } else { 0.0 };
72    self
73  }
74
75  pub fn clip(&self) -> &Clip {
76    &self.clip
77  }
78
79  fn total_ms(&self) -> f32 {
80    self.clip.total_ms()
81  }
82
83  /// Where the playhead is right now, in milliseconds.
84  pub fn time(&self) -> f32 {
85    self.time_at(Instant::now())
86  }
87
88  /// Where the playhead would be at `now`. The pure form — tests drive
89  /// this instead of sleeping.
90  pub fn time_at(&self, now: Instant) -> f32 {
91    let Some(anchor) = self.anchor else {
92      return self.time;
93    };
94    let elapsed = now.saturating_duration_since(anchor).as_secs_f32() * 1000.0 * self.speed;
95    let raw = if self.reversed {
96      self.time - elapsed
97    } else {
98      self.time + elapsed
99    };
100    raw.clamp(0.0, self.total_ms())
101  }
102
103  /// 0..=1 through the clip. Endless clips report their position within
104  /// the current pass.
105  pub fn progress(&self) -> f32 {
106    self.clip.sample(self.time()).progress
107  }
108
109  /// Whether the clock is running. A clip that has played to its end
110  /// reports `false` even before anything cleans up.
111  pub fn is_playing(&self) -> bool {
112    self.anchor.is_some() && !self.is_settled_at(Instant::now())
113  }
114
115  /// Whether the playhead has run out of road in the direction it is going.
116  fn is_settled_at(&self, now: Instant) -> bool {
117    if self.clip.is_endless() || self.speed <= 0.0 {
118      return false;
119    }
120    let time = self.time_at(now);
121    if self.reversed {
122      time <= 0.0
123    } else {
124      time >= self.total_ms()
125    }
126  }
127
128  /// The values for this instant, and a request for the next frame while
129  /// the clip is still moving. Call it from `render`.
130  pub fn frame(&self, window: &mut Window) -> Frame {
131    let now = Instant::now();
132    if self.anchor.is_some() && !self.is_settled_at(now) {
133      window.request_animation_frame();
134    }
135    self.clip.sample(self.time_at(now))
136  }
137
138  /// The values at `now`, with no window and no repaint request.
139  pub fn frame_at(&self, now: Instant) -> Frame {
140    self.clip.sample(self.time_at(now))
141  }
142
143  pub fn play(&mut self, cx: &mut Context<Self>) {
144    let now = Instant::now();
145    if self.anchor.is_some() && !self.is_settled_at(now) {
146      return;
147    }
148    // Replaying something that already finished starts it over, rather
149    // than sitting on the end frame doing nothing.
150    if self.is_settled_at(now) {
151      self.time = if self.reversed { self.total_ms() } else { 0.0 };
152    }
153    self.anchor = Some(now);
154    cx.emit(AnimatorEvent::Begin);
155    self.reschedule(cx);
156    cx.notify();
157  }
158
159  pub fn pause(&mut self, cx: &mut Context<Self>) {
160    if self.anchor.is_none() {
161      return;
162    }
163    self.time = self.time_at(Instant::now());
164    self.anchor = None;
165    self.reschedule(cx);
166    cx.notify();
167  }
168
169  pub fn toggle(&mut self, cx: &mut Context<Self>) {
170    if self.is_playing() {
171      self.pause(cx);
172    } else {
173      self.play(cx);
174    }
175  }
176
177  /// Back to the beginning, playing.
178  pub fn restart(&mut self, cx: &mut Context<Self>) {
179    self.time = if self.reversed { self.total_ms() } else { 0.0 };
180    self.anchor = Some(Instant::now());
181    cx.emit(AnimatorEvent::Begin);
182    self.reschedule(cx);
183    cx.notify();
184  }
185
186  /// Back to the beginning, stopped.
187  pub fn stop(&mut self, cx: &mut Context<Self>) {
188    self.time = if self.reversed { self.total_ms() } else { 0.0 };
189    self.anchor = None;
190    self.reschedule(cx);
191    cx.notify();
192  }
193
194  /// Move the playhead. Keeps playing if it was playing.
195  pub fn seek(&mut self, ms: f32, cx: &mut Context<Self>) {
196    self.time = ms.clamp(0.0, self.total_ms());
197    if self.anchor.is_some() {
198      self.anchor = Some(Instant::now());
199    }
200    self.reschedule(cx);
201    cx.notify();
202  }
203
204  /// Move the playhead by fraction of the clip, 0..=1. Endless clips have
205  /// no fraction to scrub, so this does nothing for them.
206  pub fn seek_progress(&mut self, progress: f32, cx: &mut Context<Self>) {
207    let total = self.total_ms();
208    if total.is_finite() {
209      self.seek(total * progress.clamp(0.0, 1.0), cx);
210    }
211  }
212
213  /// Flip direction, leaving the playhead where it is.
214  pub fn reverse(&mut self, cx: &mut Context<Self>) {
215    self.time = self.time_at(Instant::now());
216    self.reversed = !self.reversed;
217    if self.anchor.is_some() {
218      self.anchor = Some(Instant::now());
219    }
220    self.reschedule(cx);
221    cx.notify();
222  }
223
224  pub fn is_reversed(&self) -> bool {
225    self.reversed
226  }
227
228  pub fn set_speed(&mut self, speed: f32, cx: &mut Context<Self>) {
229    self.time = self.time_at(Instant::now());
230    self.speed = speed.max(0.0);
231    if self.anchor.is_some() {
232      self.anchor = Some(Instant::now());
233    }
234    self.reschedule(cx);
235    cx.notify();
236  }
237
238  pub fn speed(&self) -> f32 {
239    self.speed
240  }
241
242  /// Arm (or disarm) the one timer that fires `Complete`.
243  ///
244  /// One timer for the whole run, not a tick: the frame values come from
245  /// the clock, so the only thing that needs waking up is the event. An
246  /// endless clip never arms one.
247  fn reschedule(&mut self, cx: &mut Context<Self>) {
248    self.epoch += 1;
249    self.completion = None;
250    if self.anchor.is_none() || self.speed <= 0.0 || self.clip.is_endless() {
251      return;
252    }
253    let remaining = if self.reversed {
254      self.time
255    } else {
256      self.total_ms() - self.time
257    };
258    if remaining <= 0.0 {
259      return;
260    }
261    let epoch = self.epoch;
262    let wait = Duration::from_secs_f32((remaining / self.speed / 1000.0).max(0.0));
263    self.completion = Some(cx.spawn(async move |this, cx| {
264      cx.background_executor().timer(wait).await;
265      this
266        .update(cx, |animator, cx| {
267          if animator.epoch != epoch {
268            return;
269          }
270          animator.time = if animator.reversed {
271            0.0
272          } else {
273            animator.total_ms()
274          };
275          animator.anchor = None;
276          animator.completion = None;
277          cx.emit(AnimatorEvent::Complete);
278          cx.notify();
279        })
280        .ok();
281    }));
282  }
283}