use std::any::{TypeId, type_name};
use std::fmt::{self, Debug};
use std::hash::{Hash, Hasher};
use std::rc::Rc;
use inset_foundation::{App, Handle, HandleId, Listener};
use crate::tween::Animatable;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AnimationStatus {
Dismissed,
Forward,
Reverse,
Completed,
}
impl AnimationStatus {
pub fn is_dismissed(self) -> bool {
self == AnimationStatus::Dismissed
}
pub fn is_completed(self) -> bool {
self == AnimationStatus::Completed
}
pub fn is_animating(self) -> bool {
match self {
AnimationStatus::Forward | AnimationStatus::Reverse => true,
AnimationStatus::Completed | AnimationStatus::Dismissed => false,
}
}
pub fn is_forward_or_completed(self) -> bool {
match self {
AnimationStatus::Forward | AnimationStatus::Completed => true,
AnimationStatus::Reverse | AnimationStatus::Dismissed => false,
}
}
}
#[derive(Clone)]
pub struct AnimationStatusListener {
callback: Rc<StatusCallback>,
identity: StatusListenerIdentity,
}
type StatusCallback = dyn Fn(AnimationStatus, &mut App);
#[derive(Clone, Copy)]
enum StatusListenerIdentity {
Closure,
HandleMethod(HandleId, TypeId),
}
impl AnimationStatusListener {
pub fn new(callback: impl Fn(AnimationStatus, &mut App) + 'static) -> AnimationStatusListener {
AnimationStatusListener {
callback: Rc::new(callback),
identity: StatusListenerIdentity::Closure,
}
}
pub fn handle_method<T: 'static, F>(this: Handle<T>, f: F) -> AnimationStatusListener
where
F: Fn(Handle<T>, &mut App, AnimationStatus) + 'static,
{
const {
assert!(
std::mem::size_of::<F>() == 0,
"pass the function by name: a fn pointer or capturing closure \
has no canonical identity to match on removal"
)
};
AnimationStatusListener {
callback: Rc::new(move |status, app: &mut App| f(this, app, status)),
identity: StatusListenerIdentity::HandleMethod(this.id(), TypeId::of::<F>()),
}
}
pub fn call(&self, status: AnimationStatus, app: &mut App) {
(self.callback)(status, app);
}
}
impl PartialEq for AnimationStatusListener {
fn eq(&self, other: &AnimationStatusListener) -> bool {
match (self.identity, other.identity) {
(StatusListenerIdentity::Closure, StatusListenerIdentity::Closure) => {
Rc::ptr_eq(&self.callback, &other.callback)
}
(
StatusListenerIdentity::HandleMethod(entity, function),
StatusListenerIdentity::HandleMethod(other_entity, other_function),
) => entity == other_entity && function == other_function,
_ => false,
}
}
}
impl Eq for AnimationStatusListener {}
impl Hash for AnimationStatusListener {
fn hash<H: Hasher>(&self, state: &mut H) {
match self.identity {
StatusListenerIdentity::Closure => {
(Rc::as_ptr(&self.callback) as *const ()).hash(state);
}
StatusListenerIdentity::HandleMethod(entity, function) => {
entity.hash(state);
function.hash(state);
}
}
}
}
impl Debug for AnimationStatusListener {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("AnimationStatusListener")
}
}
pub trait Animation<T: 'static>: Sized + 'static {
fn add_listener(self: Handle<Self>, app: &mut App, listener: Listener);
fn remove_listener(self: Handle<Self>, app: &mut App, listener: &Listener);
fn add_status_listener(self: Handle<Self>, app: &mut App, listener: AnimationStatusListener);
fn remove_status_listener(
self: Handle<Self>,
app: &mut App,
listener: &AnimationStatusListener,
);
fn status(self: Handle<Self>, app: &App) -> AnimationStatus;
fn value(self: Handle<Self>, app: &App) -> T;
fn is_dismissed(self: Handle<Self>, app: &App) -> bool {
Self::status(self, app).is_dismissed()
}
fn is_completed(self: Handle<Self>, app: &App) -> bool {
Self::status(self, app).is_completed()
}
fn is_animating(self: Handle<Self>, app: &App) -> bool {
Self::status(self, app).is_animating()
}
fn is_forward_or_completed(self: Handle<Self>, app: &App) -> bool {
Self::status(self, app).is_forward_or_completed()
}
fn as_animation(self: Handle<Self>) -> AnyAnimation<T> {
AnyAnimation {
id: self.id(),
vtable: const { &AnimationVTable::of::<Self>() },
}
}
}
pub struct AnyAnimation<T: 'static> {
id: HandleId,
vtable: &'static AnimationVTable<T>,
}
struct AnimationVTable<T> {
add_listener: fn(&mut App, HandleId, Listener),
remove_listener: fn(&mut App, HandleId, &Listener),
add_status_listener: fn(&mut App, HandleId, AnimationStatusListener),
remove_status_listener: fn(&mut App, HandleId, &AnimationStatusListener),
status: fn(&App, HandleId) -> AnimationStatus,
value: fn(&App, HandleId) -> T,
is_dismissed: fn(&App, HandleId) -> bool,
is_completed: fn(&App, HandleId) -> bool,
is_animating: fn(&App, HandleId) -> bool,
is_forward_or_completed: fn(&App, HandleId) -> bool,
}
fn resolve<A: 'static>(id: HandleId) -> Handle<A> {
Handle::from_id(id)
}
impl<T: 'static> AnimationVTable<T> {
const fn of<A: Animation<T>>() -> AnimationVTable<T> {
AnimationVTable {
add_listener: |app, id, listener| A::add_listener(resolve(id), app, listener),
remove_listener: |app, id, listener| A::remove_listener(resolve(id), app, listener),
add_status_listener: |app, id, listener| {
A::add_status_listener(resolve(id), app, listener)
},
remove_status_listener: |app, id, listener| {
A::remove_status_listener(resolve(id), app, listener)
},
status: |app, id| A::status(resolve(id), app),
value: |app, id| A::value(resolve(id), app),
is_dismissed: |app, id| A::is_dismissed(resolve(id), app),
is_completed: |app, id| A::is_completed(resolve(id), app),
is_animating: |app, id| A::is_animating(resolve(id), app),
is_forward_or_completed: |app, id| A::is_forward_or_completed(resolve(id), app),
}
}
}
impl<T: 'static> AnyAnimation<T> {
pub fn id(self) -> HandleId {
self.id
}
pub fn downcast<A: 'static>(self, app: &App) -> Option<Handle<A>> {
app.handle::<A>(self.id)
}
pub fn add_listener(self, app: &mut App, listener: Listener) {
(self.vtable.add_listener)(app, self.id, listener)
}
pub fn remove_listener(self, app: &mut App, listener: &Listener) {
(self.vtable.remove_listener)(app, self.id, listener)
}
pub fn add_status_listener(self, app: &mut App, listener: AnimationStatusListener) {
(self.vtable.add_status_listener)(app, self.id, listener)
}
pub fn remove_status_listener(self, app: &mut App, listener: &AnimationStatusListener) {
(self.vtable.remove_status_listener)(app, self.id, listener)
}
pub fn status(self, app: &App) -> AnimationStatus {
(self.vtable.status)(app, self.id)
}
pub fn value(self, app: &App) -> T {
(self.vtable.value)(app, self.id)
}
pub fn is_dismissed(self, app: &App) -> bool {
(self.vtable.is_dismissed)(app, self.id)
}
pub fn is_completed(self, app: &App) -> bool {
(self.vtable.is_completed)(app, self.id)
}
pub fn is_animating(self, app: &App) -> bool {
(self.vtable.is_animating)(app, self.id)
}
pub fn is_forward_or_completed(self, app: &App) -> bool {
(self.vtable.is_forward_or_completed)(app, self.id)
}
}
impl AnyAnimation<f64> {
pub fn drive<U: 'static>(
self,
app: &mut App,
child: impl Animatable<U> + Clone + 'static,
) -> AnyAnimation<U> {
child.animate(app, self)
}
}
impl<T: 'static> inset_foundation::Listenable for AnyAnimation<T> {
fn add_listener(&self, app: &mut App, listener: Listener) {
AnyAnimation::add_listener(*self, app, listener);
}
fn remove_listener(&self, app: &mut App, listener: &Listener) {
AnyAnimation::remove_listener(*self, app, listener);
}
}
impl<T> Clone for AnyAnimation<T> {
fn clone(&self) -> AnyAnimation<T> {
*self
}
}
impl<T> Copy for AnyAnimation<T> {}
impl<T> From<AnyAnimation<T>> for HandleId {
fn from(animation: AnyAnimation<T>) -> HandleId {
animation.id
}
}
impl<T> PartialEq for AnyAnimation<T> {
fn eq(&self, other: &AnyAnimation<T>) -> bool {
self.id == other.id
}
}
impl<T> Eq for AnyAnimation<T> {}
impl<T> Debug for AnyAnimation<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "AnyAnimation<{}>({:?})", type_name::<T>(), self.id)
}
}
#[cfg(test)]
mod tests {
use inset_foundation::AppCell;
use std::cell::Cell;
use super::*;
#[test]
fn the_derived_status_getters_match_flutter() {
use AnimationStatus::*;
assert_eq!(
[
Dismissed.is_dismissed(),
Forward.is_dismissed(),
Reverse.is_dismissed(),
Completed.is_dismissed()
],
[true, false, false, false]
);
assert_eq!(
[
Dismissed.is_completed(),
Forward.is_completed(),
Reverse.is_completed(),
Completed.is_completed()
],
[false, false, false, true]
);
assert_eq!(
[
Dismissed.is_animating(),
Forward.is_animating(),
Reverse.is_animating(),
Completed.is_animating()
],
[false, true, true, false]
);
assert_eq!(
[
Dismissed.is_forward_or_completed(),
Forward.is_forward_or_completed(),
Reverse.is_forward_or_completed(),
Completed.is_forward_or_completed()
],
[false, true, false, true]
);
}
#[test]
fn only_a_handle_to_the_same_status_closure_compares_equal() {
let listener = AnimationStatusListener::new(|_status, _app| {});
assert_eq!(listener, listener.clone());
assert_ne!(listener, AnimationStatusListener::new(|_status, _app| {}));
}
#[test]
fn a_rebuilt_handle_method_status_listener_matches_like_a_dart_tear_off() {
struct Host;
fn hook(_this: Handle<Host>, _app: &mut App, _status: AnimationStatus) {}
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let a = app.create(Host);
let b = app.create(Host);
assert_eq!(
AnimationStatusListener::handle_method(a, hook),
AnimationStatusListener::handle_method(a, hook)
);
assert_ne!(
AnimationStatusListener::handle_method(a, hook),
AnimationStatusListener::handle_method(b, hook)
);
assert_ne!(
AnimationStatusListener::handle_method(a, hook),
AnimationStatusListener::new(|_status, _app| {})
);
}
struct OverridingNode;
impl Animation<f64> for OverridingNode {
fn add_listener(self: Handle<Self>, _app: &mut App, _listener: Listener) {}
fn remove_listener(self: Handle<Self>, _app: &mut App, _listener: &Listener) {}
fn add_status_listener(
self: Handle<Self>,
_app: &mut App,
_listener: AnimationStatusListener,
) {
}
fn remove_status_listener(
self: Handle<Self>,
_app: &mut App,
_listener: &AnimationStatusListener,
) {
}
fn status(self: Handle<Self>, _app: &App) -> AnimationStatus {
AnimationStatus::Forward
}
fn value(self: Handle<Self>, _app: &App) -> f64 {
0.25
}
fn is_animating(self: Handle<Self>, _app: &App) -> bool {
false
}
}
#[test]
fn the_erased_handle_dispatches_overridden_getters() {
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let node = app.create(OverridingNode);
let animation = node.as_animation();
assert_eq!(animation.status(&app), AnimationStatus::Forward);
assert_eq!(animation.value(&app), 0.25);
assert!(
!animation.is_animating(&app),
"the override, not the status"
);
assert!(animation.is_forward_or_completed(&app), "the default");
}
#[test]
fn erased_handles_are_equal_exactly_when_their_entities_are() {
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let first = app.create(OverridingNode);
let second = app.create(OverridingNode);
assert_eq!(first.as_animation(), first.as_animation());
assert_ne!(first.as_animation(), second.as_animation());
}
struct Counter(u32);
impl Animation<f64> for Counter {
fn add_listener(self: Handle<Self>, _app: &mut App, _listener: Listener) {}
fn remove_listener(self: Handle<Self>, _app: &mut App, _listener: &Listener) {}
fn add_status_listener(
self: Handle<Self>,
_app: &mut App,
_listener: AnimationStatusListener,
) {
}
fn remove_status_listener(
self: Handle<Self>,
_app: &mut App,
_listener: &AnimationStatusListener,
) {
}
fn status(self: Handle<Self>, _app: &App) -> AnimationStatus {
AnimationStatus::Forward
}
fn value(self: Handle<Self>, app: &App) -> f64 {
f64::from(app.get(self).0)
}
}
#[test]
fn an_erased_handle_does_not_consume_the_typed_one() {
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let counter = app.create(Counter(1));
let erased = counter.as_animation();
app.get_mut(counter).0 = 2;
assert_eq!(erased.value(&app), 2.0);
}
#[test]
#[should_panic(expected = "stale handle")]
fn an_erased_handle_to_a_destroyed_animation_panics_on_slot_access() {
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let counter = app.create(Counter(1));
let animation = counter.as_animation();
app.destroy(counter);
animation.value(&app);
}
#[test]
fn a_status_listener_receives_the_status_and_the_app() {
thread_local! {
static RECEIVED: Cell<Option<AnimationStatus>> = const { Cell::new(None) };
}
let listener = AnimationStatusListener::new(|status, _app| {
RECEIVED.with(|received| received.set(Some(status)));
});
listener.call(AnimationStatus::Reverse, &mut AppCell::new().borrow_mut());
assert_eq!(RECEIVED.with(Cell::get), Some(AnimationStatus::Reverse));
}
}