mirror of
https://github.com/slint-ui/slint.git
synced 2025-09-28 21:04:47 +00:00
480 lines
17 KiB
Rust
480 lines
17 KiB
Rust
#![warn(missing_docs)]
|
|
|
|
use std::cell::{Cell, RefCell};
|
|
use std::rc::{Rc, Weak};
|
|
use std::time::{Duration, Instant};
|
|
|
|
/// The AnimationState describes the state reported to the Animated entity when it changes.
|
|
#[derive(Clone, Copy, PartialEq, Debug)]
|
|
pub enum AnimationState {
|
|
/// The animation has started.
|
|
Started,
|
|
/// The animation is in progress.
|
|
Running {
|
|
/// The progress of the running animation in the range between 0 and 1.
|
|
progress: f32,
|
|
},
|
|
/// The animation has stopped.
|
|
Stopped,
|
|
}
|
|
|
|
/// Animated is a trait representing an abstract entity that can be animated over a period of time.
|
|
pub trait Animated {
|
|
/// Called by the animation system to notify the animation about the progress, represented by
|
|
/// a value between 0 (start) and 1 (end).
|
|
fn update_animation_state(self: Rc<Self>, state: AnimationState);
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
/// The Animation structure holds everything needed to describe an animation.
|
|
pub struct Animation {
|
|
/// The duration of the animation.
|
|
pub duration: Duration,
|
|
/// The current state of the animation.
|
|
state: InternalAnimationState,
|
|
advance_callback: Weak<dyn Animated>,
|
|
}
|
|
|
|
impl Animation {
|
|
/// Creates a new animation with the specified duration and the advance_callback that'll be called
|
|
/// as time passes.
|
|
pub fn new(duration: Duration, advance_callback: Weak<dyn Animated>) -> Self {
|
|
Self { duration, state: InternalAnimationState::Stopped, advance_callback }
|
|
}
|
|
}
|
|
|
|
enum InternalAnimationEntry {
|
|
Allocated(RefCell<Animation>),
|
|
Free { next_free_idx: Option<usize> },
|
|
}
|
|
|
|
impl InternalAnimationEntry {
|
|
fn as_animation<'a>(&'a self) -> &'a RefCell<Animation> {
|
|
match self {
|
|
InternalAnimationEntry::Allocated(ref anim) => {
|
|
return anim;
|
|
}
|
|
InternalAnimationEntry::Free { .. } => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
/// The InternalAnimationState describes the three states that an existing animation can be in.
|
|
enum InternalAnimationState {
|
|
/// The animation has been scheduled and is ready to transition to running state.
|
|
ReadyToRun {
|
|
/// This is the time that has elapsed since the animation was paused, or 0 if this is
|
|
/// a new animation.
|
|
time_elapsed: Duration,
|
|
},
|
|
/// The animation is currently running.
|
|
Running {
|
|
/// start_time is the tick when the animation was actually started.
|
|
start_time: Instant,
|
|
},
|
|
ReadyToPause {
|
|
/// start_time is the tick when the animation was actually started.
|
|
start_time: Instant,
|
|
},
|
|
/// The animation is paused. It was previously running.
|
|
Paused {
|
|
/// time_elapsed_until_pause is the time between when the animation started and when it was paused.
|
|
/// This is used to calculate the new start_time when resuming.
|
|
time_elapsed_until_pause: Duration,
|
|
},
|
|
/// The animation is stopped. From here must be activately restarted.
|
|
Stopped,
|
|
}
|
|
|
|
/// The AnimationDriver
|
|
#[derive(Default)]
|
|
pub struct AnimationDriver {
|
|
animations: Vec<InternalAnimationEntry>,
|
|
next_free: Option<usize>,
|
|
len: usize,
|
|
/// Indicate whether there are any active animations that require a future call to update_animations.
|
|
active_animations: Cell<bool>,
|
|
}
|
|
|
|
/// The AnimationHandle can be used to refer to an animation after it's been started, in order to
|
|
/// pause or stop it, for example.
|
|
#[derive(Copy, Clone, Debug)]
|
|
pub struct AnimationHandle(usize);
|
|
|
|
impl AnimationDriver {
|
|
/// Iterates through all animations based on the new time tick and updates their state. This should be called by
|
|
/// the windowing system driver for every frame.
|
|
pub fn update_animations(&self, new_tick: Instant) {
|
|
self.active_animations.set(false);
|
|
let mut need_new_animation_frame = false;
|
|
let mut i: usize = 0;
|
|
while i < self.animations.len() {
|
|
{
|
|
let animation = &self.animations[i].as_animation().borrow().clone();
|
|
match animation.state {
|
|
InternalAnimationState::ReadyToRun { time_elapsed } => {
|
|
self.set_animation_state(
|
|
i,
|
|
InternalAnimationState::Running { start_time: new_tick - time_elapsed },
|
|
);
|
|
if time_elapsed == Duration::default() {
|
|
if let Some(cb) = animation.advance_callback.upgrade() {
|
|
cb.update_animation_state(AnimationState::Started);
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
};
|
|
}
|
|
|
|
let animation = &self.animations[i].as_animation().borrow().clone();
|
|
match animation.state {
|
|
InternalAnimationState::ReadyToRun { .. } => unreachable!(),
|
|
InternalAnimationState::Running { start_time } => {
|
|
let time_progress = new_tick.duration_since(start_time).as_millis() as f32;
|
|
let progress = time_progress / (animation.duration.as_millis() as f32);
|
|
if let Some(cb) = animation.advance_callback.upgrade() {
|
|
cb.update_animation_state(AnimationState::Running {
|
|
progress: progress.min(1.),
|
|
});
|
|
}
|
|
|
|
if progress >= 1. {
|
|
if let Some(cb) = animation.advance_callback.upgrade() {
|
|
cb.update_animation_state(AnimationState::Stopped);
|
|
}
|
|
|
|
self.set_animation_state(i, InternalAnimationState::Stopped);
|
|
} else {
|
|
need_new_animation_frame = true;
|
|
}
|
|
}
|
|
InternalAnimationState::ReadyToPause { start_time } => {
|
|
self.set_animation_state(
|
|
i,
|
|
InternalAnimationState::Paused {
|
|
time_elapsed_until_pause: new_tick - start_time,
|
|
},
|
|
);
|
|
}
|
|
InternalAnimationState::Paused { .. } => {}
|
|
InternalAnimationState::Stopped => {}
|
|
};
|
|
i += 1;
|
|
}
|
|
|
|
self.active_animations.set(self.active_animations.get() | need_new_animation_frame);
|
|
}
|
|
|
|
/// Returns true if there are any active or ready animations. This is used by the windowing system to determine
|
|
/// if a new animation frame is required or not. Returns false otherwise.
|
|
pub fn has_active_animations(&self) -> bool {
|
|
self.active_animations.get()
|
|
}
|
|
|
|
/// Start a new animation and returns a handle for it.
|
|
pub fn start_animation(&mut self, mut new_animation: Animation) -> AnimationHandle {
|
|
new_animation.state =
|
|
InternalAnimationState::ReadyToRun { time_elapsed: Duration::default() };
|
|
|
|
let idx = {
|
|
if let Some(free_idx) = self.next_free {
|
|
let entry = &mut self.animations[free_idx];
|
|
if let InternalAnimationEntry::Free { next_free_idx } = entry {
|
|
self.next_free = *next_free_idx;
|
|
} else {
|
|
unreachable!();
|
|
}
|
|
*entry = InternalAnimationEntry::Allocated(RefCell::new(new_animation));
|
|
free_idx
|
|
} else {
|
|
self.animations
|
|
.push(InternalAnimationEntry::Allocated(RefCell::new(new_animation)));
|
|
self.animations.len() - 1
|
|
}
|
|
};
|
|
self.active_animations.set(true);
|
|
self.len = self.len + 1;
|
|
AnimationHandle(idx)
|
|
}
|
|
|
|
/// Pauses the animation specified by the handle. The animation will not receive any further state updates.
|
|
pub fn pause_animation(&self, handle: AnimationHandle) {
|
|
let animation = self.animations[handle.0].as_animation();
|
|
let state = animation.borrow().state;
|
|
match state {
|
|
InternalAnimationState::ReadyToRun { .. } => {}
|
|
InternalAnimationState::Running { start_time } => {
|
|
animation.borrow_mut().state = InternalAnimationState::ReadyToPause { start_time }
|
|
}
|
|
InternalAnimationState::Paused { .. } => {}
|
|
InternalAnimationState::ReadyToPause { .. } => {}
|
|
InternalAnimationState::Stopped => {}
|
|
}
|
|
}
|
|
|
|
/// Resumes the animation specified by the handle. The animation will continue from its last progress.
|
|
pub fn resume_animation(&self, handle: AnimationHandle) {
|
|
let animation = self.animations[handle.0].as_animation();
|
|
let state = animation.borrow().state;
|
|
match state {
|
|
InternalAnimationState::ReadyToRun { .. } => {}
|
|
InternalAnimationState::Running { .. } => {}
|
|
InternalAnimationState::Paused { time_elapsed_until_pause } => {
|
|
animation.borrow_mut().state =
|
|
InternalAnimationState::ReadyToRun { time_elapsed: time_elapsed_until_pause }
|
|
}
|
|
InternalAnimationState::ReadyToPause { .. } => {}
|
|
InternalAnimationState::Stopped => {}
|
|
}
|
|
self.active_animations.set(true);
|
|
}
|
|
|
|
/// Returns a temporary reference to the animation behind the given handle.
|
|
pub fn get_animation<'a>(&'a self, handle: AnimationHandle) -> &'a RefCell<Animation> {
|
|
match self.animations[handle.0] {
|
|
InternalAnimationEntry::Allocated(ref anim) => {
|
|
return anim;
|
|
}
|
|
_ => unreachable!(),
|
|
};
|
|
}
|
|
|
|
/// Marks the animation specified by the handle as free/unused.
|
|
pub fn free_animation(&mut self, handle: AnimationHandle) {
|
|
self.animations[handle.0] = InternalAnimationEntry::Free { next_free_idx: self.next_free };
|
|
self.next_free = Some(handle.0);
|
|
self.len = self.len - 1;
|
|
}
|
|
|
|
fn set_animation_state(&self, index: usize, new_state: InternalAnimationState) {
|
|
let anim = self.animations[index].as_animation();
|
|
anim.borrow_mut().state = new_state;
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
use std::cell::RefCell;
|
|
use std::rc::Rc;
|
|
|
|
#[derive(Default)]
|
|
struct RecordingAnimation {
|
|
reported_states: Vec<AnimationState>,
|
|
}
|
|
|
|
impl Animated for RefCell<RecordingAnimation> {
|
|
fn update_animation_state(self: Rc<Self>, state: AnimationState) {
|
|
self.borrow_mut().reported_states.push(state)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_animation_driver() {
|
|
let mut driver = AnimationDriver::default();
|
|
|
|
let test_animation = Rc::new(RefCell::new(RecordingAnimation::default()));
|
|
|
|
assert!(!driver.has_active_animations());
|
|
|
|
let handle = driver.start_animation(Animation::new(
|
|
Duration::from_secs(10),
|
|
Rc::downgrade(&(test_animation.clone() as Rc<dyn Animated>)),
|
|
));
|
|
|
|
assert!(
|
|
matches!(driver.get_animation(handle).borrow().state, InternalAnimationState::ReadyToRun{..})
|
|
);
|
|
|
|
assert!(driver.has_active_animations());
|
|
|
|
assert_eq!(test_animation.borrow().reported_states.len(), 0);
|
|
|
|
let mut test_time = std::time::Instant::now();
|
|
|
|
driver.update_animations(test_time);
|
|
assert!(
|
|
matches!(driver.get_animation(handle).borrow().state, InternalAnimationState::Running{..})
|
|
);
|
|
|
|
assert_eq!(
|
|
test_animation.borrow().reported_states,
|
|
vec![AnimationState::Started, AnimationState::Running { progress: 0.0f32 }]
|
|
);
|
|
|
|
test_time += Duration::from_secs(5);
|
|
driver.update_animations(test_time);
|
|
|
|
assert_eq!(
|
|
test_animation.borrow().reported_states,
|
|
vec![
|
|
AnimationState::Started,
|
|
AnimationState::Running { progress: 0.0f32 },
|
|
AnimationState::Running { progress: 0.5f32 },
|
|
]
|
|
);
|
|
|
|
test_time += Duration::from_secs(5);
|
|
driver.update_animations(test_time);
|
|
|
|
assert_eq!(
|
|
test_animation.borrow().reported_states,
|
|
vec![
|
|
AnimationState::Started,
|
|
AnimationState::Running { progress: 0.0f32 },
|
|
AnimationState::Running { progress: 0.5f32 },
|
|
AnimationState::Running { progress: 1.0f32 },
|
|
AnimationState::Stopped,
|
|
]
|
|
);
|
|
|
|
test_time += Duration::from_secs(5);
|
|
driver.update_animations(test_time);
|
|
|
|
assert_eq!(
|
|
test_animation.borrow().reported_states,
|
|
vec![
|
|
AnimationState::Started,
|
|
AnimationState::Running { progress: 0.0f32 },
|
|
AnimationState::Running { progress: 0.5f32 },
|
|
AnimationState::Running { progress: 1.0f32 },
|
|
AnimationState::Stopped,
|
|
]
|
|
);
|
|
|
|
driver.free_animation(handle);
|
|
assert!(!driver.has_active_animations());
|
|
}
|
|
|
|
#[test]
|
|
fn pause_animation() {
|
|
let mut driver = AnimationDriver::default();
|
|
|
|
let test_animation = Rc::new(RefCell::new(RecordingAnimation::default()));
|
|
|
|
let handle = driver.start_animation(Animation::new(
|
|
Duration::from_secs(10),
|
|
Rc::downgrade(&(test_animation.clone() as Rc<dyn Animated>)),
|
|
));
|
|
|
|
let mut test_time = std::time::Instant::now();
|
|
|
|
driver.update_animations(test_time);
|
|
assert!(
|
|
matches!(driver.get_animation(handle).borrow().state, InternalAnimationState::Running{..})
|
|
);
|
|
|
|
assert_eq!(
|
|
test_animation.borrow().reported_states,
|
|
vec![AnimationState::Started, AnimationState::Running { progress: 0.0f32 }]
|
|
);
|
|
|
|
test_time += Duration::from_secs(5);
|
|
driver.update_animations(test_time);
|
|
|
|
assert_eq!(
|
|
test_animation.borrow().reported_states,
|
|
vec![
|
|
AnimationState::Started,
|
|
AnimationState::Running { progress: 0.0f32 },
|
|
AnimationState::Running { progress: 0.5f32 },
|
|
]
|
|
);
|
|
|
|
driver.pause_animation(handle);
|
|
|
|
test_time += Duration::from_secs(5);
|
|
driver.update_animations(test_time);
|
|
|
|
assert_eq!(
|
|
test_animation.borrow().reported_states,
|
|
vec![
|
|
AnimationState::Started,
|
|
AnimationState::Running { progress: 0.0f32 },
|
|
AnimationState::Running { progress: 0.5f32 },
|
|
]
|
|
);
|
|
|
|
driver.resume_animation(handle);
|
|
|
|
test_time += Duration::from_secs(5);
|
|
driver.update_animations(test_time);
|
|
|
|
assert_eq!(
|
|
test_animation.borrow().reported_states,
|
|
vec![
|
|
AnimationState::Started,
|
|
AnimationState::Running { progress: 0.0f32 },
|
|
AnimationState::Running { progress: 0.5f32 },
|
|
AnimationState::Running { progress: 1.0f32 },
|
|
AnimationState::Stopped,
|
|
]
|
|
);
|
|
|
|
assert!(!driver.has_active_animations());
|
|
|
|
driver.free_animation(handle);
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct SelfPausingAnimation {
|
|
pause_on_next_progress_update: bool,
|
|
driver: Rc<RefCell<AnimationDriver>>,
|
|
handle: Option<AnimationHandle>,
|
|
}
|
|
|
|
impl Animated for RefCell<SelfPausingAnimation> {
|
|
fn update_animation_state(self: Rc<Self>, state: AnimationState) {
|
|
if matches!(state, AnimationState::Running{..}) {
|
|
let this = self.borrow();
|
|
if this.pause_on_next_progress_update {
|
|
this.driver.borrow().pause_animation(this.handle.unwrap());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_self_pausing_animation() {
|
|
let driver = Rc::new(RefCell::new(AnimationDriver::default()));
|
|
|
|
let anim = Rc::new(RefCell::new(SelfPausingAnimation::default()));
|
|
let handle = {
|
|
let a = &mut anim.borrow_mut();
|
|
a.driver = driver.clone();
|
|
a.handle = Some(driver.borrow_mut().start_animation(Animation::new(
|
|
Duration::from_secs(10),
|
|
Rc::downgrade(&(anim.clone() as Rc<dyn Animated>)),
|
|
)));
|
|
a.handle.unwrap()
|
|
};
|
|
|
|
let mut test_time = std::time::Instant::now();
|
|
|
|
driver.borrow().update_animations(test_time);
|
|
|
|
assert!(
|
|
matches!(driver.borrow().get_animation(handle).borrow().state, InternalAnimationState::Running{..})
|
|
);
|
|
|
|
test_time += Duration::from_secs(1);
|
|
|
|
anim.borrow_mut().pause_on_next_progress_update = true;
|
|
|
|
test_time += Duration::from_secs(1);
|
|
driver.borrow().update_animations(test_time);
|
|
|
|
assert!(
|
|
matches!(driver.borrow().get_animation(handle).borrow().state, InternalAnimationState::ReadyToPause{..})
|
|
);
|
|
|
|
test_time += Duration::from_secs(1);
|
|
driver.borrow().update_animations(test_time);
|
|
|
|
assert!(
|
|
matches!(driver.borrow().get_animation(handle).borrow().state, InternalAnimationState::Paused{..})
|
|
);
|
|
}
|
|
}
|