use std::{
cell::{Ref, RefCell},
time::Instant,
};
use crate::{Animated, App, Entity, EntityId, Motion, Progress, Window, lerp::Lerp};
#[derive(Clone)]
struct TransitionCache<T> {
value: Option<T>,
progress: Progress,
is_active: bool,
}
impl<T> TransitionCache<T> {
fn empty() -> Self {
Self {
value: None,
progress: Progress::END,
is_active: false,
}
}
fn clear(&mut self) {
self.value = None;
}
}
#[derive(Clone)]
pub struct Transition<T: Lerp + Clone + PartialEq + 'static> {
motion: Motion,
state: Entity<TransitionState<T>>,
cache: RefCell<TransitionCache<T>>,
continuous: bool,
}
impl<T: Lerp + Clone + PartialEq + 'static> Transition<T> {
pub fn new(state: Entity<TransitionState<T>>, motion: impl Into<Motion>) -> Self {
Self {
motion: motion.into(),
state,
cache: RefCell::new(TransitionCache::empty()),
continuous: true,
}
}
pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
self.motion = self.motion.with_easing(easing);
self.clear_cache();
self
}
pub fn continuous(mut self, continuous: bool) -> Self {
self.continuous = continuous;
self
}
fn sample(&self, cx: &mut App) -> Ref<'_, TransitionCache<T>> {
if self.cache.borrow().value.is_none() {
let sample = self
.state
.update(cx, |state, _| state.sample(Instant::now()));
*self.cache.borrow_mut() = TransitionCache {
value: Some(sample.value),
progress: sample.progress,
is_active: sample.is_active,
};
}
self.cache.borrow()
}
fn clear_cache(&self) {
self.cache.borrow_mut().clear();
}
pub fn evaluate(&self, window: &mut Window, cx: &mut App) -> Ref<'_, T> {
let sample = self.sample(cx);
if sample.is_active {
window.request_animation_frame();
}
Ref::map(sample, |sample| sample.value.as_ref().unwrap())
}
pub fn read_goal<'b>(&'b self, cx: &'b mut App) -> &'b T {
self.state.read(cx).value()
}
pub fn read_cache(&self) -> Ref<'_, Option<T>> {
Ref::map(self.cache.borrow(), |cache| &cache.value)
}
pub fn evaluate_delta(&self, cx: &App) -> f32 {
if self.cache.borrow().value.is_some() {
return self.cache.borrow().progress.get();
}
self.state.read(cx).progress_at(Instant::now()).get()
}
pub fn update<R>(
&self,
cx: &mut App,
update: impl FnOnce(&mut T, &mut crate::Context<TransitionState<T>>) -> R,
) -> bool {
let mut was_updated = false;
self.state.update(cx, |state, cx| {
let mut value = state.value().clone();
update(&mut value, cx);
was_updated = if self.continuous {
state.set(value, &self.motion, Instant::now())
} else {
state.restart(value, &self.motion, Instant::now())
};
});
if was_updated {
self.clear_cache();
}
was_updated
}
pub fn jump_to(&self, target: T, cx: &mut App) {
self.state.update(cx, |state, _cx| state.jump_to(target));
self.clear_cache();
}
pub fn scale_by(&self, ratio: f32, cx: &mut App)
where
T: std::ops::Mul<f32, Output = T>,
{
self.state.update(cx, |state, _cx| state.scale_by(ratio));
self.clear_cache();
}
pub fn entity_id(&self) -> EntityId {
self.state.entity_id()
}
pub fn reset(&self, cx: &mut App) {
self.state.update(cx, |state, _cx| state.reset());
self.clear_cache();
}
}
pub type TransitionState<T> = Animated<T, Instant>;
#[cfg(all(test, feature = "test-support"))]
mod tests {
use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration;
use crate::{AppContext, Context, IntoElement, Render, canvas, px, size};
use super::*;
use gpui::TestAppContext;
fn create_transition<T: Lerp + Clone + PartialEq + 'static>(
cx: &mut App,
motion: impl Into<Motion>,
initial: T,
) -> Transition<T> {
let motion = motion.into();
let state = cx.new(|_| TransitionState::new(initial, motion.clone()));
Transition::new(state, motion)
}
#[gpui::test]
fn unkeyed_transitions_have_distinct_state_that_persists(cx: &mut TestAppContext) {
struct Hooks {
seen: Rc<RefCell<Vec<[EntityId; 2]>>>,
}
impl Render for Hooks {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let first = window.use_transition(cx, Duration::from_secs(1), |_, _| 2.0_f32);
let second = window.use_transition(cx, Duration::from_secs(1), |_, _| 7.0_f32);
assert_eq!(*first.read_goal(cx), 2.0);
assert_eq!(*second.read_goal(cx), 7.0);
self.seen
.borrow_mut()
.push([first.entity_id(), second.entity_id()]);
crate::Empty
}
}
let seen = Rc::new(RefCell::new(Vec::new()));
let window = cx.open_window(size(px(100.), px(100.)), {
let seen = seen.clone();
move |_, _| Hooks { seen }
});
cx.run_until_parked();
window.update(cx, |_, _, cx| cx.notify()).unwrap();
cx.run_until_parked();
let seen = seen.borrow();
assert!(seen.len() >= 2);
assert_ne!(seen[0][0], seen[0][1]);
assert!(seen.iter().all(|ids| *ids == seen[0]));
}
struct TransitionTestView {
transition: Transition<f32>,
rendered_values: Rc<RefCell<Vec<f32>>>,
}
impl Render for TransitionTestView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let transition = self.transition.clone();
let rendered_values = self.rendered_values.clone();
canvas(
move |_, window, cx| {
rendered_values
.borrow_mut()
.push(*transition.evaluate(window, cx));
},
|_, _, _, _| {},
)
}
}
#[gpui::test]
fn transition_exercises_state_updates_and_cache(cx: &mut TestAppContext) {
cx.update(|cx| {
let transition =
create_transition(cx, Duration::from_secs(1), 0.0_f32).with_easing(|_| 0.5);
assert!(transition.read_cache().is_none());
assert!(transition.update(cx, |value, _| *value = 100.0));
assert_eq!(*transition.read_goal(cx), 100.0);
let entity_id = transition.entity_id();
assert_eq!(entity_id, transition.entity_id());
let value1 = *transition.sample(cx).value.as_ref().unwrap();
let value2 = *transition.sample(cx).value.as_ref().unwrap();
assert_eq!((value1, value2), (50.0, 50.0));
assert_eq!(transition.evaluate_delta(cx), 0.5);
assert!(!transition.update(cx, |value, _| *value = 100.0));
assert!(transition.read_cache().is_some());
assert!(transition.update(cx, |value, _| *value = 200.0));
assert!(transition.read_cache().is_none());
let transition = transition.with_easing(|_| 0.75);
assert!(transition.read_cache().is_none());
assert_eq!(*transition.sample(cx).value.as_ref().unwrap(), 125.0);
assert_eq!(transition.evaluate_delta(cx), 0.5);
let motion = Motion::new(Duration::from_secs(1)).with_easing(|_| 0.5);
let continuous = create_transition(cx, motion.clone(), 0.0_f32);
let restarting = create_transition(cx, motion, 0.0_f32).continuous(false);
for transition in [&continuous, &restarting] {
assert!(transition.update(cx, |value, _| *value = 100.0));
assert_eq!(*transition.sample(cx).value.as_ref().unwrap(), 50.0);
}
assert!(continuous.update(cx, |value, _| *value = 200.0));
assert!(restarting.update(cx, |value, _| *value = 200.0));
assert_eq!(*continuous.sample(cx).value.as_ref().unwrap(), 125.0);
assert_eq!(*restarting.sample(cx).value.as_ref().unwrap(), 100.0);
let motion = Motion::new(Duration::from_secs(1)).with_easing(|_| 0.5);
let mutators = create_transition(cx, motion, 2.0_f32);
assert!(mutators.update(cx, |value, _| *value = 10.0));
assert_eq!(*mutators.sample(cx).value.as_ref().unwrap(), 6.0);
mutators.scale_by(2.0, cx);
assert_eq!(*mutators.read_goal(cx), 20.0);
assert_eq!(*mutators.sample(cx).value.as_ref().unwrap(), 12.0);
mutators.jump_to(7.0, cx);
assert_eq!(*mutators.read_goal(cx), 7.0);
assert_eq!(*mutators.sample(cx).value.as_ref().unwrap(), 7.0);
mutators.reset(cx);
assert_eq!(*mutators.read_goal(cx), 2.0);
assert!(mutators.read_cache().is_none());
let alternating = create_transition(
cx,
Motion::new(Duration::ZERO).iterations(2).alternate(),
0.0_f32,
);
assert!(alternating.update(cx, |value, _| *value = 100.0));
let sample = alternating.sample(cx);
assert_eq!(sample.value, Some(0.0));
assert_eq!(sample.progress, Progress::START);
assert!(!sample.is_active);
drop(sample);
assert_eq!(alternating.evaluate_delta(cx), 0.0);
});
}
#[gpui::test]
fn evaluate_uses_samples_and_requests_frames_while_active(cx: &mut TestAppContext) {
let transition = cx.update(|cx| {
create_transition(
cx,
Motion::new(Duration::from_secs(1)).with_easing(|_| 0.5),
0.0_f32,
)
});
cx.update(|cx| {
assert!(transition.update(cx, |value, _| *value = 100.0));
});
let rendered_values = Rc::new(RefCell::new(Vec::new()));
let window = cx.open_window(size(px(100.0), px(100.0)), {
let transition = transition.clone();
let rendered_values = rendered_values.clone();
move |_, _| TransitionTestView {
transition,
rendered_values,
}
});
cx.run_until_parked();
assert_eq!(*rendered_values.borrow(), vec![50.0]);
cx.update(|cx| transition.jump_to(100.0, cx));
assert_eq!(
window
.update(cx, |_, window, cx| window.simulate_next_frame(cx))
.unwrap(),
1
);
cx.run_until_parked();
assert_eq!(*rendered_values.borrow(), vec![50.0, 100.0]);
assert_eq!(
window
.update(cx, |_, window, cx| window.simulate_next_frame(cx))
.unwrap(),
0
);
}
}