use animato_core::Update;
use animato_spring::{SpringConfig, SpringN};
use animato_tween::{Loop, Tween, TweenBuilder};
use std::fmt;
use std::sync::{Arc, Mutex};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct NativeWindowState {
pub position: [f32; 2],
pub size: [f32; 2],
pub opacity: f32,
}
impl Default for NativeWindowState {
fn default() -> Self {
Self {
position: [0.0, 0.0],
size: [800.0, 600.0],
opacity: 1.0,
}
}
}
#[derive(Clone, Debug)]
struct WindowTweenOptions {
duration: f32,
easing: animato_core::Easing,
delay: f32,
time_scale: f32,
looping: Loop,
}
impl WindowTweenOptions {
fn from_tween<T: animato_core::Animatable>(tween: &Tween<T>) -> Self {
Self {
duration: tween.duration,
easing: tween.easing.clone(),
delay: tween.delay,
time_scale: tween.time_scale,
looping: tween.looping.clone(),
}
}
fn apply<T: animato_core::Animatable>(&self, builder: TweenBuilder<T>) -> Tween<T> {
builder
.duration(self.duration)
.easing(self.easing.clone())
.delay(self.delay)
.time_scale(self.time_scale)
.looping(self.looping.clone())
.build()
}
}
#[derive(Clone)]
pub struct WindowAnimationHandle {
state: Arc<Mutex<NativeWindowState>>,
position: Arc<Mutex<Option<Tween<[f32; 2]>>>>,
size: Arc<Mutex<Option<Tween<[f32; 2]>>>>,
opacity: Arc<Mutex<Option<Tween<f32>>>>,
options: WindowTweenOptions,
}
impl fmt::Debug for WindowAnimationHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WindowAnimationHandle")
.field("state", &self.state())
.finish_non_exhaustive()
}
}
impl WindowAnimationHandle {
pub fn state(&self) -> NativeWindowState {
*self
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub fn move_to(&self, x: f32, y: f32) {
let start = self.state().position;
let tween = self.options.apply(Tween::new(start, [x, y]));
crate::with_lock(&self.position, |slot| *slot = Some(tween));
}
pub fn resize_to(&self, w: f32, h: f32) {
let start = self.state().size;
let tween = self
.options
.apply(Tween::new(start, [w.max(1.0), h.max(1.0)]));
crate::with_lock(&self.size, |slot| *slot = Some(tween));
}
pub fn opacity_to(&self, opacity: f32) {
let start = self.state().opacity;
let tween = self
.options
.apply(Tween::new(start, opacity.clamp(0.0, 1.0)));
crate::with_lock(&self.opacity, |slot| *slot = Some(tween));
}
pub fn snap_to(&self, state: NativeWindowState) {
crate::with_lock(&self.state, |current| *current = state);
crate::with_lock(&self.position, |slot| *slot = None);
crate::with_lock(&self.size, |slot| *slot = None);
crate::with_lock(&self.opacity, |slot| *slot = None);
}
pub fn tick(&self, dt: f32) -> bool {
let mut running = false;
let dt = dt.max(0.0);
crate::with_lock(&self.position, |slot| {
if let Some(tween) = slot.as_mut() {
running |= tween.update(dt);
let value = tween.value();
crate::with_lock(&self.state, |state| state.position = value);
if tween.is_complete() {
*slot = None;
}
}
});
crate::with_lock(&self.size, |slot| {
if let Some(tween) = slot.as_mut() {
running |= tween.update(dt);
let value = tween.value();
crate::with_lock(&self.state, |state| state.size = value);
if tween.is_complete() {
*slot = None;
}
}
});
crate::with_lock(&self.opacity, |slot| {
if let Some(tween) = slot.as_mut() {
running |= tween.update(dt);
let value = tween.value();
crate::with_lock(&self.state, |state| state.opacity = value);
if tween.is_complete() {
*slot = None;
}
}
});
running
}
}
#[derive(Clone)]
pub struct WindowSpringHandle {
state: Arc<Mutex<NativeWindowState>>,
position: Arc<Mutex<SpringN<[f32; 2]>>>,
}
impl fmt::Debug for WindowSpringHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WindowSpringHandle")
.field("state", &self.state())
.finish_non_exhaustive()
}
}
impl WindowSpringHandle {
pub fn state(&self) -> NativeWindowState {
*self
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub fn move_to(&self, x: f32, y: f32) {
crate::with_lock(&self.position, |spring| spring.set_target([x, y]));
}
pub fn snap_to(&self, x: f32, y: f32) {
crate::with_lock(&self.position, |spring| spring.snap_to([x, y]));
crate::with_lock(&self.state, |state| state.position = [x, y]);
}
pub fn tick(&self, dt: f32) -> bool {
crate::with_lock(&self.position, |spring| {
let running = spring.update(dt.max(0.0));
let position = spring.position();
crate::with_lock(&self.state, |state| state.position = position);
running
})
}
}
pub fn use_window_animation(
config: impl FnOnce(TweenBuilder<[f32; 2]>) -> TweenBuilder<[f32; 2]>,
) -> WindowAnimationHandle {
let template = config(Tween::new([0.0, 0.0], [0.0, 0.0])).build();
let options = WindowTweenOptions::from_tween(&template);
WindowAnimationHandle {
state: Arc::new(Mutex::new(NativeWindowState::default())),
position: Arc::new(Mutex::new(None)),
size: Arc::new(Mutex::new(None)),
opacity: Arc::new(Mutex::new(None)),
options,
}
}
pub fn use_window_spring(config: SpringConfig) -> WindowSpringHandle {
WindowSpringHandle {
state: Arc::new(Mutex::new(NativeWindowState::default())),
position: Arc::new(Mutex::new(SpringN::new(config, [0.0, 0.0]))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn window_animation_tracks_state() {
let handle = use_window_animation(|builder| builder.duration(0.1));
handle.move_to(100.0, 50.0);
assert!(handle.tick(0.05));
assert!(handle.state().position[0] >= 0.0);
handle.tick(0.1);
assert_eq!(handle.state().position, [100.0, 50.0]);
}
#[test]
fn window_spring_snap_updates_state() {
let handle = use_window_spring(SpringConfig::snappy());
handle.snap_to(12.0, 24.0);
assert_eq!(handle.state().position, [12.0, 24.0]);
}
#[test]
fn window_animation_tracks_size_opacity_snap_and_debug() {
let handle = use_window_animation(|builder| {
builder
.duration(0.2)
.delay(0.05)
.time_scale(2.0)
.looping(Loop::Once)
});
assert_eq!(handle.state(), NativeWindowState::default());
handle.resize_to(-20.0, 120.0);
handle.opacity_to(2.0);
assert!(handle.tick(0.05));
assert_eq!(handle.state().size, [800.0, 600.0]);
let _ = handle.tick(0.1);
assert!(handle.state().size[0] <= 800.0);
assert!(handle.state().opacity <= 1.0);
assert!(format!("{handle:?}").contains("WindowAnimationHandle"));
let snapped = NativeWindowState {
position: [10.0, 20.0],
size: [320.0, 240.0],
opacity: 0.25,
};
handle.snap_to(snapped);
assert_eq!(handle.state(), snapped);
assert!(!handle.tick(1.0));
}
#[test]
fn window_spring_moves_ticks_and_debug_formats() {
let handle = use_window_spring(SpringConfig::stiff());
assert_eq!(handle.state().position, [0.0, 0.0]);
handle.move_to(40.0, -20.0);
assert!(handle.tick(1.0 / 60.0));
assert_ne!(handle.state().position, [0.0, 0.0]);
assert!(format!("{handle:?}").contains("WindowSpringHandle"));
}
}