Skip to main content

gpui/
animated.rs

1use std::{ops::Sub, time::Duration};
2
3use crate::{AnyMotion, Lerp, Progress};
4
5/// A value sampled from an [`Animated`], together with its activity and progress.
6#[derive(Clone)]
7pub struct AnimatedSample<T> {
8    /// The interpolated value at the sampled time.
9    pub value: T,
10
11    /// Whether another sample may produce a different value.
12    pub is_active: bool,
13
14    /// Eased presentation progress from the interruption point to the target. An easing curve may
15    /// take it outside `0.0..=1.0`.
16    pub progress: Progress,
17}
18
19/// A value that animates toward a target, and keeps the state it needs to change course.
20///
21/// `Animated` stores the logical target, the motion, and when the current run started. When the
22/// target changes mid-run, the new run starts from the value on screen at that moment, so the
23/// motion doesn't jump. [`Transition`](crate::Transition) and style transitions are built on it.
24///
25/// `Time` is the clock type. The default is [`std::time::Instant`]. Any `Time` that subtracts to a
26/// [`Duration`](std::time::Duration) works, and a `Duration` makes the examples deterministic.
27///
28/// # Examples
29///
30/// ```
31/// use std::time::Duration;
32///
33/// use gpui::{Animated, Motion, millis, secs};
34///
35/// let motion = Motion::new(secs(1));
36/// let mut value = Animated::<f32, Duration>::new(0.0, motion.clone());
37///
38/// assert!(value.set(10.0, &motion, Duration::ZERO));
39/// assert_eq!(value.sample(millis(500)).value, 5.0);
40///
41/// // Setting the same target again changes nothing.
42/// assert!(!value.set(10.0, &motion, millis(500)));
43/// ```
44#[derive(Clone)]
45pub struct Animated<T, Time = std::time::Instant> {
46    value: T,
47    initial_value: T,
48    last_value: T,
49    motion: AnyMotion,
50    reverse_progress: bool,
51    settled_progress: Option<Progress>,
52    started_at: Option<Time>,
53}
54
55impl<T, Time> Animated<T, Time>
56where
57    T: Lerp + Clone + PartialEq,
58    Time: Copy + Sub<Time, Output = Duration>,
59{
60    /// Creates a value at rest at `value`. `motion` describes how later changes animate.
61    pub fn new(value: T, motion: impl Into<AnyMotion>) -> Self {
62        Self {
63            initial_value: value.clone(),
64            last_value: value.clone(),
65            value,
66            motion: motion.into(),
67            reverse_progress: false,
68            settled_progress: None,
69            started_at: None,
70        }
71    }
72
73    /// Returns the logical target: the value most recently given to [`set`](Self::set).
74    ///
75    /// This is not the value on screen during a run. Use [`sample`](Self::sample) for that.
76    pub fn value(&self) -> &T {
77        &self.value
78    }
79
80    /// Returns eased presentation progress without changing state.
81    /// The result may overshoot zero through one.
82    ///
83    /// A value with no active or previously settled run is already presenting
84    /// its logical target and therefore reports [`Progress::END`].
85    pub(crate) fn progress_at(&self, now: Time) -> Progress {
86        self.started_at.map_or_else(
87            || self.settled_progress.unwrap_or(Progress::END),
88            |started_at| {
89                self.presentation_progress(self.motion.sample_at(started_at, now).progress)
90            },
91        )
92    }
93
94    fn presentation_progress(&self, progress: Progress) -> Progress {
95        if self.reverse_progress {
96            progress.reversed()
97        } else {
98            progress
99        }
100    }
101
102    /// Sets a new target and starts a run from the current presentation.
103    ///
104    /// The run begins at `now`, using `motion`, and starts from the value on screen at that
105    /// moment. Returns `true` if the target changed. Setting the current target again returns
106    /// `false` and leaves the run alone.
107    ///
108    /// Reverse-first playback flips motion progress into anchor-to-target progress. A custom
109    /// easing curve that starts away from zero can still cause an initial jump.
110    pub fn set(&mut self, value: T, motion: impl Into<AnyMotion>, now: Time) -> bool {
111        self.retarget(value, motion, now, true)
112    }
113
114    /// Updates the logical value and restarts from the initial value.
115    pub(crate) fn restart(&mut self, value: T, motion: impl Into<AnyMotion>, now: Time) -> bool {
116        self.retarget(value, motion, now, false)
117    }
118
119    fn retarget(
120        &mut self,
121        value: T,
122        motion: impl Into<AnyMotion>,
123        now: Time,
124        continuous: bool,
125    ) -> bool {
126        if self.value == value {
127            return false;
128        }
129
130        let current = self.sample(now);
131        self.last_value = if continuous {
132            current.value
133        } else {
134            self.initial_value.clone()
135        };
136        self.value = value;
137        self.motion = motion.into();
138        // Reverse-first playback uses the opposite interpolation axis. Flip
139        // its progress so easing that starts at zero begins at the anchor.
140        self.reverse_progress = self.motion.starts_in_reverse();
141        self.settled_progress = None;
142        self.started_at = Some(now);
143        true
144    }
145
146    /// Sets the target and the value on screen to `value`, with no animation.
147    ///
148    /// This stops any run in progress.
149    pub fn jump_to(&mut self, value: T) {
150        self.value = value.clone();
151        self.last_value = value;
152        self.settled_progress = None;
153        self.started_at = None;
154    }
155
156    /// Aligns an inactive END presentation with its logical target.
157    ///
158    /// Style transitions present the authored target at this endpoint, even if
159    /// interpolation produced a different value. Keeping the same value as the
160    /// next interruption anchor prevents a jump on a later retarget.
161    pub(crate) fn adopt_completed_target(&mut self) {
162        if self.started_at.is_none() && self.settled_progress == Some(Progress::END) {
163            self.last_value = self.value.clone();
164            self.settled_progress = None;
165        }
166    }
167
168    /// Restores the value this animation was created with, with no animation.
169    ///
170    /// This stops any run in progress.
171    pub fn reset(&mut self) {
172        self.value = self.initial_value.clone();
173        self.last_value = self.initial_value.clone();
174        self.settled_progress = None;
175        self.started_at = None;
176    }
177
178    /// Returns the value presented at `now`, and whether the animation can still change.
179    ///
180    /// Call this once per frame. After a run finishes, the sample returns its final value with
181    /// `is_active` set to `false`, and that value becomes the anchor for the next change. Before a
182    /// run starts, sampling returns the last settled value.
183    pub fn sample(&mut self, now: Time) -> AnimatedSample<T> {
184        let Some(started_at) = self.started_at else {
185            return AnimatedSample {
186                value: self.last_value.clone(),
187                is_active: false,
188                progress: self.settled_progress.unwrap_or(Progress::END),
189            };
190        };
191
192        let sample = self.motion.sample_at(started_at, now);
193        let progress = self.presentation_progress(sample.progress);
194        let value = self.last_value.lerp(&self.value, progress.get());
195
196        if !sample.is_active {
197            self.last_value = value.clone();
198            self.settled_progress = Some(progress);
199            self.started_at = None;
200        }
201
202        AnimatedSample {
203            value,
204            is_active: sample.is_active,
205            progress,
206        }
207    }
208
209    pub(crate) fn scale_by(&mut self, ratio: f32)
210    where
211        T: std::ops::Mul<f32, Output = T>,
212    {
213        self.last_value = self.last_value.clone() * ratio;
214        self.value = self.value.clone() * ratio;
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    use crate::{Direction, Motion, SpringConfig};
223
224    fn assert_sample(sample: AnimatedSample<f32>, value: f32, progress: Progress, is_active: bool) {
225        assert_eq!(
226            (sample.value, sample.progress, sample.is_active),
227            (value, progress, is_active)
228        );
229    }
230
231    #[test]
232    fn animated_value_supports_a_complete_lifecycle() {
233        let motion = Motion::new(Duration::from_secs(1));
234        let mut animated = Animated::<f32, Duration>::new(2.0, motion.clone());
235
236        assert_eq!(animated.value(), &2.0);
237        assert_eq!(animated.progress_at(Duration::ZERO), Progress::END);
238        assert_sample(animated.sample(Duration::ZERO), 2.0, Progress::END, false);
239
240        assert!(animated.set(10.0, &motion, Duration::ZERO));
241        assert_eq!(animated.sample(Duration::from_millis(500)).value, 6.0);
242        assert_eq!(
243            animated.progress_at(Duration::from_millis(500)),
244            Progress::clamped(0.5)
245        );
246
247        assert_sample(
248            animated.sample(Duration::from_secs(1)),
249            10.0,
250            Progress::END,
251            false,
252        );
253        assert!(!animated.set(10.0, &motion, Duration::from_secs(2)));
254
255        animated.jump_to(8.0);
256        assert_sample(
257            animated.sample(Duration::from_secs(2)),
258            8.0,
259            Progress::END,
260            false,
261        );
262
263        animated.scale_by(2.0);
264        assert_eq!(animated.value(), &16.0);
265
266        let immediate = Motion::default();
267        assert!(animated.set(12.0, &immediate, Duration::from_secs(2)));
268        assert_sample(
269            animated.sample(Duration::from_secs(2)),
270            12.0,
271            Progress::END,
272            false,
273        );
274        animated.reset();
275
276        assert_eq!(animated.value(), &2.0);
277        assert_sample(
278            animated.sample(Duration::from_millis(500)),
279            2.0,
280            Progress::END,
281            false,
282        );
283    }
284
285    #[test]
286    fn animated_exercises_retargeting_and_non_monotonic_motion() {
287        let motion = Motion::new(Duration::from_secs(1));
288        let mut continuous = Animated::<f32, Duration>::new(0.0, motion.clone());
289        let mut restarting = Animated::<f32, Duration>::new(0.0, motion.clone());
290
291        assert!(continuous.set(10.0, &motion, Duration::ZERO));
292        assert!(restarting.set(10.0, &motion, Duration::ZERO));
293        assert_eq!(continuous.sample(Duration::from_millis(500)).value, 5.0);
294        assert_eq!(restarting.sample(Duration::from_millis(500)).value, 5.0);
295
296        assert!(continuous.set(20.0, &motion, Duration::from_millis(500)));
297        assert!(restarting.restart(20.0, &motion, Duration::from_millis(500)));
298
299        let continuous_anchor = continuous.sample(Duration::from_millis(500));
300        let restarting_anchor = restarting.sample(Duration::from_millis(500));
301        assert_eq!(continuous_anchor.value, 5.0);
302        assert_eq!(restarting_anchor.value, 0.0);
303        assert!(continuous_anchor.is_active);
304        assert!(restarting_anchor.is_active);
305
306        assert_eq!(continuous.sample(Duration::from_secs(1)).value, 12.5);
307        assert_eq!(restarting.sample(Duration::from_secs(1)).value, 10.0);
308        assert!(!continuous.set(20.0, &motion, Duration::from_secs(1)));
309        assert!(!restarting.restart(20.0, &motion, Duration::from_secs(1)));
310
311        let non_monotonic = Motion::new(Duration::from_secs(1)).with_easing(|progress| {
312            if progress < 0.5 {
313                progress * 2.0
314            } else {
315                (1.0 - progress) * 2.0
316            }
317        });
318        let mut animated = Animated::<f32, Duration>::new(0.0, non_monotonic.clone());
319
320        assert!(animated.set(1.0, &non_monotonic, Duration::ZERO));
321        assert_sample(animated.sample(Duration::ZERO), 0.0, Progress::START, true);
322        assert_sample(
323            animated.sample(Duration::from_millis(500)),
324            1.0,
325            Progress::END,
326            true,
327        );
328        assert_sample(
329            animated.sample(Duration::from_secs(1)),
330            0.0,
331            Progress::START,
332            false,
333        );
334        assert_sample(
335            animated.sample(Duration::from_secs(2)),
336            0.0,
337            Progress::START,
338            false,
339        );
340        assert_eq!(
341            animated.progress_at(Duration::from_secs(2)),
342            Progress::START
343        );
344        assert_eq!(animated.value(), &1.0);
345
346        animated.jump_to(2.0);
347        assert_sample(
348            animated.sample(Duration::from_secs(3)),
349            2.0,
350            Progress::END,
351            false,
352        );
353        let spring =
354            Motion::new(Duration::from_secs(1)).with_spring(SpringConfig::new(100.0, 6.0, 1.0));
355        let mut animated = Animated::<f32, Duration>::new(0.0, spring.clone());
356        assert!(animated.set(1.0, &spring, Duration::ZERO));
357
358        assert!(
359            (1..100).any(|step| { animated.sample(Duration::from_millis(step * 10)).value > 1.0 })
360        );
361        assert_eq!(animated.sample(Duration::from_secs(1)).value, 1.0);
362    }
363
364    #[test]
365    fn animated_keeps_the_motion_that_started_each_run() {
366        let one_second = Motion::new(Duration::from_secs(1));
367        let two_seconds = Motion::new(Duration::from_secs(2));
368        let mut animated = Animated::<f32, Duration>::new(0.0, one_second.clone());
369
370        assert!(animated.set(10.0, &one_second, Duration::ZERO));
371        assert_eq!(animated.sample(Duration::from_millis(500)).value, 5.0);
372        assert_eq!(
373            animated.progress_at(Duration::from_millis(750)),
374            Progress::clamped(0.75)
375        );
376
377        assert!(animated.set(20.0, &two_seconds, Duration::from_millis(500)));
378        assert_eq!(animated.sample(Duration::from_millis(500)).value, 5.0);
379        assert_eq!(animated.sample(Duration::from_millis(1_500)).value, 12.5);
380    }
381
382    #[test]
383    fn delayed_retargeting_holds_the_interruption_anchor() {
384        let motion = Motion::new(Duration::from_secs(1)).with_delay(Duration::from_millis(100));
385        let mut animated = Animated::<f32, Duration>::new(0.0, motion.clone());
386
387        assert!(animated.set(10.0, &motion, Duration::ZERO));
388        assert_sample(
389            animated.sample(Duration::from_millis(100)),
390            0.0,
391            Progress::START,
392            true,
393        );
394        assert_eq!(animated.sample(Duration::from_millis(600)).value, 5.0);
395
396        assert!(animated.set(20.0, &motion, Duration::from_millis(600)));
397        for milliseconds in [600, 650, 700] {
398            assert_sample(
399                animated.sample(Duration::from_millis(milliseconds)),
400                5.0,
401                Progress::START,
402                true,
403            );
404        }
405        assert_eq!(animated.sample(Duration::from_millis(1_200)).value, 12.5);
406    }
407
408    #[test]
409    fn reverse_first_retargeting_starts_at_the_current_presentation() {
410        let motion = Motion::new(Duration::from_secs(1)).direction(Direction::Reverse);
411        let mut animated = Animated::<f32, Duration>::new(0.0, motion.clone());
412
413        assert!(animated.set(10.0, &motion, Duration::ZERO));
414        assert_sample(animated.sample(Duration::ZERO), 0.0, Progress::START, true);
415        assert_sample(
416            animated.sample(Duration::from_millis(500)),
417            5.0,
418            Progress::clamped(0.5),
419            true,
420        );
421
422        assert!(animated.set(20.0, &motion, Duration::from_millis(500)));
423        assert_sample(
424            animated.sample(Duration::from_millis(500)),
425            5.0,
426            Progress::START,
427            true,
428        );
429        assert_eq!(animated.sample(Duration::from_secs(1)).value, 12.5);
430        assert_sample(
431            animated.sample(Duration::from_millis(1_500)),
432            20.0,
433            Progress::END,
434            false,
435        );
436
437        let alternating = Motion::new(Duration::from_secs(1))
438            .iterations(2)
439            .direction(Direction::AlternateReverse);
440        assert!(animated.set(30.0, &alternating, Duration::from_secs(2)));
441        assert_eq!(animated.sample(Duration::from_secs(2)).value, 20.0);
442        assert_eq!(animated.sample(Duration::from_secs(3)).value, 30.0);
443        assert_sample(
444            animated.sample(Duration::from_secs(4)),
445            20.0,
446            Progress::START,
447            false,
448        );
449    }
450
451    #[test]
452    fn finite_iterations_settle_at_their_sampled_presentation() {
453        let restarting_motion = Motion::new(Duration::from_secs(1)).iterations(3);
454        let mut restarting = Animated::<f32, Duration>::new(0.0, restarting_motion.clone());
455        assert!(restarting.set(10.0, &restarting_motion, Duration::ZERO));
456        assert_sample(
457            restarting.sample(Duration::from_secs(1)),
458            10.0,
459            Progress::END,
460            true,
461        );
462        assert_sample(
463            restarting.sample(Duration::from_millis(1_500)),
464            5.0,
465            Progress::clamped(0.5),
466            true,
467        );
468        assert_sample(
469            restarting.sample(Duration::from_secs(3)),
470            10.0,
471            Progress::END,
472            false,
473        );
474
475        let alternating_motion = Motion::new(Duration::from_secs(1))
476            .iterations(2)
477            .alternate();
478        let mut idle = Animated::<f32, Duration>::new(4.0, alternating_motion.clone());
479        assert_sample(idle.sample(Duration::ZERO), 4.0, Progress::END, false);
480        idle.jump_to(6.0);
481        assert_sample(idle.sample(Duration::ZERO), 6.0, Progress::END, false);
482
483        let mut alternating = Animated::<f32, Duration>::new(0.0, alternating_motion.clone());
484        assert!(alternating.set(10.0, &alternating_motion, Duration::ZERO));
485        assert_sample(
486            alternating.sample(Duration::from_secs(1)),
487            10.0,
488            Progress::END,
489            true,
490        );
491        assert_sample(
492            alternating.sample(Duration::from_secs(2)),
493            0.0,
494            Progress::START,
495            false,
496        );
497        assert_sample(
498            alternating.sample(Duration::from_secs(20)),
499            0.0,
500            Progress::START,
501            false,
502        );
503
504        let zero_forever = Motion::new(Duration::ZERO).repeat_forever();
505        assert!(alternating.set(20.0, &zero_forever, Duration::from_secs(21)));
506        assert_sample(
507            alternating.sample(Duration::from_secs(21)),
508            0.0,
509            Progress::START,
510            false,
511        );
512    }
513}