mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-27 05:44:08 +00:00
1039 lines
40 KiB
Rust
1039 lines
40 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
|
|
|
use super::*;
|
|
use crate::{
|
|
items::{AnimationDirection, PropertyAnimation},
|
|
lengths::LogicalLength,
|
|
};
|
|
#[cfg(not(feature = "std"))]
|
|
use num_traits::Float;
|
|
|
|
enum AnimationState {
|
|
Delaying,
|
|
Animating { current_iteration: u64 },
|
|
Done { iteration_count: u64 },
|
|
}
|
|
|
|
pub(super) struct PropertyValueAnimationData<T> {
|
|
from_value: T,
|
|
to_value: T,
|
|
details: PropertyAnimation,
|
|
start_time: crate::animations::Instant,
|
|
state: AnimationState,
|
|
}
|
|
|
|
impl<T: InterpolatedPropertyValue + Clone> PropertyValueAnimationData<T> {
|
|
pub fn new(from_value: T, to_value: T, details: PropertyAnimation) -> Self {
|
|
let start_time = crate::animations::current_tick();
|
|
|
|
Self { from_value, to_value, details, start_time, state: AnimationState::Delaying }
|
|
}
|
|
|
|
pub fn compute_interpolated_value(&mut self) -> (T, bool) {
|
|
let new_tick = crate::animations::current_tick();
|
|
let mut time_progress = new_tick.duration_since(self.start_time).as_millis() as u64;
|
|
let reversed = |iteration: u64| -> bool {
|
|
match self.details.direction {
|
|
AnimationDirection::Normal => false,
|
|
AnimationDirection::Reverse => true,
|
|
AnimationDirection::Alternate => iteration % 2 == 1,
|
|
AnimationDirection::AlternateReverse => iteration % 2 == 0,
|
|
}
|
|
};
|
|
|
|
match self.state {
|
|
AnimationState::Delaying => {
|
|
if self.details.delay <= 0 {
|
|
self.state = AnimationState::Animating { current_iteration: 0 };
|
|
return self.compute_interpolated_value();
|
|
}
|
|
|
|
let delay = self.details.delay as u64;
|
|
|
|
if time_progress < delay {
|
|
if reversed(0) {
|
|
(self.to_value.clone(), false)
|
|
} else {
|
|
(self.from_value.clone(), false)
|
|
}
|
|
} else {
|
|
self.start_time =
|
|
new_tick - core::time::Duration::from_millis(time_progress - delay);
|
|
|
|
// Decide on next state:
|
|
self.state = AnimationState::Animating { current_iteration: 0 };
|
|
self.compute_interpolated_value()
|
|
}
|
|
}
|
|
AnimationState::Animating { mut current_iteration } => {
|
|
if self.details.duration <= 0 || self.details.iteration_count == 0. {
|
|
self.state = AnimationState::Done { iteration_count: 0 };
|
|
return self.compute_interpolated_value();
|
|
}
|
|
|
|
let duration = self.details.duration as u64;
|
|
if time_progress >= duration {
|
|
// wrap around
|
|
current_iteration += time_progress / duration;
|
|
time_progress %= duration;
|
|
self.start_time = new_tick - core::time::Duration::from_millis(time_progress);
|
|
}
|
|
|
|
if (self.details.iteration_count < 0.)
|
|
|| (((current_iteration * duration) + time_progress) as f64)
|
|
< ((self.details.iteration_count as f64) * (duration as f64))
|
|
{
|
|
self.state = AnimationState::Animating { current_iteration };
|
|
|
|
let progress = {
|
|
let progress =
|
|
(time_progress as f32 / self.details.duration as f32).clamp(0., 1.);
|
|
if reversed(current_iteration) {
|
|
1. - progress
|
|
} else {
|
|
progress
|
|
}
|
|
};
|
|
let t = crate::animations::easing_curve(&self.details.easing, progress);
|
|
let val = self.from_value.interpolate(&self.to_value, t);
|
|
|
|
(val, false)
|
|
} else {
|
|
self.state =
|
|
AnimationState::Done { iteration_count: current_iteration.max(1) - 1 };
|
|
self.compute_interpolated_value()
|
|
}
|
|
}
|
|
AnimationState::Done { iteration_count } => {
|
|
if reversed(iteration_count) {
|
|
(self.from_value.clone(), true)
|
|
} else {
|
|
(self.to_value.clone(), true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn reset(&mut self) {
|
|
self.state = AnimationState::Delaying;
|
|
self.start_time = crate::animations::current_tick();
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
|
pub(super) enum AnimatedBindingState {
|
|
Animating,
|
|
NotAnimating,
|
|
ShouldStart,
|
|
}
|
|
|
|
pub(super) struct AnimatedBindingCallable<T, A> {
|
|
pub(super) original_binding: PropertyHandle,
|
|
pub(super) state: Cell<AnimatedBindingState>,
|
|
pub(super) animation_data: RefCell<PropertyValueAnimationData<T>>,
|
|
pub(super) compute_animation_details: A,
|
|
}
|
|
|
|
pub(super) type AnimationDetail = Option<(PropertyAnimation, crate::animations::Instant)>;
|
|
|
|
unsafe impl<T: InterpolatedPropertyValue + Clone, A: Fn() -> AnimationDetail> BindingCallable
|
|
for AnimatedBindingCallable<T, A>
|
|
{
|
|
unsafe fn evaluate(self: Pin<&Self>, value: *mut ()) -> BindingResult {
|
|
let original_binding = Pin::new_unchecked(&self.original_binding);
|
|
original_binding.register_as_dependency_to_current_binding(
|
|
#[cfg(slint_debug_property)]
|
|
"<AnimatedBindingCallable>",
|
|
);
|
|
match self.state.get() {
|
|
AnimatedBindingState::Animating => {
|
|
let (val, finished) = self.animation_data.borrow_mut().compute_interpolated_value();
|
|
*(value as *mut T) = val;
|
|
if finished {
|
|
self.state.set(AnimatedBindingState::NotAnimating)
|
|
} else {
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.set_has_active_animations());
|
|
}
|
|
}
|
|
AnimatedBindingState::NotAnimating => {
|
|
self.original_binding.update(value);
|
|
}
|
|
AnimatedBindingState::ShouldStart => {
|
|
let value = &mut *(value as *mut T);
|
|
self.state.set(AnimatedBindingState::Animating);
|
|
let mut animation_data = self.animation_data.borrow_mut();
|
|
// animation_data.details.iteration_count = 1.;
|
|
animation_data.from_value = value.clone();
|
|
self.original_binding.update((&mut animation_data.to_value) as *mut T as *mut ());
|
|
if let Some((details, start_time)) = (self.compute_animation_details)() {
|
|
animation_data.start_time = start_time;
|
|
animation_data.details = details;
|
|
}
|
|
let (val, finished) = animation_data.compute_interpolated_value();
|
|
*value = val;
|
|
if finished {
|
|
self.state.set(AnimatedBindingState::NotAnimating)
|
|
} else {
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.set_has_active_animations());
|
|
}
|
|
}
|
|
};
|
|
BindingResult::KeepBinding
|
|
}
|
|
fn mark_dirty(self: Pin<&Self>) {
|
|
if self.state.get() == AnimatedBindingState::ShouldStart {
|
|
return;
|
|
}
|
|
let original_dirty = self.original_binding.access(|b| b.unwrap().dirty.get());
|
|
if original_dirty {
|
|
self.state.set(AnimatedBindingState::ShouldStart);
|
|
self.animation_data.borrow_mut().reset();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// InterpolatedPropertyValue is a trait used to enable properties to be used with
|
|
/// animations that interpolate values. The basic requirement is the ability to apply
|
|
/// a progress that's typically between 0 and 1 to a range.
|
|
pub trait InterpolatedPropertyValue: PartialEq + Default + 'static {
|
|
/// Returns the interpolated value between self and target_value according to the
|
|
/// progress parameter t that's usually between 0 and 1. With certain animation
|
|
/// easing curves it may over- or undershoot though.
|
|
#[must_use]
|
|
fn interpolate(&self, target_value: &Self, t: f32) -> Self;
|
|
}
|
|
|
|
impl InterpolatedPropertyValue for f32 {
|
|
fn interpolate(&self, target_value: &Self, t: f32) -> Self {
|
|
self + t * (target_value - self)
|
|
}
|
|
}
|
|
|
|
impl InterpolatedPropertyValue for i32 {
|
|
fn interpolate(&self, target_value: &Self, t: f32) -> Self {
|
|
self + (t * (target_value - self) as f32).round() as i32
|
|
}
|
|
}
|
|
|
|
impl InterpolatedPropertyValue for i64 {
|
|
fn interpolate(&self, target_value: &Self, t: f32) -> Self {
|
|
self + (t * (target_value - self) as f32).round() as Self
|
|
}
|
|
}
|
|
|
|
impl InterpolatedPropertyValue for u8 {
|
|
fn interpolate(&self, target_value: &Self, t: f32) -> Self {
|
|
((*self as f32) + (t * ((*target_value as f32) - (*self as f32)))).round().clamp(0., 255.)
|
|
as u8
|
|
}
|
|
}
|
|
|
|
impl InterpolatedPropertyValue for LogicalLength {
|
|
fn interpolate(&self, target_value: &Self, t: f32) -> Self {
|
|
LogicalLength::new(self.get().interpolate(&target_value.get(), t))
|
|
}
|
|
}
|
|
|
|
impl<T: Clone + InterpolatedPropertyValue + 'static> Property<T> {
|
|
/// Change the value of this property, by animating (interpolating) from the current property's value
|
|
/// to the specified parameter value. The animation is done according to the parameters described by
|
|
/// the PropertyAnimation object.
|
|
///
|
|
/// If other properties have binding depending of this property, these properties will
|
|
/// be marked as dirty.
|
|
pub fn set_animated_value(&self, value: T, animation_data: PropertyAnimation) {
|
|
// FIXME if the current value is a dirty binding, we must run it, but we do not have the context
|
|
let d = RefCell::new(properties_animations::PropertyValueAnimationData::new(
|
|
self.get_internal(),
|
|
value,
|
|
animation_data,
|
|
));
|
|
// Safety: the BindingCallable will cast its argument to T
|
|
unsafe {
|
|
self.handle.set_binding(
|
|
move |val: *mut ()| {
|
|
let (value, finished) = d.borrow_mut().compute_interpolated_value();
|
|
*(val as *mut T) = value;
|
|
if finished {
|
|
BindingResult::RemoveBinding
|
|
} else {
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.set_has_active_animations());
|
|
BindingResult::KeepBinding
|
|
}
|
|
},
|
|
#[cfg(slint_debug_property)]
|
|
self.debug_name.borrow().as_str(),
|
|
);
|
|
}
|
|
self.handle.mark_dirty(
|
|
#[cfg(slint_debug_property)]
|
|
self.debug_name.borrow().as_str(),
|
|
);
|
|
}
|
|
|
|
/// Set a binding to this property.
|
|
///
|
|
pub fn set_animated_binding(
|
|
&self,
|
|
binding: impl Binding<T> + 'static,
|
|
animation_data: PropertyAnimation,
|
|
) {
|
|
let binding_callable = properties_animations::AnimatedBindingCallable::<T, _> {
|
|
original_binding: PropertyHandle {
|
|
handle: Cell::new(
|
|
(alloc_binding_holder(move |val: *mut ()| unsafe {
|
|
let val = &mut *(val as *mut T);
|
|
*(val as *mut T) = binding.evaluate(val);
|
|
BindingResult::KeepBinding
|
|
}) as usize)
|
|
| 0b10,
|
|
),
|
|
},
|
|
state: Cell::new(properties_animations::AnimatedBindingState::NotAnimating),
|
|
animation_data: RefCell::new(properties_animations::PropertyValueAnimationData::new(
|
|
T::default(),
|
|
T::default(),
|
|
animation_data,
|
|
)),
|
|
compute_animation_details: || -> properties_animations::AnimationDetail { None },
|
|
};
|
|
|
|
// Safety: the `AnimatedBindingCallable`'s type match the property type
|
|
unsafe {
|
|
self.handle.set_binding(
|
|
binding_callable,
|
|
#[cfg(slint_debug_property)]
|
|
self.debug_name.borrow().as_str(),
|
|
)
|
|
};
|
|
self.handle.mark_dirty(
|
|
#[cfg(slint_debug_property)]
|
|
self.debug_name.borrow().as_str(),
|
|
);
|
|
}
|
|
|
|
/// Set a binding to this property, providing a callback for the transition animation
|
|
///
|
|
pub fn set_animated_binding_for_transition(
|
|
&self,
|
|
binding: impl Binding<T> + 'static,
|
|
compute_animation_details: impl Fn() -> (PropertyAnimation, crate::animations::Instant)
|
|
+ 'static,
|
|
) {
|
|
let binding_callable = properties_animations::AnimatedBindingCallable::<T, _> {
|
|
original_binding: PropertyHandle {
|
|
handle: Cell::new(
|
|
(alloc_binding_holder(move |val: *mut ()| unsafe {
|
|
let val = &mut *(val as *mut T);
|
|
*(val as *mut T) = binding.evaluate(val);
|
|
BindingResult::KeepBinding
|
|
}) as usize)
|
|
| 0b10,
|
|
),
|
|
},
|
|
state: Cell::new(properties_animations::AnimatedBindingState::NotAnimating),
|
|
animation_data: RefCell::new(properties_animations::PropertyValueAnimationData::new(
|
|
T::default(),
|
|
T::default(),
|
|
PropertyAnimation::default(),
|
|
)),
|
|
compute_animation_details: move || Some(compute_animation_details()),
|
|
};
|
|
|
|
// Safety: the `AnimatedBindingCallable`'s type match the property type
|
|
unsafe {
|
|
self.handle.set_binding(
|
|
binding_callable,
|
|
#[cfg(slint_debug_property)]
|
|
self.debug_name.borrow().as_str(),
|
|
)
|
|
};
|
|
self.handle.mark_dirty(
|
|
#[cfg(slint_debug_property)]
|
|
self.debug_name.borrow().as_str(),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod animation_tests {
|
|
use super::*;
|
|
|
|
#[derive(Default)]
|
|
struct Component {
|
|
width: Property<i32>,
|
|
width_times_two: Property<i32>,
|
|
feed_property: Property<i32>, // used by binding to feed values into width
|
|
}
|
|
|
|
impl Component {
|
|
fn new_test_component() -> Rc<Self> {
|
|
let compo = Rc::new(Component::default());
|
|
let w = Rc::downgrade(&compo);
|
|
compo.width_times_two.set_binding(move || {
|
|
let compo = w.upgrade().unwrap();
|
|
get_prop_value(&compo.width) * 2
|
|
});
|
|
|
|
compo
|
|
}
|
|
}
|
|
|
|
const DURATION: std::time::Duration = std::time::Duration::from_millis(10000);
|
|
const DELAY: std::time::Duration = std::time::Duration::from_millis(800);
|
|
|
|
// Helper just for testing
|
|
fn get_prop_value<T: Clone>(prop: &Property<T>) -> T {
|
|
unsafe { Pin::new_unchecked(prop).get() }
|
|
}
|
|
|
|
#[test]
|
|
fn properties_test_animation_negative_delay_triggered_by_set() {
|
|
let compo = Component::new_test_component();
|
|
|
|
let animation_details = PropertyAnimation {
|
|
delay: -25,
|
|
duration: DURATION.as_millis() as _,
|
|
iteration_count: 1.,
|
|
..PropertyAnimation::default()
|
|
};
|
|
|
|
compo.width.set(100);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
let start_time = crate::animations::current_tick();
|
|
|
|
compo.width.set_animated_value(200, animation_details);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 150);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 300);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION));
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 400);
|
|
|
|
// Overshoot: Always to_value.
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 400);
|
|
|
|
// the binding should be removed
|
|
compo.width.handle.access(|binding| assert!(binding.is_none()));
|
|
}
|
|
|
|
#[test]
|
|
fn properties_test_animation_triggered_by_set() {
|
|
let compo = Component::new_test_component();
|
|
|
|
let animation_details = PropertyAnimation {
|
|
duration: DURATION.as_millis() as _,
|
|
iteration_count: 1.,
|
|
..PropertyAnimation::default()
|
|
};
|
|
|
|
compo.width.set(100);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
let start_time = crate::animations::current_tick();
|
|
|
|
compo.width.set_animated_value(200, animation_details);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 150);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 300);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION));
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 400);
|
|
|
|
// Overshoot: Always to_value.
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 400);
|
|
|
|
// the binding should be removed
|
|
compo.width.handle.access(|binding| assert!(binding.is_none()));
|
|
}
|
|
|
|
#[test]
|
|
fn properties_test_delayed_animation_triggered_by_set() {
|
|
let compo = Component::new_test_component();
|
|
|
|
let animation_details = PropertyAnimation {
|
|
delay: DELAY.as_millis() as _,
|
|
iteration_count: 1.,
|
|
duration: DURATION.as_millis() as _,
|
|
..PropertyAnimation::default()
|
|
};
|
|
|
|
compo.width.set(100);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
let start_time = crate::animations::current_tick();
|
|
|
|
compo.width.set_animated_value(200, animation_details);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
// In delay:
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
// In animation:
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY));
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY + DURATION / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 150);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 300);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY + DURATION));
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 400);
|
|
|
|
// Overshoot: Always to_value.
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 400);
|
|
|
|
// the binding should be removed
|
|
compo.width.handle.access(|binding| assert!(binding.is_none()));
|
|
}
|
|
|
|
#[test]
|
|
fn properties_test_delayed_animation_fractual_iteration_triggered_by_set() {
|
|
let compo = Component::new_test_component();
|
|
|
|
let animation_details = PropertyAnimation {
|
|
delay: DELAY.as_millis() as _,
|
|
iteration_count: 1.5,
|
|
duration: DURATION.as_millis() as _,
|
|
..PropertyAnimation::default()
|
|
};
|
|
|
|
compo.width.set(100);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
let start_time = crate::animations::current_tick();
|
|
|
|
compo.width.set_animated_value(200, animation_details);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
// In delay:
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
// In animation:
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY));
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY + DURATION / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 150);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 300);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY + DURATION));
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
// (fractual) end of animation
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 4));
|
|
assert_eq!(get_prop_value(&compo.width), 125);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 250);
|
|
|
|
// End of animation:
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 400);
|
|
|
|
// the binding should be removed
|
|
compo.width.handle.access(|binding| assert!(binding.is_none()));
|
|
}
|
|
#[test]
|
|
fn properties_test_delayed_animation_null_duration_triggered_by_set() {
|
|
let compo = Component::new_test_component();
|
|
|
|
let animation_details = PropertyAnimation {
|
|
delay: DELAY.as_millis() as _,
|
|
iteration_count: 1.0,
|
|
duration: 0,
|
|
..PropertyAnimation::default()
|
|
};
|
|
|
|
compo.width.set(100);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
let start_time = crate::animations::current_tick();
|
|
|
|
compo.width.set_animated_value(200, animation_details);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
// In delay:
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
// No animation:
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY));
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 400);
|
|
|
|
// Overshoot: Always to_value.
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 400);
|
|
|
|
// the binding should be removed
|
|
compo.width.handle.access(|binding| assert!(binding.is_none()));
|
|
}
|
|
|
|
#[test]
|
|
fn properties_test_delayed_animation_negative_duration_triggered_by_set() {
|
|
let compo = Component::new_test_component();
|
|
|
|
let animation_details = PropertyAnimation {
|
|
delay: DELAY.as_millis() as _,
|
|
iteration_count: 1.0,
|
|
duration: -25,
|
|
..PropertyAnimation::default()
|
|
};
|
|
|
|
compo.width.set(100);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
let start_time = crate::animations::current_tick();
|
|
|
|
compo.width.set_animated_value(200, animation_details);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
// In delay:
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
// No animation:
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY));
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 400);
|
|
|
|
// Overshoot: Always to_value.
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 400);
|
|
|
|
// the binding should be removed
|
|
compo.width.handle.access(|binding| assert!(binding.is_none()));
|
|
}
|
|
|
|
#[test]
|
|
fn properties_test_delayed_animation_no_iteration_triggered_by_set() {
|
|
let compo = Component::new_test_component();
|
|
|
|
let animation_details = PropertyAnimation {
|
|
delay: DELAY.as_millis() as _,
|
|
iteration_count: 0.0,
|
|
duration: DURATION.as_millis() as _,
|
|
..PropertyAnimation::default()
|
|
};
|
|
|
|
compo.width.set(100);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
let start_time = crate::animations::current_tick();
|
|
|
|
compo.width.set_animated_value(200, animation_details);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
// In delay:
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
// No animation:
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY));
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 400);
|
|
|
|
// Overshoot: Always to_value.
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 400);
|
|
|
|
// the binding should be removed
|
|
compo.width.handle.access(|binding| assert!(binding.is_none()));
|
|
}
|
|
|
|
#[test]
|
|
fn properties_test_delayed_animation_negative_iteration_triggered_by_set() {
|
|
let compo = Component::new_test_component();
|
|
|
|
let animation_details = PropertyAnimation {
|
|
delay: DELAY.as_millis() as _,
|
|
iteration_count: -42., // loop forever!
|
|
duration: DURATION.as_millis() as _,
|
|
..PropertyAnimation::default()
|
|
};
|
|
|
|
compo.width.set(100);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
let start_time = crate::animations::current_tick();
|
|
|
|
compo.width.set_animated_value(200, animation_details);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
// In delay:
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
// In animation:
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY));
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY + DURATION / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 150);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 300);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY + DURATION));
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
// In animation (again):
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY + 500 * DURATION));
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| {
|
|
driver.update_animations(start_time + DELAY + 50000 * DURATION + DURATION / 2)
|
|
});
|
|
assert_eq!(get_prop_value(&compo.width), 150);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 300);
|
|
|
|
// the binding should not be removed as it is still animating!
|
|
compo.width.handle.access(|binding| assert!(binding.is_some()));
|
|
}
|
|
|
|
#[test]
|
|
fn properties_test_animation_direction_triggered_by_set() {
|
|
let compo = Component::new_test_component();
|
|
|
|
let animation_details = PropertyAnimation {
|
|
delay: -25,
|
|
duration: DURATION.as_millis() as _,
|
|
direction: AnimationDirection::AlternateReverse,
|
|
iteration_count: 1.,
|
|
..PropertyAnimation::default()
|
|
};
|
|
|
|
compo.width.set(100);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
let start_time = crate::animations::current_tick();
|
|
|
|
compo.width.set_animated_value(200, animation_details);
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 400);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 150);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 300);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION));
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
// Overshoot: Always from_value.
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
// the binding should be removed
|
|
compo.width.handle.access(|binding| assert!(binding.is_none()));
|
|
}
|
|
|
|
#[test]
|
|
fn properties_test_animation_triggered_by_binding() {
|
|
let compo = Component::new_test_component();
|
|
|
|
let start_time = crate::animations::current_tick();
|
|
|
|
let animation_details = PropertyAnimation {
|
|
duration: DURATION.as_millis() as _,
|
|
iteration_count: 1.,
|
|
..PropertyAnimation::default()
|
|
};
|
|
|
|
let w = Rc::downgrade(&compo);
|
|
compo.width.set_animated_binding(
|
|
move || {
|
|
let compo = w.upgrade().unwrap();
|
|
get_prop_value(&compo.feed_property)
|
|
},
|
|
animation_details,
|
|
);
|
|
|
|
compo.feed_property.set(100);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
compo.feed_property.set(200);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 150);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 300);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION));
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 400);
|
|
}
|
|
|
|
#[test]
|
|
fn properties_test_delayed_animation_triggered_by_binding() {
|
|
let compo = Component::new_test_component();
|
|
|
|
let start_time = crate::animations::current_tick();
|
|
|
|
let animation_details = PropertyAnimation {
|
|
delay: DELAY.as_millis() as _,
|
|
duration: DURATION.as_millis() as _,
|
|
iteration_count: 1.0,
|
|
..PropertyAnimation::default()
|
|
};
|
|
|
|
let w = Rc::downgrade(&compo);
|
|
compo.width.set_animated_binding(
|
|
move || {
|
|
let compo = w.upgrade().unwrap();
|
|
get_prop_value(&compo.feed_property)
|
|
},
|
|
animation_details,
|
|
);
|
|
|
|
compo.feed_property.set(100);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
compo.feed_property.set(200);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
// In delay:
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
// In animation:
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY));
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 200);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY + DURATION / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 150);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 300);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY + DURATION));
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 400);
|
|
|
|
// Overshoot: Always to_value.
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
assert_eq!(get_prop_value(&compo.width_times_two), 400);
|
|
}
|
|
|
|
#[test]
|
|
fn test_loop() {
|
|
let compo = Component::new_test_component();
|
|
|
|
let animation_details = PropertyAnimation {
|
|
duration: DURATION.as_millis() as _,
|
|
iteration_count: 2.,
|
|
..PropertyAnimation::default()
|
|
};
|
|
|
|
compo.width.set(100);
|
|
|
|
let start_time = crate::animations::current_tick();
|
|
|
|
compo.width.set_animated_value(200, animation_details);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 150);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION));
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2));
|
|
assert_eq!(get_prop_value(&compo.width), 150);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION * 2));
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
|
|
// the binding should be removed
|
|
compo.width.handle.access(|binding| assert!(binding.is_none()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_loop_via_binding() {
|
|
// Loop twice, restart the animation and still loop twice.
|
|
|
|
let compo = Component::new_test_component();
|
|
|
|
let start_time = crate::animations::current_tick();
|
|
|
|
let animation_details = PropertyAnimation {
|
|
duration: DURATION.as_millis() as _,
|
|
iteration_count: 2.,
|
|
..PropertyAnimation::default()
|
|
};
|
|
|
|
let w = Rc::downgrade(&compo);
|
|
compo.width.set_animated_binding(
|
|
move || {
|
|
let compo = w.upgrade().unwrap();
|
|
get_prop_value(&compo.feed_property)
|
|
},
|
|
animation_details,
|
|
);
|
|
|
|
compo.feed_property.set(100);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
|
|
compo.feed_property.set(200);
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION / 2));
|
|
|
|
assert_eq!(get_prop_value(&compo.width), 150);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION));
|
|
|
|
assert_eq!(get_prop_value(&compo.width), 100);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2));
|
|
|
|
assert_eq!(get_prop_value(&compo.width), 150);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + 2 * DURATION));
|
|
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
|
|
// Overshoot a bit:
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + 2 * DURATION + DURATION / 2));
|
|
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
|
|
// Restart the animation by setting a new value.
|
|
|
|
let start_time = crate::animations::current_tick();
|
|
|
|
compo.feed_property.set(300);
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION / 2));
|
|
|
|
assert_eq!(get_prop_value(&compo.width), 250);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION));
|
|
|
|
assert_eq!(get_prop_value(&compo.width), 200);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2));
|
|
|
|
assert_eq!(get_prop_value(&compo.width), 250);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + 2 * DURATION));
|
|
|
|
assert_eq!(get_prop_value(&compo.width), 300);
|
|
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.update_animations(start_time + 2 * DURATION + DURATION / 2));
|
|
|
|
assert_eq!(get_prop_value(&compo.width), 300);
|
|
}
|
|
}
|