use std::time::{Duration, Instant};
use crate::animation::{
config::{AnimatorConfig, SizeAnimation},
easing::{ease_position, ease_size},
interpolation::lerp_rect,
types::{Rect, WindowRef, WindowTarget},
};
pub(crate) struct WindowTween {
pub window_ref: WindowRef,
pub from_rect: Rect,
pub to_rect: Rect,
pub size_skip: bool,
}
pub(crate) struct AnimationBatch {
pub start_time: Instant,
pub duration: Duration,
pub tweens: Vec<WindowTween>,
}
impl AnimationBatch {
pub fn new(tweens: Vec<WindowTween>, duration: Duration) -> Self {
Self {
start_time: Instant::now(),
duration,
tweens,
}
}
pub fn progress(&self) -> f64 {
if self.duration.is_zero() {
return 1.0;
}
let elapsed = self.start_time.elapsed();
(elapsed.as_secs_f64() / self.duration.as_secs_f64()).min(1.0)
}
pub fn interpolated_rect(tween: &WindowTween, config: &AnimatorConfig, t: f64) -> Rect {
let tp = ease_position(&config.position_animation, t);
let ts = ease_size(&config.size_animation, tween.size_skip, t);
lerp_rect(tween.from_rect, tween.to_rect, tp, ts)
}
}
pub(crate) fn build_tweens(
targets: &[WindowTarget],
from_rects: &[(WindowRef, Rect)],
config: &AnimatorConfig,
) -> Vec<WindowTween> {
targets
.iter()
.filter_map(|target| {
let from_rect = from_rects
.iter()
.find(|(wref, _)| *wref == target.window_ref)
.map(|(_, rect)| *rect)?;
let to_rect = target.as_rect();
if from_rect == to_rect {
return None;
}
let size_skip = config.size_animation == SizeAnimation::DisabledIfUnchanged
&& from_rect.w == to_rect.w
&& from_rect.h == to_rect.h;
Some(WindowTween {
window_ref: target.window_ref,
from_rect,
to_rect,
size_skip,
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::animation::{
config::{AnimatorConfig, SizeAnimation},
types::{IVec2, WindowRef, WindowTarget},
};
use std::time::Duration;
fn make_config() -> AnimatorConfig {
AnimatorConfig::default()
}
fn make_batch(dur_ms: u64) -> AnimationBatch {
AnimationBatch::new(vec![], Duration::from_millis(dur_ms))
}
#[test]
fn progress_is_zero_at_start() {
let batch = make_batch(500);
assert!(batch.progress() < 0.05, "progress={}", batch.progress());
}
#[test]
fn progress_clamps_to_one_after_duration() {
let batch = make_batch(0);
assert_eq!(batch.progress(), 1.0);
}
#[test]
fn progress_clamps_to_exactly_one_after_real_duration() {
let batch = make_batch(5);
std::thread::sleep(Duration::from_millis(20));
let p = batch.progress();
assert!(
(p - 1.0).abs() < f64::EPSILON,
"progress should clamp to exactly 1.0, got {p}"
);
}
#[test]
fn progress_never_exceeds_one_even_well_past_duration() {
let batch = make_batch(1); std::thread::sleep(Duration::from_millis(50)); let p = batch.progress();
assert!(
p <= 1.0,
"progress must never exceed 1.0; got {p} \
(this breaks the t >= 1.0 completion invariant in run_worker)"
);
assert!(
(p - 1.0).abs() < f64::EPSILON,
"progress should be exactly 1.0 when well past duration, got {p}"
);
}
#[test]
fn progress_is_always_one_for_zero_duration() {
let batch = make_batch(0);
assert_eq!(batch.progress(), 1.0);
std::thread::sleep(Duration::from_millis(5));
assert_eq!(batch.progress(), 1.0);
}
#[test]
fn interpolated_rect_at_zero_returns_from() {
let from = Rect::new(0, 0, 100, 100);
let to = Rect::new(200, 200, 200, 200);
let tween = WindowTween {
window_ref: WindowRef(1),
from_rect: from,
to_rect: to,
size_skip: false,
};
let config = make_config();
let result = AnimationBatch::interpolated_rect(&tween, &config, 0.0);
assert_eq!(result, from);
}
#[test]
fn interpolated_rect_at_one_returns_to() {
let from = Rect::new(0, 0, 100, 100);
let to = Rect::new(200, 200, 200, 200);
let tween = WindowTween {
window_ref: WindowRef(1),
from_rect: from,
to_rect: to,
size_skip: false,
};
let config = make_config();
let result = AnimationBatch::interpolated_rect(&tween, &config, 1.0);
assert_eq!(result, to);
}
fn target(id: isize, x: i32, y: i32, w: i32, h: i32) -> WindowTarget {
WindowTarget::new(WindowRef(id), IVec2::new(x, y), IVec2::new(w, h))
}
fn from_rect(id: isize, x: i32, y: i32, w: i32, h: i32) -> (WindowRef, Rect) {
(WindowRef(id), Rect::new(x, y, w, h))
}
#[test]
fn build_tweens_filters_noop_windows() {
let targets = vec![
target(1, 0, 0, 100, 100), target(2, 50, 50, 100, 100), ];
let from_rects = vec![from_rect(1, 0, 0, 100, 100), from_rect(2, 0, 0, 100, 100)];
let tweens = build_tweens(&targets, &from_rects, &make_config());
assert_eq!(tweens.len(), 1);
assert_eq!(tweens[0].window_ref, WindowRef(2));
}
#[test]
fn build_tweens_size_skip_true_when_size_unchanged_and_disabled_if_unchanged() {
let targets = vec![target(1, 100, 100, 200, 200)]; let from_rects = vec![from_rect(1, 0, 0, 200, 200)];
let config = AnimatorConfig {
size_animation: SizeAnimation::DisabledIfUnchanged,
..AnimatorConfig::default()
};
let tweens = build_tweens(&targets, &from_rects, &config);
assert_eq!(tweens.len(), 1);
assert!(tweens[0].size_skip, "expected size_skip=true");
}
#[test]
fn build_tweens_size_skip_false_when_size_changes_even_with_disabled_if_unchanged() {
let targets = vec![target(1, 0, 0, 300, 300)]; let from_rects = vec![from_rect(1, 0, 0, 200, 200)];
let config = AnimatorConfig {
size_animation: SizeAnimation::DisabledIfUnchanged,
..AnimatorConfig::default()
};
let tweens = build_tweens(&targets, &from_rects, &config);
assert_eq!(tweens.len(), 1);
assert!(!tweens[0].size_skip, "expected size_skip=false");
}
#[test]
fn build_tweens_skips_windows_not_in_from_rects() {
let targets = vec![target(99, 10, 10, 100, 100)];
let from_rects: Vec<(WindowRef, Rect)> = vec![]; let tweens = build_tweens(&targets, &from_rects, &make_config());
assert!(tweens.is_empty());
}
}