Skip to main content

gpui_kit/motion/
transition.rs

1//! Animating a value toward a target that can change mid-flight.
2
3use std::time::Duration;
4
5use gpui::{App, SharedString, Window};
6use web_time::Instant;
7
8use super::{Interpolate, MotionSpec, keyed};
9
10/// A value that animates toward whatever it is last told to be.
11///
12/// Retargeting starts from the value currently on screen rather than from the
13/// previous target, so an interrupted transition does not jump backward.
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub struct Transition<T: Interpolate> {
16    from: T,
17    to: T,
18    spec: MotionSpec,
19    elapsed: Duration,
20    /// Progress velocity carried in from the motion this one interrupted, in
21    /// units of the current distance per second.
22    carried: f32,
23    /// How long the run takes, delay excluded. A spring that was already
24    /// moving needs longer than its resting settle time.
25    duration: Duration,
26    last_frame: Option<Instant>,
27}
28
29impl<T: Interpolate> Transition<T> {
30    /// Starts settled at `value`, so a first render does not animate in.
31    pub fn new(value: T, spec: MotionSpec) -> Self {
32        Self {
33            from: value,
34            to: value,
35            spec,
36            elapsed: spec.total(),
37            carried: 0.0,
38            duration: Self::run_time(spec, 0.0),
39            last_frame: None,
40        }
41    }
42
43    pub fn spec(mut self, spec: MotionSpec) -> Self {
44        self.spec = spec;
45        self.duration = Self::run_time(spec, self.carried);
46        self
47    }
48
49    fn run_time(spec: MotionSpec, carried: f32) -> Duration {
50        match spec.spring() {
51            Some(spring) if carried != 0.0 => spring.settle_time_at(carried),
52            _ => Duration::from_millis(spec.duration_ms),
53        }
54    }
55
56    fn delay(&self) -> Duration {
57        Duration::from_millis(self.spec.delay_ms)
58    }
59
60    fn total(&self) -> Duration {
61        self.delay() + self.duration
62    }
63
64    pub fn target(&self) -> T {
65        self.to
66    }
67
68    pub fn value(&self) -> T {
69        self.from.lerp(self.to, self.progress())
70    }
71
72    pub fn is_animating(&self) -> bool {
73        self.elapsed < self.total()
74    }
75
76    fn progress(&self) -> f32 {
77        let local = self.elapsed.saturating_sub(self.delay());
78        if self.duration.is_zero() || local >= self.duration {
79            return 1.0;
80        }
81        match self.spec.spring() {
82            Some(spring) => spring.value_at(local, self.carried).0,
83            None => self
84                .spec
85                .curve
86                .eval(local.as_secs_f32() / self.duration.as_secs_f32()),
87        }
88    }
89
90    /// How fast progress is moving right now, in progress per second.
91    ///
92    /// A curve reports nothing: a cubic bezier is a shape read off a clock,
93    /// with no state to hand on, so pretending it has momentum would be an
94    /// invention rather than a continuation.
95    fn progress_velocity(&self) -> f32 {
96        let Some(spring) = self.spec.spring() else {
97            return 0.0;
98        };
99        let local = self.elapsed.saturating_sub(self.delay());
100        if self.duration.is_zero() || local >= self.duration {
101            return 0.0;
102        }
103        spring.value_at(local, self.carried).1
104    }
105
106    /// Whether travel along the current path also closes on `target`.
107    ///
108    /// A carried speed needs a direction, and [`Interpolate::distance`] is a
109    /// length with no sign. Stepping a little further along the path the value
110    /// is already on and asking whether that landed nearer `target` recovers
111    /// one, without asking every interpolable value to define an axis. It also
112    /// reads a spring that has overshot correctly, where the value is past its
113    /// target and travelling back.
114    fn heads_toward(&self, target: T) -> bool {
115        const PROBE: f32 = 1e-3;
116        let ahead = self.from.lerp(self.to, self.progress() + PROBE);
117        ahead.distance(target) < self.value().distance(target)
118    }
119
120    /// Aims at a new target. Setting the current target again is a no-op, so a
121    /// render that re-declares the same value does not restart the animation.
122    ///
123    /// A retarget hands the motion on rather than restarting it: the speed the
124    /// value already had is measured, converted into the new distance, and
125    /// released into the new motion. Without that, a target changed mid-flight
126    /// stalls the value for the first few frames of the new run.
127    ///
128    /// The speed keeps its direction, so reversing a target throws the value
129    /// on the way it was already going before it comes back. Turning it round
130    /// on the spot would be the stall this exists to remove, wearing a
131    /// different shape.
132    pub fn set(&mut self, target: T)
133    where
134        T: PartialEq,
135    {
136        if target == self.to {
137            return;
138        }
139        let current = self.value();
140        let speed = self.progress_velocity() * self.from.distance(self.to);
141        let forward = self.heads_toward(target);
142        self.from = current;
143        self.to = target;
144        self.elapsed = Duration::ZERO;
145        let distance = self.from.distance(self.to);
146        self.carried = if distance > 0.0 {
147            let along = if forward { speed } else { -speed };
148            along / distance
149        } else {
150            0.0
151        };
152        self.duration = Self::run_time(self.spec, self.carried);
153    }
154
155    /// Aims at a new target with a speed the value did not get from an
156    /// animation, which is what a value let go of by the hand has.
157    ///
158    /// This is inertia. The gesture reports its speed — see
159    /// [`VelocityTracker`](super::VelocityTracker) — and the spring is
160    /// released with it rather than from a standstill, so a flicked thing
161    /// carries on and settles instead of stopping dead the instant the finger
162    /// leaves it. It is the same handover a retarget performs, with the speed
163    /// coming from outside instead of from the motion being interrupted.
164    ///
165    /// `velocity` is in value units a second and positive toward `target`. A
166    /// release always restarts the motion, including at the current target: a
167    /// value thrown at where it already is has somewhere to go and come back
168    /// from.
169    ///
170    /// Only a sprung specification can carry it. A curve has no momentum, so a
171    /// released curve is an ordinary [`Transition::set`].
172    pub fn release(&mut self, target: T, velocity: f32) {
173        self.from = self.value();
174        self.to = target;
175        self.elapsed = Duration::ZERO;
176        let distance = self.from.distance(self.to);
177        self.carried = if distance > 0.0 {
178            velocity / distance
179        } else {
180            0.0
181        };
182        self.duration = Self::run_time(self.spec, self.carried);
183    }
184
185    /// Jumps to `target` without animating, for state changes the user did not
186    /// cause, such as a theme switch.
187    pub fn snap(&mut self, target: T) {
188        self.from = target;
189        self.to = target;
190        self.carried = 0.0;
191        self.duration = Self::run_time(self.spec, 0.0);
192        self.elapsed = self.total();
193    }
194
195    pub fn advance(&mut self, delta: Duration) {
196        self.elapsed = (self.elapsed + delta).min(self.total());
197    }
198
199    /// Advances by the time since the previous frame and schedules the next
200    /// one while the transition is still running.
201    ///
202    /// Honors reduced motion by finishing immediately, so a caller gets the
203    /// final value without any intermediate frames.
204    pub fn animate(&mut self, window: &mut Window, cx: &mut App) -> T {
205        if cx.reduce_motion() {
206            self.elapsed = self.total();
207            self.last_frame = None;
208            return self.value();
209        }
210
211        let now = cx.background_executor().now();
212        if let Some(last) = self.last_frame {
213            self.advance(now.saturating_duration_since(last));
214        }
215        if self.is_animating() {
216            self.last_frame = Some(now);
217            window.request_animation_frame();
218        } else {
219            self.last_frame = None;
220        }
221        self.value()
222    }
223}
224
225/// One transition kept per semantic id, for a `RenderOnce` builder that is
226/// rebuilt every frame and cannot carry state of its own.
227///
228/// `Default` is what the keyed global needs, and `None` is the honest default:
229/// the transition can only be created once the caller's first target is known,
230/// so it starts settled there rather than animating in from nothing.
231struct Tracked<T: Interpolate>(Option<Transition<T>>);
232
233impl<T: Interpolate> Default for Tracked<T> {
234    fn default() -> Self {
235        Self(None)
236    }
237}
238
239/// Moves the value kept for `id` toward `target` and returns what to draw.
240///
241/// The first frame for an id is already settled, so a control that appears
242/// with a value does not animate up to it from zero.
243pub(crate) fn tracked<T>(
244    id: &SharedString,
245    target: T,
246    spec: MotionSpec,
247    window: &mut Window,
248    cx: &mut App,
249) -> T
250where
251    T: Interpolate + PartialEq + 'static,
252{
253    tracked_or_snap(id, target, spec, false, window, cx)
254}
255
256/// The same, except that `snap` jumps straight to the target.
257///
258/// A control the pointer is holding must be exactly where the pointer is: a
259/// spring that trails the finger by even a frame reads as the control being
260/// broken rather than as motion.
261pub(crate) fn tracked_or_snap<T>(
262    id: &SharedString,
263    target: T,
264    spec: MotionSpec,
265    snap: bool,
266    window: &mut Window,
267    cx: &mut App,
268) -> T
269where
270    T: Interpolate + PartialEq + 'static,
271{
272    let cell = keyed::slot::<Tracked<T>>(id, cx);
273    let mut tracked = cell.borrow_mut();
274    let mut transition = tracked
275        .0
276        .unwrap_or_else(|| Transition::new(target, spec))
277        .spec(spec);
278    if snap {
279        transition.snap(target);
280    } else {
281        transition.set(target);
282    }
283    let shown = transition.animate(window, cx);
284    tracked.0 = Some(transition);
285    shown
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use crate::motion::{CubicBezier, MotionSpec, Spring};
292
293    fn linear(duration_ms: u64) -> MotionSpec {
294        MotionSpec::new(duration_ms, CubicBezier::new(0.0, 0.0, 1.0, 1.0))
295    }
296
297    /// An underdamped spring, so overshoot is available to assert on.
298    fn sprung() -> MotionSpec {
299        MotionSpec::sprung(Spring::new(400.0, 28.0, 1.0))
300    }
301
302    /// A transition caught while it is travelling upward at speed.
303    fn in_flight() -> Transition<f32> {
304        let mut transition = Transition::new(0.0_f32, sprung());
305        transition.set(10.0);
306        transition.advance(Duration::from_millis(40));
307        transition
308    }
309
310    #[test]
311    fn a_new_transition_is_already_settled() {
312        let transition = Transition::new(1.0_f32, linear(200));
313        assert!(!transition.is_animating());
314        assert_eq!(transition.value(), 1.0);
315    }
316
317    #[test]
318    fn advancing_moves_the_value_and_finishes_exactly_on_target() {
319        let mut transition = Transition::new(0.0_f32, linear(200));
320        transition.set(10.0);
321        transition.advance(Duration::from_millis(100));
322        assert!((transition.value() - 5.0).abs() < 0.1);
323        transition.advance(Duration::from_millis(100));
324        assert_eq!(transition.value(), 10.0);
325        assert!(!transition.is_animating());
326    }
327
328    #[test]
329    fn retargeting_continues_from_the_value_on_screen() {
330        let mut transition = Transition::new(0.0_f32, linear(200));
331        transition.set(10.0);
332        transition.advance(Duration::from_millis(100));
333        let interrupted = transition.value();
334
335        transition.set(0.0);
336        assert_eq!(transition.value(), interrupted);
337        transition.advance(Duration::from_millis(200));
338        assert_eq!(transition.value(), 0.0);
339    }
340
341    #[test]
342    fn setting_the_current_target_does_not_restart_the_animation() {
343        let mut transition = Transition::new(0.0_f32, linear(200));
344        transition.set(10.0);
345        transition.advance(Duration::from_millis(100));
346        let midpoint = transition.value();
347        transition.set(10.0);
348        assert_eq!(transition.value(), midpoint);
349    }
350
351    #[test]
352    fn snapping_skips_the_animation_entirely() {
353        let mut transition = Transition::new(0.0_f32, linear(200));
354        transition.snap(10.0);
355        assert_eq!(transition.value(), 10.0);
356        assert!(!transition.is_animating());
357    }
358
359    #[test]
360    fn a_retargeted_spring_keeps_moving_instead_of_starting_again() {
361        let mut carried = in_flight();
362        let interrupted = carried.value();
363        carried.set(20.0);
364
365        let mut from_rest = Transition::new(interrupted, sprung());
366        from_rest.set(20.0);
367
368        for _ in 0..2 {
369            carried.advance(Duration::from_millis(16));
370            from_rest.advance(Duration::from_millis(16));
371        }
372        assert!(
373            carried.value() > interrupted,
374            "the value stalled at {interrupted}"
375        );
376        assert!(
377            carried.value() > from_rest.value(),
378            "a retarget must not throw away the speed the value had: {} against {}",
379            carried.value(),
380            from_rest.value()
381        );
382    }
383
384    #[test]
385    fn a_retarget_rescales_the_speed_it_carries_into_the_new_distance() {
386        let mut transition = in_flight();
387        let speed = transition.progress_velocity() * transition.from.distance(transition.to);
388        transition.set(10.2);
389        let released = transition.progress_velocity() * transition.from.distance(transition.to);
390        assert!(
391            (released - speed).abs() < 1e-2,
392            "a shorter distance changed the speed of the value: {released} against {speed}"
393        );
394    }
395
396    #[test]
397    fn a_spring_that_was_moving_the_other_way_is_given_longer_to_settle() {
398        let mut transition = Transition::new(0.0_f32, sprung());
399        transition.set(10.0);
400        // Past the first overshoot, where the value is on its way back down.
401        transition.advance(Duration::from_millis(300));
402        assert!(transition.progress_velocity() < 0.0);
403
404        // A short hop away from a value moving the wrong way at speed: the
405        // spring has to turn the motion around before it can land.
406        transition.set(transition.value() + 0.1);
407        assert!(transition.total() > sprung().total());
408    }
409
410    #[test]
411    fn reversing_mid_flight_carries_on_before_it_turns_round() {
412        let mut transition = in_flight();
413        let interrupted = transition.value();
414        assert!(transition.progress_velocity() > 0.0);
415
416        transition.set(0.0);
417        let mut highest = f32::MIN;
418        let mut lowest = f32::MAX;
419        while transition.is_animating() {
420            transition.advance(Duration::from_millis(8));
421            highest = highest.max(transition.value());
422            lowest = lowest.min(transition.value());
423        }
424        assert!(
425            highest > interrupted,
426            "a value moving away from its new target has to travel before it \
427             can come back: it turned round on the spot at {interrupted}"
428        );
429        assert!(
430            lowest < 0.0,
431            "an underdamped reversal passes its target, lowest was {lowest}"
432        );
433        assert_eq!(transition.value(), 0.0);
434    }
435
436    #[test]
437    fn a_reversal_and_a_continuation_carry_the_speed_opposite_ways() {
438        let mut onward = in_flight();
439        let mut back = in_flight();
440        assert_eq!(onward.value(), back.value());
441
442        onward.set(20.0);
443        back.set(0.0);
444        assert!(
445            onward.carried > 0.0 && back.carried < 0.0,
446            "the same motion was released {} one way and {} the other",
447            onward.carried,
448            back.carried
449        );
450    }
451
452    #[test]
453    fn a_spring_on_its_way_back_is_read_as_closing_on_a_target_behind_it() {
454        let mut transition = Transition::new(0.0_f32, sprung());
455        transition.set(10.0);
456        // Past the first overshoot: the value is above its target and falling.
457        transition.advance(Duration::from_millis(300));
458        assert!(transition.value() > 10.0);
459        assert!(transition.progress_velocity() < 0.0);
460
461        transition.set(5.0);
462        assert!(
463            transition.carried > 0.0,
464            "a value already falling toward a lower target is closing on it, \
465             but it was released at {}",
466            transition.carried
467        );
468    }
469
470    #[test]
471    fn a_curve_carries_no_speed_across_a_retarget() {
472        let mut transition = Transition::new(0.0_f32, linear(200));
473        transition.set(10.0);
474        transition.advance(Duration::from_millis(100));
475        assert_eq!(transition.value(), 5.0);
476
477        transition.set(0.0);
478        assert_eq!(transition.total(), linear(200).total());
479        transition.advance(Duration::from_millis(100));
480        assert_eq!(
481            transition.value(),
482            2.5,
483            "a bezier has no momentum, so half the remaining distance is exactly half"
484        );
485    }
486
487    #[test]
488    fn advancing_past_the_end_never_overshoots_the_target() {
489        let mut transition = Transition::new(0.0_f32, linear(100));
490        transition.set(1.0);
491        transition.advance(Duration::from_secs(5));
492        assert_eq!(transition.value(), 1.0);
493    }
494}