1use std::{
2 cell::{Ref, RefCell},
3 time::Instant,
4};
5
6use crate::{Animated, App, Entity, EntityId, Motion, Progress, Window, lerp::Lerp};
7
8#[derive(Clone)]
9struct TransitionCache<T> {
10 value: Option<T>,
11 progress: Progress,
12 is_active: bool,
13}
14
15impl<T> TransitionCache<T> {
16 fn empty() -> Self {
17 Self {
18 value: None,
19 progress: Progress::END,
20 is_active: false,
21 }
22 }
23
24 fn clear(&mut self) {
25 self.value = None;
26 }
27}
28
29#[derive(Clone)]
99pub struct Transition<T: Lerp + Clone + PartialEq + 'static> {
100 motion: Motion,
101
102 state: Entity<TransitionState<T>>,
103
104 cache: RefCell<TransitionCache<T>>,
106
107 continuous: bool,
111}
112
113impl<T: Lerp + Clone + PartialEq + 'static> Transition<T> {
114 pub fn new(state: Entity<TransitionState<T>>, motion: impl Into<Motion>) -> Self {
118 Self {
119 motion: motion.into(),
120 state,
121 cache: RefCell::new(TransitionCache::empty()),
122 continuous: true,
123 }
124 }
125
126 pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
131 self.motion = self.motion.with_easing(easing);
132 self.clear_cache();
133 self
134 }
135
136 pub fn continuous(mut self, continuous: bool) -> Self {
141 self.continuous = continuous;
142 self
143 }
144
145 fn sample(&self, cx: &mut App) -> Ref<'_, TransitionCache<T>> {
146 if self.cache.borrow().value.is_none() {
147 let sample = self
151 .state
152 .update(cx, |state, _| state.sample(Instant::now()));
153
154 *self.cache.borrow_mut() = TransitionCache {
155 value: Some(sample.value),
156 progress: sample.progress,
157 is_active: sample.is_active,
158 };
159 }
160
161 self.cache.borrow()
162 }
163
164 fn clear_cache(&self) {
165 self.cache.borrow_mut().clear();
166 }
167
168 pub fn evaluate(&self, window: &mut Window, cx: &mut App) -> Ref<'_, T> {
176 let sample = self.sample(cx);
177 if sample.is_active {
178 window.request_animation_frame();
179 }
180
181 Ref::map(sample, |sample| sample.value.as_ref().unwrap())
182 }
183
184 pub fn read_goal<'b>(&'b self, cx: &'b mut App) -> &'b T {
189 self.state.read(cx).value()
190 }
191
192 pub fn read_cache(&self) -> Ref<'_, Option<T>> {
198 Ref::map(self.cache.borrow(), |cache| &cache.value)
199 }
200
201 pub fn evaluate_delta(&self, cx: &App) -> f32 {
206 if self.cache.borrow().value.is_some() {
207 return self.cache.borrow().progress.get();
208 }
209
210 self.state.read(cx).progress_at(Instant::now()).get()
211 }
212
213 pub fn update<R>(
224 &self,
225 cx: &mut App,
226 update: impl FnOnce(&mut T, &mut crate::Context<TransitionState<T>>) -> R,
227 ) -> bool {
228 let mut was_updated = false;
229
230 self.state.update(cx, |state, cx| {
231 let mut value = state.value().clone();
232 update(&mut value, cx);
233 was_updated = if self.continuous {
234 state.set(value, &self.motion, Instant::now())
235 } else {
236 state.restart(value, &self.motion, Instant::now())
237 };
238 });
239
240 if was_updated {
241 self.clear_cache();
242 }
243
244 was_updated
245 }
246
247 pub fn jump_to(&self, target: T, cx: &mut App) {
252 self.state.update(cx, |state, _cx| state.jump_to(target));
253 self.clear_cache();
254 }
255
256 pub fn scale_by(&self, ratio: f32, cx: &mut App)
261 where
262 T: std::ops::Mul<f32, Output = T>,
263 {
264 self.state.update(cx, |state, _cx| state.scale_by(ratio));
265 self.clear_cache();
266 }
267
268 pub fn entity_id(&self) -> EntityId {
272 self.state.entity_id()
273 }
274
275 pub fn reset(&self, cx: &mut App) {
280 self.state.update(cx, |state, _cx| state.reset());
281 self.clear_cache();
282 }
283}
284
285pub type TransitionState<T> = Animated<T, Instant>;
290
291#[cfg(all(test, feature = "test-support"))]
292mod tests {
293 use std::cell::RefCell;
294 use std::rc::Rc;
295 use std::time::Duration;
296
297 use crate::{AppContext, Context, IntoElement, Render, canvas, px, size};
298
299 use super::*;
300 use gpui::TestAppContext;
301
302 fn create_transition<T: Lerp + Clone + PartialEq + 'static>(
305 cx: &mut App,
306 motion: impl Into<Motion>,
307 initial: T,
308 ) -> Transition<T> {
309 let motion = motion.into();
310 let state = cx.new(|_| TransitionState::new(initial, motion.clone()));
311 Transition::new(state, motion)
312 }
313
314 #[gpui::test]
315 fn unkeyed_transitions_have_distinct_state_that_persists(cx: &mut TestAppContext) {
316 struct Hooks {
317 seen: Rc<RefCell<Vec<[EntityId; 2]>>>,
318 }
319
320 impl Render for Hooks {
321 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
322 let first = window.use_transition(cx, Duration::from_secs(1), |_, _| 2.0_f32);
323 let second = window.use_transition(cx, Duration::from_secs(1), |_, _| 7.0_f32);
324 assert_eq!(*first.read_goal(cx), 2.0);
325 assert_eq!(*second.read_goal(cx), 7.0);
326 self.seen
327 .borrow_mut()
328 .push([first.entity_id(), second.entity_id()]);
329 crate::Empty
330 }
331 }
332
333 let seen = Rc::new(RefCell::new(Vec::new()));
334 let window = cx.open_window(size(px(100.), px(100.)), {
335 let seen = seen.clone();
336 move |_, _| Hooks { seen }
337 });
338 cx.run_until_parked();
339 window.update(cx, |_, _, cx| cx.notify()).unwrap();
340 cx.run_until_parked();
341
342 let seen = seen.borrow();
343 assert!(seen.len() >= 2);
344 assert_ne!(seen[0][0], seen[0][1]);
345 assert!(seen.iter().all(|ids| *ids == seen[0]));
346 }
347
348 struct TransitionTestView {
349 transition: Transition<f32>,
350 rendered_values: Rc<RefCell<Vec<f32>>>,
351 }
352
353 impl Render for TransitionTestView {
354 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
355 let transition = self.transition.clone();
356 let rendered_values = self.rendered_values.clone();
357 canvas(
358 move |_, window, cx| {
359 rendered_values
360 .borrow_mut()
361 .push(*transition.evaluate(window, cx));
362 },
363 |_, _, _, _| {},
364 )
365 }
366 }
367
368 #[gpui::test]
369 fn transition_exercises_state_updates_and_cache(cx: &mut TestAppContext) {
370 cx.update(|cx| {
371 let transition =
372 create_transition(cx, Duration::from_secs(1), 0.0_f32).with_easing(|_| 0.5);
373
374 assert!(transition.read_cache().is_none());
375 assert!(transition.update(cx, |value, _| *value = 100.0));
376 assert_eq!(*transition.read_goal(cx), 100.0);
377 let entity_id = transition.entity_id();
378 assert_eq!(entity_id, transition.entity_id());
379
380 let value1 = *transition.sample(cx).value.as_ref().unwrap();
381 let value2 = *transition.sample(cx).value.as_ref().unwrap();
382 assert_eq!((value1, value2), (50.0, 50.0));
383 assert_eq!(transition.evaluate_delta(cx), 0.5);
384
385 assert!(!transition.update(cx, |value, _| *value = 100.0));
386 assert!(transition.read_cache().is_some());
387 assert!(transition.update(cx, |value, _| *value = 200.0));
388 assert!(transition.read_cache().is_none());
389
390 let transition = transition.with_easing(|_| 0.75);
391 assert!(transition.read_cache().is_none());
392 assert_eq!(*transition.sample(cx).value.as_ref().unwrap(), 125.0);
393 assert_eq!(transition.evaluate_delta(cx), 0.5);
394
395 let motion = Motion::new(Duration::from_secs(1)).with_easing(|_| 0.5);
396 let continuous = create_transition(cx, motion.clone(), 0.0_f32);
397 let restarting = create_transition(cx, motion, 0.0_f32).continuous(false);
398
399 for transition in [&continuous, &restarting] {
400 assert!(transition.update(cx, |value, _| *value = 100.0));
401 assert_eq!(*transition.sample(cx).value.as_ref().unwrap(), 50.0);
402 }
403
404 assert!(continuous.update(cx, |value, _| *value = 200.0));
405 assert!(restarting.update(cx, |value, _| *value = 200.0));
406
407 assert_eq!(*continuous.sample(cx).value.as_ref().unwrap(), 125.0);
408 assert_eq!(*restarting.sample(cx).value.as_ref().unwrap(), 100.0);
409
410 let motion = Motion::new(Duration::from_secs(1)).with_easing(|_| 0.5);
411 let mutators = create_transition(cx, motion, 2.0_f32);
412
413 assert!(mutators.update(cx, |value, _| *value = 10.0));
414 assert_eq!(*mutators.sample(cx).value.as_ref().unwrap(), 6.0);
415
416 mutators.scale_by(2.0, cx);
417 assert_eq!(*mutators.read_goal(cx), 20.0);
418 assert_eq!(*mutators.sample(cx).value.as_ref().unwrap(), 12.0);
419
420 mutators.jump_to(7.0, cx);
421 assert_eq!(*mutators.read_goal(cx), 7.0);
422 assert_eq!(*mutators.sample(cx).value.as_ref().unwrap(), 7.0);
423
424 mutators.reset(cx);
425 assert_eq!(*mutators.read_goal(cx), 2.0);
426 assert!(mutators.read_cache().is_none());
427
428 let alternating = create_transition(
429 cx,
430 Motion::new(Duration::ZERO).iterations(2).alternate(),
431 0.0_f32,
432 );
433 assert!(alternating.update(cx, |value, _| *value = 100.0));
434 let sample = alternating.sample(cx);
435 assert_eq!(sample.value, Some(0.0));
436 assert_eq!(sample.progress, Progress::START);
437 assert!(!sample.is_active);
438 drop(sample);
439 assert_eq!(alternating.evaluate_delta(cx), 0.0);
440 });
441 }
442
443 #[gpui::test]
444 fn evaluate_uses_samples_and_requests_frames_while_active(cx: &mut TestAppContext) {
445 let transition = cx.update(|cx| {
446 create_transition(
447 cx,
448 Motion::new(Duration::from_secs(1)).with_easing(|_| 0.5),
449 0.0_f32,
450 )
451 });
452 cx.update(|cx| {
453 assert!(transition.update(cx, |value, _| *value = 100.0));
454 });
455 let rendered_values = Rc::new(RefCell::new(Vec::new()));
456 let window = cx.open_window(size(px(100.0), px(100.0)), {
457 let transition = transition.clone();
458 let rendered_values = rendered_values.clone();
459 move |_, _| TransitionTestView {
460 transition,
461 rendered_values,
462 }
463 });
464 cx.run_until_parked();
465
466 assert_eq!(*rendered_values.borrow(), vec![50.0]);
467 cx.update(|cx| transition.jump_to(100.0, cx));
468 assert_eq!(
469 window
470 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
471 .unwrap(),
472 1
473 );
474 cx.run_until_parked();
475
476 assert_eq!(*rendered_values.borrow(), vec![50.0, 100.0]);
477 assert_eq!(
478 window
479 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
480 .unwrap(),
481 0
482 );
483 }
484}