1use std::time::Duration;
4
5use gpui::{App, SharedString, Window};
6use web_time::Instant;
7
8use super::{Interpolate, MotionSpec, keyed};
9
10#[derive(Debug, Clone, Copy, PartialEq)]
15pub struct Transition<T: Interpolate> {
16 from: T,
17 to: T,
18 spec: MotionSpec,
19 elapsed: Duration,
20 carried: f32,
23 duration: Duration,
26 last_frame: Option<Instant>,
27}
28
29impl<T: Interpolate> Transition<T> {
30 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 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 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 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 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 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 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
225struct Tracked<T: Interpolate>(Option<Transition<T>>);
232
233impl<T: Interpolate> Default for Tracked<T> {
234 fn default() -> Self {
235 Self(None)
236 }
237}
238
239pub(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
256pub(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 fn sprung() -> MotionSpec {
299 MotionSpec::sprung(Spring::new(400.0, 28.0, 1.0))
300 }
301
302 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 transition.advance(Duration::from_millis(300));
402 assert!(transition.progress_velocity() < 0.0);
403
404 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 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}