use scheduler::Instant;
use std::{cell::Cell, rc::Rc, time::Duration};
use crate::{
AnyElement, App, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement,
ParentElement, SpringAnimation, SpringConfig, SpringPlayback, SpringState, SpringTarget,
Window,
};
pub use easing::*;
use smallvec::SmallVec;
#[derive(Clone)]
pub struct Animation {
pub duration: Duration,
pub oneshot: bool,
pub synced: bool,
pub easing: Rc<dyn Fn(f32) -> f32>,
pub max_fps: Option<f32>,
}
impl Animation {
pub fn new(duration: Duration) -> Self {
Self {
duration,
oneshot: true,
synced: false,
easing: Rc::new(linear),
max_fps: None,
}
}
pub fn repeat(mut self) -> Self {
self.oneshot = false;
self
}
pub fn repeat_synced(mut self) -> Self {
self.oneshot = false;
self.synced = true;
self
}
pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
self.easing = Rc::new(easing);
self
}
pub fn with_max_fps(mut self, max_fps: f32) -> Self {
self.max_fps = Some(max_fps);
self
}
}
pub trait AnimationExt {
fn with_animation(
self,
id: impl Into<ElementId>,
animation: Animation,
animator: impl Fn(Self, f32) -> Self + 'static,
) -> AnimationElement<Self>
where
Self: Sized,
{
AnimationElement {
id: id.into(),
element: Some(self),
animator: Box::new(move |this, _, value| animator(this, value)),
animations: smallvec::smallvec![animation],
}
}
fn with_animations(
self,
id: impl Into<ElementId>,
animations: Vec<Animation>,
animator: impl Fn(Self, usize, f32) -> Self + 'static,
) -> AnimationElement<Self>
where
Self: Sized,
{
AnimationElement {
id: id.into(),
element: Some(self),
animator: Box::new(animator),
animations: animations.into(),
}
}
fn with_spring<T>(
self,
id: impl Into<ElementId>,
animation: SpringAnimation<T>,
animator: impl FnOnce(Self, T::Output) -> Self + 'static,
) -> SpringAnimationElement<Self>
where
Self: Sized,
T: SpringTarget,
T::Output: 'static,
{
let SpringAnimation {
config,
target,
epsilon,
initial,
playback,
} = animation;
let scalar_target = target.target();
SpringAnimationElement {
id: id.into(),
element: Some(self),
config,
target: scalar_target,
epsilon,
initial,
playback,
animator: Some(Box::new(move |this, value| {
animator(this, target.resolve(value))
})),
}
}
}
impl<E: IntoElement + 'static> AnimationExt for E {}
pub struct AnimationElement<E> {
id: ElementId,
element: Option<E>,
animations: SmallVec<[Animation; 1]>,
animator: Box<dyn Fn(E, usize, f32) -> E + 'static>,
}
pub struct SpringAnimationElement<E> {
id: ElementId,
element: Option<E>,
config: SpringConfig,
target: f32,
epsilon: f32,
initial: Option<f32>,
playback: SpringPlayback,
animator: Option<Box<dyn FnOnce(E, f32) -> E + 'static>>,
}
impl<E: ParentElement> ParentElement for SpringAnimationElement<E> {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
let Some(element) = &mut self.element else {
return;
};
element.extend(elements);
}
}
impl<E> SpringAnimationElement<E> {
pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> SpringAnimationElement<E> {
self.element = self.element.map(f);
self
}
}
impl<E: IntoElement + 'static> IntoElement for SpringAnimationElement<E> {
type Element = SpringAnimationElement<E>;
fn into_element(self) -> Self::Element {
self
}
}
impl<E: ParentElement> ParentElement for AnimationElement<E> {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
let Some(element) = &mut self.element else {
return;
};
element.extend(elements);
}
}
impl<E> AnimationElement<E> {
pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement<E> {
self.element = self.element.map(f);
self
}
}
impl<E: IntoElement + 'static> IntoElement for AnimationElement<E> {
type Element = AnimationElement<E>;
fn into_element(self) -> Self::Element {
self
}
}
struct AnimationState {
start: Instant,
animation_ix: usize,
delayed_frame_pending: Rc<Cell<bool>>,
}
struct SpringElementState {
spring: SpringState,
target: f32,
config: SpringConfig,
initial: f32,
playback: SpringPlayback,
updated_at: Instant,
}
impl<E: IntoElement + 'static> Element for SpringAnimationElement<E> {
type RequestLayoutState = AnyElement;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (crate::LayoutId, Self::RequestLayoutState) {
window.with_element_state(global_id.unwrap(), |state, window| {
let now = Instant::now();
let initial = self.initial.unwrap_or(self.target);
let mut state = state.unwrap_or_else(|| SpringElementState {
spring: SpringState {
position: initial,
velocity: 0.0,
},
target: self.target,
config: self.config,
initial,
playback: self.playback,
updated_at: now,
});
let elapsed = now.duration_since(state.updated_at).as_secs_f32();
match state.playback {
SpringPlayback::Running => {
state.spring = state.config.step(state.spring, state.target, elapsed);
}
SpringPlayback::Paused
| SpringPlayback::Stopped
| SpringPlayback::Completed
| SpringPlayback::Cancelled => {}
}
state.config = self.config;
state.target = self.target;
let done = match self.playback {
SpringPlayback::Running => {
if cx.reduce_motion() {
state.spring = SpringState {
position: state.target,
velocity: 0.0,
};
true
} else {
let done =
state
.config
.is_settled(state.spring, state.target, self.epsilon);
if done {
state.spring = SpringState {
position: state.target,
velocity: 0.0,
};
}
done
}
}
SpringPlayback::Paused => true,
SpringPlayback::Stopped => {
state.spring.velocity = 0.0;
true
}
SpringPlayback::Completed => {
state.spring = SpringState {
position: state.target,
velocity: 0.0,
};
true
}
SpringPlayback::Cancelled => {
state.spring = SpringState {
position: state.initial,
velocity: 0.0,
};
true
}
};
state.playback = self.playback;
state.updated_at = now;
let element = self.element.take().expect("should only be called once");
let animator = self.animator.take().expect("should only be called once");
let mut element = animator(element, state.spring.position).into_any_element();
if !done {
window.request_animation_frame();
}
((element.request_layout(window, cx), element), state)
})
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: crate::Bounds<crate::Pixels>,
element: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
element.prepaint(window, cx);
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: crate::Bounds<crate::Pixels>,
element: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
element.paint(window, cx);
}
}
impl<E: IntoElement + 'static> Element for AnimationElement<E> {
type RequestLayoutState = AnyElement;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (crate::LayoutId, Self::RequestLayoutState) {
window.with_element_state(global_id.unwrap(), |state, window| {
let mut state = state.unwrap_or_else(|| AnimationState {
start: Instant::now(),
animation_ix: 0,
delayed_frame_pending: Rc::new(Cell::new(false)),
});
let (animation_ix, delta, done) = if cx.reduce_motion() {
let animation_ix = self.animations.len() - 1;
let delta = if self.animations[animation_ix].oneshot {
1.0
} else {
0.0
};
(animation_ix, delta, true)
} else {
let animation_ix = state.animation_ix;
let duration = self.animations[animation_ix].duration;
let elapsed = if self.animations[animation_ix].synced && !duration.is_zero() {
let elapsed = cx.background_executor().now() - cx.synced_animation_epoch;
Duration::from_nanos((elapsed.as_nanos() % duration.as_nanos()) as u64)
} else {
state.start.elapsed()
};
let mut delta = elapsed.as_secs_f32() / duration.as_secs_f32();
let mut done = false;
if delta > 1.0 {
if self.animations[animation_ix].oneshot {
if animation_ix >= self.animations.len() - 1 {
done = true;
} else {
state.start = Instant::now();
state.animation_ix += 1;
}
delta = 1.0;
} else {
delta %= 1.0;
}
}
(animation_ix, delta, done)
};
let delta = (self.animations[animation_ix].easing)(delta);
debug_assert!(delta.is_finite(), "animated value should be finite");
let element = self.element.take().expect("should only be called once");
let mut element = (self.animator)(element, animation_ix, delta).into_any_element();
if !done {
match self.animations[animation_ix].max_fps {
Some(max_fps) if max_fps.is_finite() && max_fps > 0.0 => {
if !state.delayed_frame_pending.get() {
state.delayed_frame_pending.set(true);
let delayed_frame_pending = state.delayed_frame_pending.clone();
let view = window.current_view();
let interval = Duration::from_secs_f32(1.0 / max_fps);
window
.spawn(cx, async move |cx| {
cx.background_executor().timer(interval).await;
delayed_frame_pending.set(false);
cx.update(move |_, cx| cx.notify(view)).ok();
})
.detach();
}
}
_ => window.request_animation_frame(),
}
}
((element.request_layout(window, cx), element), state)
})
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: crate::Bounds<crate::Pixels>,
element: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
element.prepaint(window, cx);
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: crate::Bounds<crate::Pixels>,
element: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
element.paint(window, cx);
}
}
mod easing {
use std::f32::consts::PI;
pub fn linear(delta: f32) -> f32 {
delta
}
pub fn quadratic(delta: f32) -> f32 {
delta * delta
}
pub fn ease_in_out(delta: f32) -> f32 {
if delta < 0.5 {
2.0 * delta * delta
} else {
let x = -2.0 * delta + 2.0;
1.0 - x * x / 2.0
}
}
pub fn ease_out_quint() -> impl Fn(f32) -> f32 {
move |delta| 1.0 - (1.0 - delta).powi(5)
}
pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 {
move |delta| {
if delta < 0.5 {
easing(delta * 2.0)
} else {
easing((1.0 - delta) * 2.0)
}
}
}
pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 {
let range = max - min;
move |delta| {
let t = (delta * 2.0 * PI).sin();
let breath = (t * t * t + t) / 2.0;
let normalized_alpha = (breath + 1.0) / 2.0;
min + (normalized_alpha * range)
}
}
}
#[cfg(test)]
mod tests {
use std::{cell::RefCell, rc::Rc, time::Duration};
use crate::{
Animation, Context, InteractiveElement, Pixels, Render, SpringAnimation, SpringConfig,
TestAppContext, WindowHandle, div, prelude::*, px, size,
};
use super::*;
struct AnimationTestView {
rendered_deltas: Rc<RefCell<Vec<f32>>>,
max_fps: Option<f32>,
}
struct SyncedAnimationTestView {
show_second: bool,
first_deltas: Rc<RefCell<Vec<f32>>>,
second_deltas: Rc<RefCell<Vec<f32>>>,
}
struct SpringAnimationTestView {
target: Pixels,
initial: Option<Pixels>,
playback: SpringPlayback,
rendered_values: Rc<RefCell<Vec<Pixels>>>,
}
impl Render for SpringAnimationTestView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let rendered_values = self.rendered_values.clone();
let mut animation = SpringAnimation::new(SpringConfig::new(100.0, 2.0, 1.0))
.to(self.target)
.with_epsilon(0.01)
.playback(self.playback);
if let Some(initial) = self.initial {
animation = animation.from(initial);
}
div().with_spring("spring-animation", animation, move |this, value| {
rendered_values.borrow_mut().push(value);
this.left(value)
})
}
}
impl Render for SyncedAnimationTestView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let record_deltas = |deltas: Rc<RefCell<Vec<f32>>>| {
move |this, delta| {
deltas.borrow_mut().push(delta);
this
}
};
div()
.size_full()
.child(div().with_animation(
"first-synced-animation",
Animation::new(Duration::from_secs(1)).repeat_synced(),
record_deltas(self.first_deltas.clone()),
))
.when(self.show_second, |this| {
this.child(div().with_animation(
"second-synced-animation",
Animation::new(Duration::from_secs(1)).repeat_synced(),
record_deltas(self.second_deltas.clone()),
))
})
}
}
impl Render for AnimationTestView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let rendered_deltas = self.rendered_deltas.clone();
let mut animation = Animation::new(Duration::from_secs(1));
if let Some(max_fps) = self.max_fps {
animation = animation.repeat_synced().with_max_fps(max_fps);
} else {
animation = animation.repeat();
}
div().size_full().child(div().with_animation(
"repeating-animation",
animation,
move |this, delta| {
rendered_deltas.borrow_mut().push(delta);
this
},
))
}
}
fn open_test_window(
cx: &mut TestAppContext,
) -> (Rc<RefCell<Vec<f32>>>, WindowHandle<AnimationTestView>) {
open_test_window_with_max_fps(cx, None)
}
fn open_test_window_with_max_fps(
cx: &mut TestAppContext,
max_fps: Option<f32>,
) -> (Rc<RefCell<Vec<f32>>>, WindowHandle<AnimationTestView>) {
let rendered_deltas = Rc::new(RefCell::new(Vec::new()));
let window = cx.open_window(size(px(100.), px(100.)), {
let rendered_deltas = rendered_deltas.clone();
move |_, _| AnimationTestView {
rendered_deltas,
max_fps,
}
});
cx.run_until_parked();
(rendered_deltas, window)
}
fn simulate_next_frame<V: Render>(window: &WindowHandle<V>, cx: &mut TestAppContext) -> usize {
let callback_count = window
.update(cx, |_, window, cx| window.simulate_next_frame(cx))
.unwrap();
cx.run_until_parked();
callback_count
}
#[test]
fn test_animation_parent() {
div()
.id("id")
.with_animation(
"animation",
Animation::new(Duration::from_secs(1)),
|el, _t| {
el
},
)
.child(
div(),
);
}
#[test]
fn test_spring_animation_parent() {
div()
.id("id")
.with_spring(
"spring-animation",
SpringAnimation::new(SpringConfig::new(100.0, 10.0, 1.0))
.to(px(10.0))
.from(px(0.0)),
|element, value| element.left(value),
)
.child(div());
}
#[gpui::test]
fn test_spring_animation_preserves_velocity_when_retargeted(cx: &mut TestAppContext) {
let rendered_values = Rc::new(RefCell::new(Vec::new()));
let window = cx.open_window(size(px(100.0), px(100.0)), {
let rendered_values = rendered_values.clone();
move |_, _| SpringAnimationTestView {
target: px(0.0),
initial: None,
playback: SpringPlayback::Running,
rendered_values,
}
});
cx.run_until_parked();
assert_eq!(*rendered_values.borrow(), vec![px(0.0)]);
window
.update(cx, |view, _, cx| {
view.target = px(100.0);
cx.notify();
})
.unwrap();
cx.run_until_parked();
cx.executor().advance_clock(Duration::from_millis(50));
assert!(simulate_next_frame(&window, cx) > 0);
let value_before_retargeting = *rendered_values.borrow().last().unwrap();
assert!(value_before_retargeting > px(0.0));
assert!(value_before_retargeting < px(100.0));
window
.update(cx, |view, _, cx| {
view.target = px(0.0);
cx.notify();
})
.unwrap();
cx.run_until_parked();
cx.executor().advance_clock(Duration::from_millis(5));
assert!(simulate_next_frame(&window, cx) > 0);
let value_after_retargeting = *rendered_values.borrow().last().unwrap();
assert!(value_after_retargeting > value_before_retargeting);
}
#[gpui::test]
fn test_paused_spring_resumes_with_its_velocity(cx: &mut TestAppContext) {
let rendered_values = Rc::new(RefCell::new(Vec::new()));
let window = cx.open_window(size(px(100.0), px(100.0)), {
let rendered_values = rendered_values.clone();
move |_, _| SpringAnimationTestView {
target: px(0.0),
initial: None,
playback: SpringPlayback::Running,
rendered_values,
}
});
cx.run_until_parked();
window
.update(cx, |view, _, cx| {
view.target = px(100.0);
cx.notify();
})
.unwrap();
cx.run_until_parked();
cx.executor().advance_clock(Duration::from_millis(50));
assert!(simulate_next_frame(&window, cx) > 0);
window
.update(cx, |view, _, cx| {
view.target = px(0.0);
view.playback = SpringPlayback::Paused;
cx.notify();
})
.unwrap();
cx.run_until_parked();
let paused_value = *rendered_values.borrow().last().unwrap();
cx.executor().advance_clock(Duration::from_millis(500));
assert!(simulate_next_frame(&window, cx) > 0);
assert_eq!(*rendered_values.borrow().last().unwrap(), paused_value);
assert_eq!(simulate_next_frame(&window, cx), 0);
window
.update(cx, |view, _, cx| {
view.playback = SpringPlayback::Running;
cx.notify();
})
.unwrap();
cx.run_until_parked();
cx.executor().advance_clock(Duration::from_millis(5));
assert!(simulate_next_frame(&window, cx) > 0);
assert!(*rendered_values.borrow().last().unwrap() > paused_value);
}
#[gpui::test]
fn test_stopped_spring_resumes_without_velocity(cx: &mut TestAppContext) {
let rendered_values = Rc::new(RefCell::new(Vec::new()));
let window = cx.open_window(size(px(100.0), px(100.0)), {
let rendered_values = rendered_values.clone();
move |_, _| SpringAnimationTestView {
target: px(0.0),
initial: None,
playback: SpringPlayback::Running,
rendered_values,
}
});
cx.run_until_parked();
window
.update(cx, |view, _, cx| {
view.target = px(1_000_000.0);
cx.notify();
})
.unwrap();
cx.run_until_parked();
cx.executor().advance_clock(Duration::from_millis(50));
assert!(simulate_next_frame(&window, cx) > 0);
window
.update(cx, |view, _, cx| {
view.target = px(0.0);
view.playback = SpringPlayback::Stopped;
cx.notify();
})
.unwrap();
cx.run_until_parked();
let stopped_value = *rendered_values.borrow().last().unwrap();
cx.executor().advance_clock(Duration::from_millis(500));
assert!(simulate_next_frame(&window, cx) > 0);
assert_eq!(*rendered_values.borrow().last().unwrap(), stopped_value);
assert_eq!(simulate_next_frame(&window, cx), 0);
window
.update(cx, |view, _, cx| {
view.target = stopped_value;
view.playback = SpringPlayback::Running;
cx.notify();
})
.unwrap();
cx.run_until_parked();
assert_eq!(*rendered_values.borrow().last().unwrap(), stopped_value);
assert_eq!(simulate_next_frame(&window, cx), 0);
}
#[gpui::test]
fn test_cancelled_and_completed_springs_resolve_their_endpoints(cx: &mut TestAppContext) {
let rendered_values = Rc::new(RefCell::new(Vec::new()));
let window = cx.open_window(size(px(100.0), px(100.0)), {
let rendered_values = rendered_values.clone();
move |_, _| SpringAnimationTestView {
target: px(100.0),
initial: Some(px(20.0)),
playback: SpringPlayback::Running,
rendered_values,
}
});
cx.run_until_parked();
assert_eq!(*rendered_values.borrow(), vec![px(20.0)]);
cx.executor().advance_clock(Duration::from_millis(50));
assert!(simulate_next_frame(&window, cx) > 0);
assert!(*rendered_values.borrow().last().unwrap() > px(20.0));
window
.update(cx, |view, _, cx| {
view.playback = SpringPlayback::Cancelled;
cx.notify();
})
.unwrap();
cx.run_until_parked();
assert_eq!(*rendered_values.borrow().last().unwrap(), px(20.0));
assert!(simulate_next_frame(&window, cx) > 0);
assert_eq!(simulate_next_frame(&window, cx), 0);
window
.update(cx, |view, _, cx| {
view.playback = SpringPlayback::Completed;
cx.notify();
})
.unwrap();
cx.run_until_parked();
assert_eq!(*rendered_values.borrow().last().unwrap(), px(100.0));
assert_eq!(simulate_next_frame(&window, cx), 0);
}
#[gpui::test]
fn test_spring_animation_respects_reduced_motion(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_reduce_motion(true));
let rendered_values = Rc::new(RefCell::new(Vec::new()));
let window = cx.open_window(size(px(100.0), px(100.0)), {
let rendered_values = rendered_values.clone();
move |_, _| SpringAnimationTestView {
target: px(100.0),
initial: None,
playback: SpringPlayback::Running,
rendered_values,
}
});
cx.run_until_parked();
assert_eq!(*rendered_values.borrow(), vec![px(100.0)]);
assert_eq!(simulate_next_frame(&window, cx), 0);
}
#[gpui::test]
fn test_repeating_animation_schedules_animation_frames(cx: &mut TestAppContext) {
let (rendered_deltas, window) = open_test_window(cx);
assert_eq!(rendered_deltas.borrow().len(), 1);
for expected_frames in 2..=3 {
assert_eq!(simulate_next_frame(&window, cx), 1);
assert_eq!(rendered_deltas.borrow().len(), expected_frames);
}
}
#[gpui::test]
fn test_max_fps_schedules_timer_driven_frames(cx: &mut TestAppContext) {
let (rendered_deltas, window) = open_test_window_with_max_fps(cx, Some(10.0));
let assert_deltas_approx_eq = |expected: &[f32]| {
let actual = rendered_deltas.borrow();
assert_eq!(actual.len(), expected.len(), "deltas: {actual:?}");
for (actual, expected) in actual.iter().zip(expected) {
assert!(
(actual - expected).abs() < 1e-2,
"expected {expected}, got {actual}"
);
}
};
assert_deltas_approx_eq(&[0.0]);
assert_eq!(simulate_next_frame(&window, cx), 0);
assert_deltas_approx_eq(&[0.0]);
cx.executor().advance_clock(Duration::from_millis(105));
cx.run_until_parked();
assert_deltas_approx_eq(&[0.0, 0.105]);
cx.executor().advance_clock(Duration::from_millis(105));
cx.run_until_parked();
assert_deltas_approx_eq(&[0.0, 0.105, 0.21]);
}
#[gpui::test]
fn test_synced_animations_share_phase_across_elements(cx: &mut TestAppContext) {
let first_deltas = Rc::new(RefCell::new(Vec::new()));
let second_deltas = Rc::new(RefCell::new(Vec::new()));
let window = cx.open_window(size(px(100.), px(100.)), {
let first_deltas = first_deltas.clone();
let second_deltas = second_deltas.clone();
move |_, _| SyncedAnimationTestView {
show_second: false,
first_deltas,
second_deltas,
}
});
cx.run_until_parked();
assert_eq!(*first_deltas.borrow(), vec![0.0]);
cx.executor().advance_clock(Duration::from_millis(250));
simulate_next_frame(&window, cx);
assert_eq!(*first_deltas.borrow(), vec![0.0, 0.25]);
window
.update(cx, |view, _, cx| {
view.show_second = true;
cx.notify();
})
.unwrap();
cx.run_until_parked();
cx.executor().advance_clock(Duration::from_millis(250));
simulate_next_frame(&window, cx);
assert_eq!(*second_deltas.borrow().last().unwrap(), 0.5);
assert_eq!(
*first_deltas.borrow().last().unwrap(),
*second_deltas.borrow().last().unwrap()
);
assert!(second_deltas.borrow().iter().all(|delta| *delta > 0.0));
cx.executor().advance_clock(Duration::from_millis(2250));
simulate_next_frame(&window, cx);
assert_eq!(*first_deltas.borrow().last().unwrap(), 0.75);
cx.executor()
.advance_clock(Duration::from_secs(300 * 24 * 60 * 60) + Duration::from_millis(500));
simulate_next_frame(&window, cx);
assert_eq!(*first_deltas.borrow().last().unwrap(), 0.25);
}
#[gpui::test]
fn test_reduce_motion_renders_single_static_frame(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_reduce_motion(true));
let (rendered_deltas, window) = open_test_window(cx);
assert_eq!(*rendered_deltas.borrow(), vec![0.0]);
assert_eq!(simulate_next_frame(&window, cx), 0);
assert_eq!(*rendered_deltas.borrow(), vec![0.0]);
}
}