use std::{
cell::Cell,
rc::Rc,
time::{Duration, Instant},
};
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 AnimationHandle {
cancelled: Rc<Cell<bool>>,
}
impl AnimationHandle {
fn new() -> Self {
Self {
cancelled: Rc::new(Cell::new(false)),
}
}
pub fn cancel(&self) {
self.cancelled.set(true);
}
pub fn is_cancelled(&self) -> bool {
self.cancelled.get()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum AnimationRepeat {
#[default]
Once,
Forever,
Times(u32),
}
#[derive(Clone)]
pub struct Animation {
pub duration: Duration,
pub delay: Duration,
pub repeat: AnimationRepeat,
pub easing: Rc<dyn Fn(f32) -> f32>,
pub max_fps: Option<f32>,
}
impl Animation {
pub fn new(duration: Duration) -> Self {
Self {
duration,
delay: Duration::ZERO,
repeat: AnimationRepeat::Once,
easing: Rc::new(linear),
max_fps: None,
}
}
pub fn repeat(mut self) -> Self {
self.repeat = AnimationRepeat::Forever;
self
}
pub fn repeat_n(mut self, times: u32) -> Self {
self.repeat = AnimationRepeat::Times(times);
self
}
pub fn with_repeat(mut self, repeat: AnimationRepeat) -> Self {
self.repeat = repeat;
self
}
pub fn with_delay(mut self, delay: Duration) -> Self {
self.delay = delay;
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],
cancel_handle: None,
}
}
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(),
cancel_handle: None,
}
}
fn with_cancellable_animation(
self,
id: impl Into<ElementId>,
animation: Animation,
animator: impl Fn(Self, f32) -> Self + 'static,
) -> (AnimationElement<Self>, AnimationHandle)
where
Self: Sized,
{
let handle = AnimationHandle::new();
let element = AnimationElement {
id: id.into(),
element: Some(self),
animator: Box::new(move |this, _, value| animator(this, value)),
animations: smallvec::smallvec![animation],
cancel_handle: Some(handle.cancelled.clone()),
};
(element, handle)
}
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>,
cancel_handle: Option<Rc<Cell<bool>>>,
}
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,
repaint_pending: Rc<Cell<bool>>,
repaint_generation: Rc<Cell<u64>>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum AnimationPass {
Delayed,
Running { cycle_delta: f32 },
Finished,
}
fn animation_pass(animation: &Animation, elapsed: Duration) -> AnimationPass {
if elapsed < animation.delay {
return AnimationPass::Delayed;
}
let elapsed = elapsed - animation.delay;
if animation.duration.is_zero() {
return AnimationPass::Finished;
}
match animation.repeat {
AnimationRepeat::Once => {
if elapsed >= animation.duration {
AnimationPass::Finished
} else {
AnimationPass::Running {
cycle_delta: elapsed.as_secs_f32() / animation.duration.as_secs_f32(),
}
}
}
AnimationRepeat::Forever => {
let delta = elapsed.as_secs_f32() / animation.duration.as_secs_f32();
AnimationPass::Running {
cycle_delta: delta % 1.0,
}
}
AnimationRepeat::Times(times) => {
let times = times.max(1);
let total_duration = animation
.duration
.checked_mul(times)
.unwrap_or(Duration::MAX);
if elapsed >= total_duration {
AnimationPass::Finished
} else {
let delta = elapsed.as_secs_f32() / animation.duration.as_secs_f32();
AnimationPass::Running {
cycle_delta: delta % 1.0,
}
}
}
}
}
fn schedule_repaint(
window: &mut Window,
cx: &mut App,
delay: Duration,
repaint_pending: &Rc<Cell<bool>>,
repaint_generation: &Rc<Cell<u64>>,
) {
if repaint_pending.get() {
return;
}
repaint_pending.set(true);
let generation = repaint_generation.get().wrapping_add(1);
repaint_generation.set(generation);
let repaint_pending = repaint_pending.clone();
let repaint_generation = repaint_generation.clone();
let view = window.current_view();
window
.spawn(cx, async move |cx| {
cx.background_executor().timer(delay).await;
if repaint_generation.get() != generation {
return;
}
repaint_pending.set(false);
cx.update(move |_, cx| cx.notify(view)).ok();
})
.detach();
}
fn invalidate_scheduled_repaint(state: &AnimationState) {
state
.repaint_generation
.set(state.repaint_generation.get().wrapping_add(1));
state.repaint_pending.set(false);
}
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 = cx.background_executor().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 => {
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 now = cx.background_executor().now();
let mut state = state.unwrap_or_else(|| AnimationState {
start: now,
animation_ix: 0,
repaint_pending: Rc::new(Cell::new(false)),
repaint_generation: Rc::new(Cell::new(0)),
});
let cancelled = self.cancel_handle.as_ref().map_or(false, |h| h.get());
let animation_ix = state.animation_ix;
let animation = self.animations[animation_ix].clone();
let (delta, done) = if cancelled {
(1.0_f32, true)
} else {
let elapsed = now - state.start;
match animation_pass(&animation, elapsed) {
AnimationPass::Delayed => ((animation.easing)(0.0), false),
AnimationPass::Running { cycle_delta } => {
((animation.easing)(cycle_delta), false)
}
AnimationPass::Finished => {
if animation_ix >= self.animations.len() - 1 {
((animation.easing)(1.0), true)
} else {
state.start = now;
state.animation_ix += 1;
((animation.easing)(1.0), false)
}
}
}
};
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();
let animation_changed = state.animation_ix != animation_ix;
if animation_changed || done {
invalidate_scheduled_repaint(&state);
}
if !done {
let animation = &self.animations[state.animation_ix];
let elapsed = now - state.start;
if elapsed < animation.delay {
let remaining = animation.delay - elapsed;
schedule_repaint(
window,
cx,
remaining,
&state.repaint_pending,
&state.repaint_generation,
);
} else {
match animation.max_fps {
Some(max_fps) if max_fps.is_finite() && max_fps > 0.0 => {
let interval = Duration::from_secs_f32(1.0 / max_fps);
schedule_repaint(
window,
cx,
interval,
&state.repaint_pending,
&state.repaint_generation,
);
}
_ => 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};
use crate::{
Context, InteractiveElement, IntoElement, Pixels, Render, SpringPlayback, Styled,
TestAppContext, Window, WindowHandle, div, px,
};
use super::*;
#[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());
}
#[test]
fn test_with_max_fps_stores_positive_and_invalid_rates() {
let capped = Animation::new(Duration::from_secs(1)).with_max_fps(12.0);
assert_eq!(capped.max_fps, Some(12.0));
let invalid = Animation::new(Duration::from_secs(1)).with_max_fps(0.0);
assert_eq!(invalid.max_fps, Some(0.0));
}
#[test]
fn test_with_delay_stores_delay() {
let delayed = Animation::new(Duration::from_secs(1)).with_delay(Duration::from_millis(250));
assert_eq!(delayed.delay, Duration::from_millis(250));
let immediate = Animation::new(Duration::from_secs(1));
assert_eq!(immediate.delay, Duration::ZERO);
}
#[test]
fn test_repeat_builders() {
assert_eq!(
Animation::new(Duration::from_secs(1)).repeat,
AnimationRepeat::Once
);
assert_eq!(
Animation::new(Duration::from_secs(1)).repeat().repeat,
AnimationRepeat::Forever
);
assert_eq!(
Animation::new(Duration::from_secs(1)).repeat_n(3).repeat,
AnimationRepeat::Times(3)
);
assert_eq!(
Animation::new(Duration::from_secs(1))
.with_repeat(AnimationRepeat::Times(2))
.repeat,
AnimationRepeat::Times(2)
);
}
#[test]
fn test_animation_pass_once() {
let animation = Animation::new(Duration::from_secs(2));
assert_eq!(
animation_pass(&animation, Duration::ZERO),
AnimationPass::Running { cycle_delta: 0.0 }
);
assert_eq!(
animation_pass(&animation, Duration::from_secs(1)),
AnimationPass::Running { cycle_delta: 0.5 }
);
assert_eq!(
animation_pass(&animation, Duration::from_secs(2)),
AnimationPass::Finished
);
assert_eq!(
animation_pass(&animation, Duration::from_secs(3)),
AnimationPass::Finished
);
}
#[test]
fn test_animation_pass_delay_holds_zero_then_runs() {
let animation =
Animation::new(Duration::from_secs(2)).with_delay(Duration::from_millis(500));
assert_eq!(
animation_pass(&animation, Duration::from_millis(499)),
AnimationPass::Delayed
);
assert_eq!(
animation_pass(&animation, Duration::from_millis(500)),
AnimationPass::Running { cycle_delta: 0.0 }
);
assert_eq!(
animation_pass(&animation, Duration::from_millis(1500)),
AnimationPass::Running { cycle_delta: 0.5 }
);
assert_eq!(
animation_pass(&animation, Duration::from_millis(2600)),
AnimationPass::Finished
);
}
#[test]
fn test_animation_pass_forever_wraps() {
let animation = Animation::new(Duration::from_secs(1)).repeat();
assert_eq!(
animation_pass(&animation, Duration::from_millis(1500)),
AnimationPass::Running { cycle_delta: 0.5 }
);
assert_eq!(
animation_pass(&animation, Duration::from_secs(3)),
AnimationPass::Running { cycle_delta: 0.0 }
);
}
#[test]
fn test_animation_pass_finite_times() {
let animation = Animation::new(Duration::from_secs(1)).repeat_n(3);
assert_eq!(
animation_pass(&animation, Duration::from_millis(1500)),
AnimationPass::Running { cycle_delta: 0.5 }
);
assert_eq!(
animation_pass(&animation, Duration::from_millis(2500)),
AnimationPass::Running { cycle_delta: 0.5 }
);
assert_eq!(
animation_pass(&animation, Duration::from_secs(3)),
AnimationPass::Finished
);
assert_eq!(
animation_pass(&animation, Duration::from_millis(3100)),
AnimationPass::Finished
);
}
#[test]
fn test_animation_pass_times_zero_clamps_to_once() {
let animation = Animation::new(Duration::from_secs(1)).repeat_n(0);
assert_eq!(
animation_pass(&animation, Duration::from_millis(500)),
AnimationPass::Running { cycle_delta: 0.5 }
);
assert_eq!(
animation_pass(&animation, Duration::from_millis(1500)),
AnimationPass::Finished
);
}
#[test]
fn test_animation_pass_zero_duration_finishes_immediately() {
let animation = Animation::new(Duration::ZERO);
assert_eq!(
animation_pass(&animation, Duration::ZERO),
AnimationPass::Finished
);
assert_eq!(
animation_pass(&animation.repeat(), Duration::from_secs(1)),
AnimationPass::Finished
);
}
struct AnimationTestView {
rendered_deltas: Rc<RefCell<Vec<f32>>>,
max_fps: Option<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 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)).repeat();
if let Some(max_fps) = self.max_fps {
animation = animation.with_max_fps(max_fps);
}
div().size_full().child(div().with_animation(
"repeating-animation",
animation,
move |this, delta| {
rendered_deltas.borrow_mut().push(delta);
this
},
))
}
}
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.add_window({
let rendered_deltas = rendered_deltas.clone();
move |_, _| AnimationTestView {
rendered_deltas,
max_fps,
}
});
cx.run_until_parked();
(rendered_deltas, window)
}
fn redraw<V: Render>(window: &WindowHandle<V>, cx: &mut TestAppContext) {
window.update(cx, |_, _, cx| cx.notify()).unwrap();
cx.run_until_parked();
}
#[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]);
cx.run_until_parked();
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.1]);
cx.executor().advance_clock(Duration::from_millis(105));
cx.run_until_parked();
assert_deltas_approx_eq(&[0.0, 0.1, 0.2]);
}
#[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.add_window({
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));
redraw(&window, cx);
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));
redraw(&window, cx);
let value_after_retargeting = *rendered_values.borrow().last().unwrap();
assert!(value_after_retargeting > value_before_retargeting);
}
}