1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use super::*;
5use crate::{
6 items::{AnimationDirection, PropertyAnimation},
7 lengths::LogicalLength,
8};
9#[cfg(not(feature = "std"))]
10use num_traits::Float;
11
12enum AnimationState {
13 Delaying,
14 Animating { current_iteration: u64 },
15 Done { iteration_count: u64 },
16}
17
18pub(super) struct PropertyValueAnimationData<T> {
19 from_value: T,
20 to_value: T,
21 details: PropertyAnimation,
22 start_time: crate::animations::Instant,
23 state: AnimationState,
24}
25
26impl<T: InterpolatedPropertyValue + Clone> PropertyValueAnimationData<T> {
27 pub fn new(from_value: T, to_value: T, details: PropertyAnimation) -> Self {
28 let start_time = crate::animations::current_tick();
29
30 Self { from_value, to_value, details, start_time, state: AnimationState::Delaying }
31 }
32
33 pub fn compute_interpolated_value(&mut self) -> (T, bool) {
34 let new_tick = crate::animations::current_tick();
35 let mut time_progress = new_tick.duration_since(self.start_time).as_millis() as u64;
36 let reversed = |iteration: u64| -> bool {
37 match self.details.direction {
38 AnimationDirection::Normal => false,
39 AnimationDirection::Reverse => true,
40 AnimationDirection::Alternate => iteration % 2 == 1,
41 AnimationDirection::AlternateReverse => iteration % 2 == 0,
42 }
43 };
44
45 match self.state {
46 AnimationState::Delaying => {
47 if self.details.delay <= 0 {
48 self.state = AnimationState::Animating { current_iteration: 0 };
49 return self.compute_interpolated_value();
50 }
51
52 let delay = self.details.delay as u64;
53
54 if time_progress < delay {
55 if reversed(0) {
56 (self.to_value.clone(), false)
57 } else {
58 (self.from_value.clone(), false)
59 }
60 } else {
61 self.start_time =
62 new_tick - core::time::Duration::from_millis(time_progress - delay);
63
64 // Decide on next state:
65 self.state = AnimationState::Animating { current_iteration: 0 };
66 self.compute_interpolated_value()
67 }
68 }
69 AnimationState::Animating { mut current_iteration } => {
70 if self.details.duration <= 0 || self.details.iteration_count == 0. {
71 self.state = AnimationState::Done { iteration_count: 0 };
72 return self.compute_interpolated_value();
73 }
74
75 let duration = self.details.duration as u64;
76 if time_progress >= duration {
77 // wrap around
78 current_iteration += time_progress / duration;
79 time_progress %= duration;
80 self.start_time = new_tick - core::time::Duration::from_millis(time_progress);
81 }
82
83 if (self.details.iteration_count < 0.)
84 || (((current_iteration * duration) + time_progress) as f64)
85 < ((self.details.iteration_count as f64) * (duration as f64))
86 {
87 self.state = AnimationState::Animating { current_iteration };
88
89 let progress = {
90 let progress =
91 (time_progress as f32 / self.details.duration as f32).clamp(0., 1.);
92 if reversed(current_iteration) {
93 1. - progress
94 } else {
95 progress
96 }
97 };
98 let t = crate::animations::easing_curve(&self.details.easing, progress);
99 let val = self.from_value.interpolate(&self.to_value, t);
100
101 (val, false)
102 } else {
103 self.state =
104 AnimationState::Done { iteration_count: current_iteration.max(1) - 1 };
105 self.compute_interpolated_value()
106 }
107 }
108 AnimationState::Done { iteration_count } => {
109 if reversed(iteration_count) {
110 (self.from_value.clone(), true)
111 } else {
112 (self.to_value.clone(), true)
113 }
114 }
115 }
116 }
117
118 fn reset(&mut self) {
119 self.state = AnimationState::Delaying;
120 self.start_time = crate::animations::current_tick();
121 }
122}
123
124#[derive(Clone, Copy, Eq, PartialEq, Debug)]
125pub(super) enum AnimatedBindingState {
126 Animating,
127 NotAnimating,
128 ShouldStart,
129}
130
131pub(super) struct AnimatedBindingCallable<T, A> {
132 pub(super) original_binding: PropertyHandle,
133 pub(super) state: Cell<AnimatedBindingState>,
134 pub(super) animation_data: RefCell<PropertyValueAnimationData<T>>,
135 pub(super) compute_animation_details: A,
136}
137
138pub(super) type AnimationDetail = Option<(PropertyAnimation, crate::animations::Instant)>;
139
140unsafe impl<T: InterpolatedPropertyValue + Clone, A: Fn() -> AnimationDetail> BindingCallable
141 for AnimatedBindingCallable<T, A>
142{
143 unsafe fn evaluate(self: Pin<&Self>, value: *mut ()) -> BindingResult {
144 let original_binding = Pin::new_unchecked(&self.original_binding);
145 original_binding.register_as_dependency_to_current_binding(
146 #[cfg(slint_debug_property)]
147 "<AnimatedBindingCallable>",
148 );
149 match self.state.get() {
150 AnimatedBindingState::Animating => {
151 let (val, finished) = self.animation_data.borrow_mut().compute_interpolated_value();
152 *(value as *mut T) = val;
153 if finished {
154 self.state.set(AnimatedBindingState::NotAnimating)
155 } else {
156 crate::animations::CURRENT_ANIMATION_DRIVER
157 .with(|driver| driver.set_has_active_animations());
158 }
159 }
160 AnimatedBindingState::NotAnimating => {
161 self.original_binding.update(value);
162 }
163 AnimatedBindingState::ShouldStart => {
164 let value = &mut *(value as *mut T);
165 self.state.set(AnimatedBindingState::Animating);
166 let mut animation_data = self.animation_data.borrow_mut();
167 // animation_data.details.iteration_count = 1.;
168 animation_data.from_value = value.clone();
169 self.original_binding.update((&mut animation_data.to_value) as *mut T as *mut ());
170 if let Some((details, start_time)) = (self.compute_animation_details)() {
171 animation_data.start_time = start_time;
172 animation_data.details = details;
173 }
174 let (val, finished) = animation_data.compute_interpolated_value();
175 *value = val;
176 if finished {
177 self.state.set(AnimatedBindingState::NotAnimating)
178 } else {
179 crate::animations::CURRENT_ANIMATION_DRIVER
180 .with(|driver| driver.set_has_active_animations());
181 }
182 }
183 };
184 BindingResult::KeepBinding
185 }
186 fn mark_dirty(self: Pin<&Self>) {
187 if self.state.get() == AnimatedBindingState::ShouldStart {
188 return;
189 }
190 let original_dirty = self.original_binding.access(|b| b.unwrap().dirty.get());
191 if original_dirty {
192 self.state.set(AnimatedBindingState::ShouldStart);
193 self.animation_data.borrow_mut().reset();
194 }
195 }
196}
197
198/// InterpolatedPropertyValue is a trait used to enable properties to be used with
199/// animations that interpolate values. The basic requirement is the ability to apply
200/// a progress that's typically between 0 and 1 to a range.
201pub trait InterpolatedPropertyValue: PartialEq + Default + 'static {
202 /// Returns the interpolated value between self and target_value according to the
203 /// progress parameter t that's usually between 0 and 1. With certain animation
204 /// easing curves it may over- or undershoot though.
205 #[must_use]
206 fn interpolate(&self, target_value: &Self, t: f32) -> Self;
207}
208
209impl InterpolatedPropertyValue for f32 {
210 fn interpolate(&self, target_value: &Self, t: f32) -> Self {
211 self + t * (target_value - self)
212 }
213}
214
215impl InterpolatedPropertyValue for i32 {
216 fn interpolate(&self, target_value: &Self, t: f32) -> Self {
217 self + (t * (target_value - self) as f32).round() as i32
218 }
219}
220
221impl InterpolatedPropertyValue for i64 {
222 fn interpolate(&self, target_value: &Self, t: f32) -> Self {
223 self + (t * (target_value - self) as f32).round() as Self
224 }
225}
226
227impl InterpolatedPropertyValue for u8 {
228 fn interpolate(&self, target_value: &Self, t: f32) -> Self {
229 ((*self as f32) + (t * ((*target_value as f32) - (*self as f32)))).round().clamp(min:0., max:255.)
230 as u8
231 }
232}
233
234impl InterpolatedPropertyValue for LogicalLength {
235 fn interpolate(&self, target_value: &Self, t: f32) -> Self {
236 LogicalLength::new(self.get().interpolate(&target_value.get(), t))
237 }
238}
239
240impl<T: Clone + InterpolatedPropertyValue + 'static> Property<T> {
241 /// Change the value of this property, by animating (interpolating) from the current property's value
242 /// to the specified parameter value. The animation is done according to the parameters described by
243 /// the PropertyAnimation object.
244 ///
245 /// If other properties have binding depending of this property, these properties will
246 /// be marked as dirty.
247 pub fn set_animated_value(&self, value: T, animation_data: PropertyAnimation) {
248 // FIXME if the current value is a dirty binding, we must run it, but we do not have the context
249 let d = RefCell::new(properties_animations::PropertyValueAnimationData::new(
250 self.get_internal(),
251 value,
252 animation_data,
253 ));
254 // Safety: the BindingCallable will cast its argument to T
255 unsafe {
256 self.handle.set_binding(
257 move |val: *mut ()| {
258 let (value, finished) = d.borrow_mut().compute_interpolated_value();
259 *(val as *mut T) = value;
260 if finished {
261 BindingResult::RemoveBinding
262 } else {
263 crate::animations::CURRENT_ANIMATION_DRIVER
264 .with(|driver| driver.set_has_active_animations());
265 BindingResult::KeepBinding
266 }
267 },
268 #[cfg(slint_debug_property)]
269 self.debug_name.borrow().as_str(),
270 );
271 }
272 self.handle.mark_dirty(
273 #[cfg(slint_debug_property)]
274 self.debug_name.borrow().as_str(),
275 );
276 }
277
278 /// Set a binding to this property.
279 ///
280 pub fn set_animated_binding(
281 &self,
282 binding: impl Binding<T> + 'static,
283 animation_data: PropertyAnimation,
284 ) {
285 let binding_callable = properties_animations::AnimatedBindingCallable::<T, _> {
286 original_binding: PropertyHandle {
287 handle: Cell::new(
288 (alloc_binding_holder(move |val: *mut ()| unsafe {
289 let val = &mut *(val as *mut T);
290 *(val as *mut T) = binding.evaluate(val);
291 BindingResult::KeepBinding
292 }) as usize)
293 | 0b10,
294 ),
295 },
296 state: Cell::new(properties_animations::AnimatedBindingState::NotAnimating),
297 animation_data: RefCell::new(properties_animations::PropertyValueAnimationData::new(
298 T::default(),
299 T::default(),
300 animation_data,
301 )),
302 compute_animation_details: || -> properties_animations::AnimationDetail { None },
303 };
304
305 // Safety: the `AnimatedBindingCallable`'s type match the property type
306 unsafe {
307 self.handle.set_binding(
308 binding_callable,
309 #[cfg(slint_debug_property)]
310 self.debug_name.borrow().as_str(),
311 )
312 };
313 self.handle.mark_dirty(
314 #[cfg(slint_debug_property)]
315 self.debug_name.borrow().as_str(),
316 );
317 }
318
319 /// Set a binding to this property, providing a callback for the transition animation
320 ///
321 pub fn set_animated_binding_for_transition(
322 &self,
323 binding: impl Binding<T> + 'static,
324 compute_animation_details: impl Fn() -> (PropertyAnimation, crate::animations::Instant)
325 + 'static,
326 ) {
327 let binding_callable = properties_animations::AnimatedBindingCallable::<T, _> {
328 original_binding: PropertyHandle {
329 handle: Cell::new(
330 (alloc_binding_holder(move |val: *mut ()| unsafe {
331 let val = &mut *(val as *mut T);
332 *(val as *mut T) = binding.evaluate(val);
333 BindingResult::KeepBinding
334 }) as usize)
335 | 0b10,
336 ),
337 },
338 state: Cell::new(properties_animations::AnimatedBindingState::NotAnimating),
339 animation_data: RefCell::new(properties_animations::PropertyValueAnimationData::new(
340 T::default(),
341 T::default(),
342 PropertyAnimation::default(),
343 )),
344 compute_animation_details: move || Some(compute_animation_details()),
345 };
346
347 // Safety: the `AnimatedBindingCallable`'s type match the property type
348 unsafe {
349 self.handle.set_binding(
350 binding_callable,
351 #[cfg(slint_debug_property)]
352 self.debug_name.borrow().as_str(),
353 )
354 };
355 self.handle.mark_dirty(
356 #[cfg(slint_debug_property)]
357 self.debug_name.borrow().as_str(),
358 );
359 }
360}
361
362#[cfg(test)]
363mod animation_tests {
364 use super::*;
365
366 #[derive(Default)]
367 struct Component {
368 width: Property<i32>,
369 width_times_two: Property<i32>,
370 feed_property: Property<i32>, // used by binding to feed values into width
371 }
372
373 impl Component {
374 fn new_test_component() -> Rc<Self> {
375 let compo = Rc::new(Component::default());
376 let w = Rc::downgrade(&compo);
377 compo.width_times_two.set_binding(move || {
378 let compo = w.upgrade().unwrap();
379 get_prop_value(&compo.width) * 2
380 });
381
382 compo
383 }
384 }
385
386 const DURATION: std::time::Duration = std::time::Duration::from_millis(10000);
387 const DELAY: std::time::Duration = std::time::Duration::from_millis(800);
388
389 // Helper just for testing
390 fn get_prop_value<T: Clone>(prop: &Property<T>) -> T {
391 unsafe { Pin::new_unchecked(prop).get() }
392 }
393
394 #[test]
395 fn properties_test_animation_negative_delay_triggered_by_set() {
396 let compo = Component::new_test_component();
397
398 let animation_details = PropertyAnimation {
399 delay: -25,
400 duration: DURATION.as_millis() as _,
401 iteration_count: 1.,
402 ..PropertyAnimation::default()
403 };
404
405 compo.width.set(100);
406 assert_eq!(get_prop_value(&compo.width), 100);
407 assert_eq!(get_prop_value(&compo.width_times_two), 200);
408
409 let start_time = crate::animations::current_tick();
410
411 compo.width.set_animated_value(200, animation_details);
412 assert_eq!(get_prop_value(&compo.width), 100);
413 assert_eq!(get_prop_value(&compo.width_times_two), 200);
414
415 crate::animations::CURRENT_ANIMATION_DRIVER
416 .with(|driver| driver.update_animations(start_time + DURATION / 2));
417 assert_eq!(get_prop_value(&compo.width), 150);
418 assert_eq!(get_prop_value(&compo.width_times_two), 300);
419
420 crate::animations::CURRENT_ANIMATION_DRIVER
421 .with(|driver| driver.update_animations(start_time + DURATION));
422 assert_eq!(get_prop_value(&compo.width), 200);
423 assert_eq!(get_prop_value(&compo.width_times_two), 400);
424
425 // Overshoot: Always to_value.
426 crate::animations::CURRENT_ANIMATION_DRIVER
427 .with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2));
428 assert_eq!(get_prop_value(&compo.width), 200);
429 assert_eq!(get_prop_value(&compo.width_times_two), 400);
430
431 // the binding should be removed
432 compo.width.handle.access(|binding| assert!(binding.is_none()));
433 }
434
435 #[test]
436 fn properties_test_animation_triggered_by_set() {
437 let compo = Component::new_test_component();
438
439 let animation_details = PropertyAnimation {
440 duration: DURATION.as_millis() as _,
441 iteration_count: 1.,
442 ..PropertyAnimation::default()
443 };
444
445 compo.width.set(100);
446 assert_eq!(get_prop_value(&compo.width), 100);
447 assert_eq!(get_prop_value(&compo.width_times_two), 200);
448
449 let start_time = crate::animations::current_tick();
450
451 compo.width.set_animated_value(200, animation_details);
452 assert_eq!(get_prop_value(&compo.width), 100);
453 assert_eq!(get_prop_value(&compo.width_times_two), 200);
454
455 crate::animations::CURRENT_ANIMATION_DRIVER
456 .with(|driver| driver.update_animations(start_time + DURATION / 2));
457 assert_eq!(get_prop_value(&compo.width), 150);
458 assert_eq!(get_prop_value(&compo.width_times_two), 300);
459
460 crate::animations::CURRENT_ANIMATION_DRIVER
461 .with(|driver| driver.update_animations(start_time + DURATION));
462 assert_eq!(get_prop_value(&compo.width), 200);
463 assert_eq!(get_prop_value(&compo.width_times_two), 400);
464
465 // Overshoot: Always to_value.
466 crate::animations::CURRENT_ANIMATION_DRIVER
467 .with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2));
468 assert_eq!(get_prop_value(&compo.width), 200);
469 assert_eq!(get_prop_value(&compo.width_times_two), 400);
470
471 // the binding should be removed
472 compo.width.handle.access(|binding| assert!(binding.is_none()));
473 }
474
475 #[test]
476 fn properties_test_delayed_animation_triggered_by_set() {
477 let compo = Component::new_test_component();
478
479 let animation_details = PropertyAnimation {
480 delay: DELAY.as_millis() as _,
481 iteration_count: 1.,
482 duration: DURATION.as_millis() as _,
483 ..PropertyAnimation::default()
484 };
485
486 compo.width.set(100);
487 assert_eq!(get_prop_value(&compo.width), 100);
488 assert_eq!(get_prop_value(&compo.width_times_two), 200);
489
490 let start_time = crate::animations::current_tick();
491
492 compo.width.set_animated_value(200, animation_details);
493 assert_eq!(get_prop_value(&compo.width), 100);
494 assert_eq!(get_prop_value(&compo.width_times_two), 200);
495
496 // In delay:
497 crate::animations::CURRENT_ANIMATION_DRIVER
498 .with(|driver| driver.update_animations(start_time + DELAY / 2));
499 assert_eq!(get_prop_value(&compo.width), 100);
500 assert_eq!(get_prop_value(&compo.width_times_two), 200);
501
502 // In animation:
503 crate::animations::CURRENT_ANIMATION_DRIVER
504 .with(|driver| driver.update_animations(start_time + DELAY));
505 assert_eq!(get_prop_value(&compo.width), 100);
506 assert_eq!(get_prop_value(&compo.width_times_two), 200);
507
508 crate::animations::CURRENT_ANIMATION_DRIVER
509 .with(|driver| driver.update_animations(start_time + DELAY + DURATION / 2));
510 assert_eq!(get_prop_value(&compo.width), 150);
511 assert_eq!(get_prop_value(&compo.width_times_two), 300);
512
513 crate::animations::CURRENT_ANIMATION_DRIVER
514 .with(|driver| driver.update_animations(start_time + DELAY + DURATION));
515 assert_eq!(get_prop_value(&compo.width), 200);
516 assert_eq!(get_prop_value(&compo.width_times_two), 400);
517
518 // Overshoot: Always to_value.
519 crate::animations::CURRENT_ANIMATION_DRIVER
520 .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2));
521 assert_eq!(get_prop_value(&compo.width), 200);
522 assert_eq!(get_prop_value(&compo.width_times_two), 400);
523
524 // the binding should be removed
525 compo.width.handle.access(|binding| assert!(binding.is_none()));
526 }
527
528 #[test]
529 fn properties_test_delayed_animation_fractual_iteration_triggered_by_set() {
530 let compo = Component::new_test_component();
531
532 let animation_details = PropertyAnimation {
533 delay: DELAY.as_millis() as _,
534 iteration_count: 1.5,
535 duration: DURATION.as_millis() as _,
536 ..PropertyAnimation::default()
537 };
538
539 compo.width.set(100);
540 assert_eq!(get_prop_value(&compo.width), 100);
541 assert_eq!(get_prop_value(&compo.width_times_two), 200);
542
543 let start_time = crate::animations::current_tick();
544
545 compo.width.set_animated_value(200, animation_details);
546 assert_eq!(get_prop_value(&compo.width), 100);
547 assert_eq!(get_prop_value(&compo.width_times_two), 200);
548
549 // In delay:
550 crate::animations::CURRENT_ANIMATION_DRIVER
551 .with(|driver| driver.update_animations(start_time + DELAY / 2));
552 assert_eq!(get_prop_value(&compo.width), 100);
553 assert_eq!(get_prop_value(&compo.width_times_two), 200);
554
555 // In animation:
556 crate::animations::CURRENT_ANIMATION_DRIVER
557 .with(|driver| driver.update_animations(start_time + DELAY));
558 assert_eq!(get_prop_value(&compo.width), 100);
559 assert_eq!(get_prop_value(&compo.width_times_two), 200);
560
561 crate::animations::CURRENT_ANIMATION_DRIVER
562 .with(|driver| driver.update_animations(start_time + DELAY + DURATION / 2));
563 assert_eq!(get_prop_value(&compo.width), 150);
564 assert_eq!(get_prop_value(&compo.width_times_two), 300);
565
566 crate::animations::CURRENT_ANIMATION_DRIVER
567 .with(|driver| driver.update_animations(start_time + DELAY + DURATION));
568 assert_eq!(get_prop_value(&compo.width), 100);
569 assert_eq!(get_prop_value(&compo.width_times_two), 200);
570
571 // (fractual) end of animation
572 crate::animations::CURRENT_ANIMATION_DRIVER
573 .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 4));
574 assert_eq!(get_prop_value(&compo.width), 125);
575 assert_eq!(get_prop_value(&compo.width_times_two), 250);
576
577 // End of animation:
578 crate::animations::CURRENT_ANIMATION_DRIVER
579 .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2));
580 assert_eq!(get_prop_value(&compo.width), 200);
581 assert_eq!(get_prop_value(&compo.width_times_two), 400);
582
583 // the binding should be removed
584 compo.width.handle.access(|binding| assert!(binding.is_none()));
585 }
586 #[test]
587 fn properties_test_delayed_animation_null_duration_triggered_by_set() {
588 let compo = Component::new_test_component();
589
590 let animation_details = PropertyAnimation {
591 delay: DELAY.as_millis() as _,
592 iteration_count: 1.0,
593 duration: 0,
594 ..PropertyAnimation::default()
595 };
596
597 compo.width.set(100);
598 assert_eq!(get_prop_value(&compo.width), 100);
599 assert_eq!(get_prop_value(&compo.width_times_two), 200);
600
601 let start_time = crate::animations::current_tick();
602
603 compo.width.set_animated_value(200, animation_details);
604 assert_eq!(get_prop_value(&compo.width), 100);
605 assert_eq!(get_prop_value(&compo.width_times_two), 200);
606
607 // In delay:
608 crate::animations::CURRENT_ANIMATION_DRIVER
609 .with(|driver| driver.update_animations(start_time + DELAY / 2));
610 assert_eq!(get_prop_value(&compo.width), 100);
611 assert_eq!(get_prop_value(&compo.width_times_two), 200);
612
613 // No animation:
614 crate::animations::CURRENT_ANIMATION_DRIVER
615 .with(|driver| driver.update_animations(start_time + DELAY));
616 assert_eq!(get_prop_value(&compo.width), 200);
617 assert_eq!(get_prop_value(&compo.width_times_two), 400);
618
619 // Overshoot: Always to_value.
620 crate::animations::CURRENT_ANIMATION_DRIVER
621 .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2));
622 assert_eq!(get_prop_value(&compo.width), 200);
623 assert_eq!(get_prop_value(&compo.width_times_two), 400);
624
625 // the binding should be removed
626 compo.width.handle.access(|binding| assert!(binding.is_none()));
627 }
628
629 #[test]
630 fn properties_test_delayed_animation_negative_duration_triggered_by_set() {
631 let compo = Component::new_test_component();
632
633 let animation_details = PropertyAnimation {
634 delay: DELAY.as_millis() as _,
635 iteration_count: 1.0,
636 duration: -25,
637 ..PropertyAnimation::default()
638 };
639
640 compo.width.set(100);
641 assert_eq!(get_prop_value(&compo.width), 100);
642 assert_eq!(get_prop_value(&compo.width_times_two), 200);
643
644 let start_time = crate::animations::current_tick();
645
646 compo.width.set_animated_value(200, animation_details);
647 assert_eq!(get_prop_value(&compo.width), 100);
648 assert_eq!(get_prop_value(&compo.width_times_two), 200);
649
650 // In delay:
651 crate::animations::CURRENT_ANIMATION_DRIVER
652 .with(|driver| driver.update_animations(start_time + DELAY / 2));
653 assert_eq!(get_prop_value(&compo.width), 100);
654 assert_eq!(get_prop_value(&compo.width_times_two), 200);
655
656 // No animation:
657 crate::animations::CURRENT_ANIMATION_DRIVER
658 .with(|driver| driver.update_animations(start_time + DELAY));
659 assert_eq!(get_prop_value(&compo.width), 200);
660 assert_eq!(get_prop_value(&compo.width_times_two), 400);
661
662 // Overshoot: Always to_value.
663 crate::animations::CURRENT_ANIMATION_DRIVER
664 .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2));
665 assert_eq!(get_prop_value(&compo.width), 200);
666 assert_eq!(get_prop_value(&compo.width_times_two), 400);
667
668 // the binding should be removed
669 compo.width.handle.access(|binding| assert!(binding.is_none()));
670 }
671
672 #[test]
673 fn properties_test_delayed_animation_no_iteration_triggered_by_set() {
674 let compo = Component::new_test_component();
675
676 let animation_details = PropertyAnimation {
677 delay: DELAY.as_millis() as _,
678 iteration_count: 0.0,
679 duration: DURATION.as_millis() as _,
680 ..PropertyAnimation::default()
681 };
682
683 compo.width.set(100);
684 assert_eq!(get_prop_value(&compo.width), 100);
685 assert_eq!(get_prop_value(&compo.width_times_two), 200);
686
687 let start_time = crate::animations::current_tick();
688
689 compo.width.set_animated_value(200, animation_details);
690 assert_eq!(get_prop_value(&compo.width), 100);
691 assert_eq!(get_prop_value(&compo.width_times_two), 200);
692
693 // In delay:
694 crate::animations::CURRENT_ANIMATION_DRIVER
695 .with(|driver| driver.update_animations(start_time + DELAY / 2));
696 assert_eq!(get_prop_value(&compo.width), 100);
697 assert_eq!(get_prop_value(&compo.width_times_two), 200);
698
699 // No animation:
700 crate::animations::CURRENT_ANIMATION_DRIVER
701 .with(|driver| driver.update_animations(start_time + DELAY));
702 assert_eq!(get_prop_value(&compo.width), 200);
703 assert_eq!(get_prop_value(&compo.width_times_two), 400);
704
705 // Overshoot: Always to_value.
706 crate::animations::CURRENT_ANIMATION_DRIVER
707 .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2));
708 assert_eq!(get_prop_value(&compo.width), 200);
709 assert_eq!(get_prop_value(&compo.width_times_two), 400);
710
711 // the binding should be removed
712 compo.width.handle.access(|binding| assert!(binding.is_none()));
713 }
714
715 #[test]
716 fn properties_test_delayed_animation_negative_iteration_triggered_by_set() {
717 let compo = Component::new_test_component();
718
719 let animation_details = PropertyAnimation {
720 delay: DELAY.as_millis() as _,
721 iteration_count: -42., // loop forever!
722 duration: DURATION.as_millis() as _,
723 ..PropertyAnimation::default()
724 };
725
726 compo.width.set(100);
727 assert_eq!(get_prop_value(&compo.width), 100);
728 assert_eq!(get_prop_value(&compo.width_times_two), 200);
729
730 let start_time = crate::animations::current_tick();
731
732 compo.width.set_animated_value(200, animation_details);
733 assert_eq!(get_prop_value(&compo.width), 100);
734 assert_eq!(get_prop_value(&compo.width_times_two), 200);
735
736 // In delay:
737 crate::animations::CURRENT_ANIMATION_DRIVER
738 .with(|driver| driver.update_animations(start_time + DELAY / 2));
739 assert_eq!(get_prop_value(&compo.width), 100);
740 assert_eq!(get_prop_value(&compo.width_times_two), 200);
741
742 // In animation:
743 crate::animations::CURRENT_ANIMATION_DRIVER
744 .with(|driver| driver.update_animations(start_time + DELAY));
745 assert_eq!(get_prop_value(&compo.width), 100);
746 assert_eq!(get_prop_value(&compo.width_times_two), 200);
747
748 crate::animations::CURRENT_ANIMATION_DRIVER
749 .with(|driver| driver.update_animations(start_time + DELAY + DURATION / 2));
750 assert_eq!(get_prop_value(&compo.width), 150);
751 assert_eq!(get_prop_value(&compo.width_times_two), 300);
752
753 crate::animations::CURRENT_ANIMATION_DRIVER
754 .with(|driver| driver.update_animations(start_time + DELAY + DURATION));
755 assert_eq!(get_prop_value(&compo.width), 100);
756 assert_eq!(get_prop_value(&compo.width_times_two), 200);
757
758 // In animation (again):
759 crate::animations::CURRENT_ANIMATION_DRIVER
760 .with(|driver| driver.update_animations(start_time + DELAY + 500 * DURATION));
761 assert_eq!(get_prop_value(&compo.width), 100);
762 assert_eq!(get_prop_value(&compo.width_times_two), 200);
763
764 crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| {
765 driver.update_animations(start_time + DELAY + 50000 * DURATION + DURATION / 2)
766 });
767 assert_eq!(get_prop_value(&compo.width), 150);
768 assert_eq!(get_prop_value(&compo.width_times_two), 300);
769
770 // the binding should not be removed as it is still animating!
771 compo.width.handle.access(|binding| assert!(binding.is_some()));
772 }
773
774 #[test]
775 fn properties_test_animation_direction_triggered_by_set() {
776 let compo = Component::new_test_component();
777
778 let animation_details = PropertyAnimation {
779 delay: -25,
780 duration: DURATION.as_millis() as _,
781 direction: AnimationDirection::AlternateReverse,
782 iteration_count: 1.,
783 ..PropertyAnimation::default()
784 };
785
786 compo.width.set(100);
787 assert_eq!(get_prop_value(&compo.width), 100);
788 assert_eq!(get_prop_value(&compo.width_times_two), 200);
789
790 let start_time = crate::animations::current_tick();
791
792 compo.width.set_animated_value(200, animation_details);
793 assert_eq!(get_prop_value(&compo.width), 200);
794 assert_eq!(get_prop_value(&compo.width_times_two), 400);
795
796 crate::animations::CURRENT_ANIMATION_DRIVER
797 .with(|driver| driver.update_animations(start_time + DURATION / 2));
798 assert_eq!(get_prop_value(&compo.width), 150);
799 assert_eq!(get_prop_value(&compo.width_times_two), 300);
800
801 crate::animations::CURRENT_ANIMATION_DRIVER
802 .with(|driver| driver.update_animations(start_time + DURATION));
803 assert_eq!(get_prop_value(&compo.width), 100);
804 assert_eq!(get_prop_value(&compo.width_times_two), 200);
805
806 // Overshoot: Always from_value.
807 crate::animations::CURRENT_ANIMATION_DRIVER
808 .with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2));
809 assert_eq!(get_prop_value(&compo.width), 100);
810 assert_eq!(get_prop_value(&compo.width_times_two), 200);
811
812 // the binding should be removed
813 compo.width.handle.access(|binding| assert!(binding.is_none()));
814 }
815
816 #[test]
817 fn properties_test_animation_triggered_by_binding() {
818 let compo = Component::new_test_component();
819
820 let start_time = crate::animations::current_tick();
821
822 let animation_details = PropertyAnimation {
823 duration: DURATION.as_millis() as _,
824 iteration_count: 1.,
825 ..PropertyAnimation::default()
826 };
827
828 let w = Rc::downgrade(&compo);
829 compo.width.set_animated_binding(
830 move || {
831 let compo = w.upgrade().unwrap();
832 get_prop_value(&compo.feed_property)
833 },
834 animation_details,
835 );
836
837 compo.feed_property.set(100);
838 assert_eq!(get_prop_value(&compo.width), 100);
839 assert_eq!(get_prop_value(&compo.width_times_two), 200);
840
841 compo.feed_property.set(200);
842 assert_eq!(get_prop_value(&compo.width), 100);
843 assert_eq!(get_prop_value(&compo.width_times_two), 200);
844
845 crate::animations::CURRENT_ANIMATION_DRIVER
846 .with(|driver| driver.update_animations(start_time + DURATION / 2));
847 assert_eq!(get_prop_value(&compo.width), 150);
848 assert_eq!(get_prop_value(&compo.width_times_two), 300);
849
850 crate::animations::CURRENT_ANIMATION_DRIVER
851 .with(|driver| driver.update_animations(start_time + DURATION));
852 assert_eq!(get_prop_value(&compo.width), 200);
853 assert_eq!(get_prop_value(&compo.width_times_two), 400);
854 }
855
856 #[test]
857 fn properties_test_delayed_animation_triggered_by_binding() {
858 let compo = Component::new_test_component();
859
860 let start_time = crate::animations::current_tick();
861
862 let animation_details = PropertyAnimation {
863 delay: DELAY.as_millis() as _,
864 duration: DURATION.as_millis() as _,
865 iteration_count: 1.0,
866 ..PropertyAnimation::default()
867 };
868
869 let w = Rc::downgrade(&compo);
870 compo.width.set_animated_binding(
871 move || {
872 let compo = w.upgrade().unwrap();
873 get_prop_value(&compo.feed_property)
874 },
875 animation_details,
876 );
877
878 compo.feed_property.set(100);
879 assert_eq!(get_prop_value(&compo.width), 100);
880 assert_eq!(get_prop_value(&compo.width_times_two), 200);
881
882 compo.feed_property.set(200);
883 assert_eq!(get_prop_value(&compo.width), 100);
884 assert_eq!(get_prop_value(&compo.width_times_two), 200);
885
886 // In delay:
887 crate::animations::CURRENT_ANIMATION_DRIVER
888 .with(|driver| driver.update_animations(start_time + DELAY / 2));
889 assert_eq!(get_prop_value(&compo.width), 100);
890 assert_eq!(get_prop_value(&compo.width_times_two), 200);
891
892 // In animation:
893 crate::animations::CURRENT_ANIMATION_DRIVER
894 .with(|driver| driver.update_animations(start_time + DELAY));
895 assert_eq!(get_prop_value(&compo.width), 100);
896 assert_eq!(get_prop_value(&compo.width_times_two), 200);
897
898 crate::animations::CURRENT_ANIMATION_DRIVER
899 .with(|driver| driver.update_animations(start_time + DELAY + DURATION / 2));
900 assert_eq!(get_prop_value(&compo.width), 150);
901 assert_eq!(get_prop_value(&compo.width_times_two), 300);
902
903 crate::animations::CURRENT_ANIMATION_DRIVER
904 .with(|driver| driver.update_animations(start_time + DELAY + DURATION));
905 assert_eq!(get_prop_value(&compo.width), 200);
906 assert_eq!(get_prop_value(&compo.width_times_two), 400);
907
908 // Overshoot: Always to_value.
909 crate::animations::CURRENT_ANIMATION_DRIVER
910 .with(|driver| driver.update_animations(start_time + DELAY + DURATION + DURATION / 2));
911 assert_eq!(get_prop_value(&compo.width), 200);
912 assert_eq!(get_prop_value(&compo.width_times_two), 400);
913 }
914
915 #[test]
916 fn test_loop() {
917 let compo = Component::new_test_component();
918
919 let animation_details = PropertyAnimation {
920 duration: DURATION.as_millis() as _,
921 iteration_count: 2.,
922 ..PropertyAnimation::default()
923 };
924
925 compo.width.set(100);
926
927 let start_time = crate::animations::current_tick();
928
929 compo.width.set_animated_value(200, animation_details);
930 assert_eq!(get_prop_value(&compo.width), 100);
931
932 crate::animations::CURRENT_ANIMATION_DRIVER
933 .with(|driver| driver.update_animations(start_time + DURATION / 2));
934 assert_eq!(get_prop_value(&compo.width), 150);
935
936 crate::animations::CURRENT_ANIMATION_DRIVER
937 .with(|driver| driver.update_animations(start_time + DURATION));
938 assert_eq!(get_prop_value(&compo.width), 100);
939
940 crate::animations::CURRENT_ANIMATION_DRIVER
941 .with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2));
942 assert_eq!(get_prop_value(&compo.width), 150);
943
944 crate::animations::CURRENT_ANIMATION_DRIVER
945 .with(|driver| driver.update_animations(start_time + DURATION * 2));
946 assert_eq!(get_prop_value(&compo.width), 200);
947
948 // the binding should be removed
949 compo.width.handle.access(|binding| assert!(binding.is_none()));
950 }
951
952 #[test]
953 fn test_loop_via_binding() {
954 // Loop twice, restart the animation and still loop twice.
955
956 let compo = Component::new_test_component();
957
958 let start_time = crate::animations::current_tick();
959
960 let animation_details = PropertyAnimation {
961 duration: DURATION.as_millis() as _,
962 iteration_count: 2.,
963 ..PropertyAnimation::default()
964 };
965
966 let w = Rc::downgrade(&compo);
967 compo.width.set_animated_binding(
968 move || {
969 let compo = w.upgrade().unwrap();
970 get_prop_value(&compo.feed_property)
971 },
972 animation_details,
973 );
974
975 compo.feed_property.set(100);
976 assert_eq!(get_prop_value(&compo.width), 100);
977
978 compo.feed_property.set(200);
979 assert_eq!(get_prop_value(&compo.width), 100);
980
981 crate::animations::CURRENT_ANIMATION_DRIVER
982 .with(|driver| driver.update_animations(start_time + DURATION / 2));
983
984 assert_eq!(get_prop_value(&compo.width), 150);
985
986 crate::animations::CURRENT_ANIMATION_DRIVER
987 .with(|driver| driver.update_animations(start_time + DURATION));
988
989 assert_eq!(get_prop_value(&compo.width), 100);
990
991 crate::animations::CURRENT_ANIMATION_DRIVER
992 .with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2));
993
994 assert_eq!(get_prop_value(&compo.width), 150);
995
996 crate::animations::CURRENT_ANIMATION_DRIVER
997 .with(|driver| driver.update_animations(start_time + 2 * DURATION));
998
999 assert_eq!(get_prop_value(&compo.width), 200);
1000
1001 // Overshoot a bit:
1002 crate::animations::CURRENT_ANIMATION_DRIVER
1003 .with(|driver| driver.update_animations(start_time + 2 * DURATION + DURATION / 2));
1004
1005 assert_eq!(get_prop_value(&compo.width), 200);
1006
1007 // Restart the animation by setting a new value.
1008
1009 let start_time = crate::animations::current_tick();
1010
1011 compo.feed_property.set(300);
1012 assert_eq!(get_prop_value(&compo.width), 200);
1013
1014 crate::animations::CURRENT_ANIMATION_DRIVER
1015 .with(|driver| driver.update_animations(start_time + DURATION / 2));
1016
1017 assert_eq!(get_prop_value(&compo.width), 250);
1018
1019 crate::animations::CURRENT_ANIMATION_DRIVER
1020 .with(|driver| driver.update_animations(start_time + DURATION));
1021
1022 assert_eq!(get_prop_value(&compo.width), 200);
1023
1024 crate::animations::CURRENT_ANIMATION_DRIVER
1025 .with(|driver| driver.update_animations(start_time + DURATION + DURATION / 2));
1026
1027 assert_eq!(get_prop_value(&compo.width), 250);
1028
1029 crate::animations::CURRENT_ANIMATION_DRIVER
1030 .with(|driver| driver.update_animations(start_time + 2 * DURATION));
1031
1032 assert_eq!(get_prop_value(&compo.width), 300);
1033
1034 crate::animations::CURRENT_ANIMATION_DRIVER
1035 .with(|driver| driver.update_animations(start_time + 2 * DURATION + DURATION / 2));
1036
1037 assert_eq!(get_prop_value(&compo.width), 300);
1038 }
1039}
1040