use alloc::boxed::Box;
use alloc::vec::Vec;
use crate::anim::Tween;
use crate::object::ObjectNode;
use crate::widget::Rect;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ObjectAnimId(u32);
struct ObjectAnimEntry {
id: ObjectAnimId,
tween: Tween,
apply: Box<dyn FnMut(i32) -> Option<Rect>>,
delay_ticks: u32,
on_complete: Option<Box<dyn FnOnce()>>,
paused: bool,
}
pub struct NodeAnimSet {
entries: Vec<ObjectAnimEntry>,
}
impl NodeAnimSet {
pub(crate) fn new() -> Self {
Self {
entries: Vec::new(),
}
}
}
#[derive(Default)]
pub struct ObjectAnims {
next_id: u32,
}
impl ObjectAnims {
pub fn new() -> Self {
Self { next_id: 0 }
}
pub fn bind(
&mut self,
node: &mut ObjectNode,
tween: Tween,
apply: Box<dyn FnMut(i32) -> Option<Rect>>,
delay_ticks: u32,
on_complete: Option<Box<dyn FnOnce()>>,
) -> ObjectAnimId {
let id = ObjectAnimId(self.next_id);
self.next_id = self.next_id.wrapping_add(1);
let slot = node
.anims
.get_or_insert_with(|| Box::new(NodeAnimSet::new()));
slot.entries.push(ObjectAnimEntry {
id,
tween,
apply,
delay_ticks,
on_complete,
paused: false,
});
id
}
pub fn tick(&mut self, root: &mut ObjectNode, sink: &mut dyn FnMut(Rect)) {
tick_node(root, sink);
}
pub fn cancel(&mut self, root: &mut ObjectNode, id: ObjectAnimId) -> bool {
cancel_in_node(root, id)
}
pub fn pause_anim(&mut self, root: &mut ObjectNode, id: ObjectAnimId) -> bool {
set_paused_in_node(root, id, true)
}
pub fn resume_anim(&mut self, root: &mut ObjectNode, id: ObjectAnimId) -> bool {
set_paused_in_node(root, id, false)
}
}
fn tick_node(node: &mut ObjectNode, sink: &mut dyn FnMut(Rect)) {
advance_node_entries(node, sink);
let child_count = node.children_mut().len();
for i in 0..child_count {
tick_node(&mut node.children_mut()[i], sink);
}
}
fn advance_node_entries(node: &mut ObjectNode, sink: &mut dyn FnMut(Rect)) {
let Some(anim_set) = node.anims.as_mut() else {
return;
};
let mut completed_indices: Vec<usize> = Vec::new();
for (i, entry) in anim_set.entries.iter_mut().enumerate() {
if entry.paused {
continue;
}
if entry.delay_ticks > 0 {
entry.delay_ticks -= 1;
continue;
}
let value = entry.tween.step();
if let Some(rect) = (entry.apply)(value) {
sink(rect);
}
if entry.tween.finished() {
completed_indices.push(i);
}
}
for &i in completed_indices.iter().rev() {
let entry = anim_set.entries.remove(i);
if let Some(cb) = entry.on_complete {
cb();
}
}
}
fn cancel_in_node(node: &mut ObjectNode, id: ObjectAnimId) -> bool {
if let Some(anim_set) = node.anims.as_mut() {
let before = anim_set.entries.len();
anim_set.entries.retain(|e| e.id != id);
if anim_set.entries.len() != before {
return true;
}
}
let child_count = node.children_mut().len();
for i in 0..child_count {
if cancel_in_node(&mut node.children_mut()[i], id) {
return true;
}
}
false
}
fn set_paused_in_node(node: &mut ObjectNode, id: ObjectAnimId, paused: bool) -> bool {
if let Some(anim_set) = node.anims.as_mut() {
for entry in anim_set.entries.iter_mut() {
if entry.id == id {
entry.paused = paused;
return true;
}
}
}
let child_count = node.children_mut().len();
for i in 0..child_count {
if set_paused_in_node(&mut node.children_mut()[i], id, paused) {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::rc::Rc;
use alloc::vec;
use alloc::vec::Vec;
use core::cell::RefCell;
use crate::anim::{Easing, LoopMode, Tween};
use crate::event::Event;
use crate::object::{ObjectFlags, ObjectNode};
use crate::renderer::Renderer;
use crate::widget::{Rect, Widget};
struct TinyWidget {
bounds: Rect,
}
impl TinyWidget {
fn node(x: i32, y: i32, w: i32, h: i32) -> ObjectNode {
ObjectNode::new(Rc::new(RefCell::new(Self {
bounds: Rect {
x,
y,
width: w,
height: h,
},
})))
}
}
impl Widget for TinyWidget {
fn bounds(&self) -> Rect {
self.bounds
}
fn draw(&self, _renderer: &mut dyn Renderer) {}
fn handle_event(&mut self, _event: &Event) -> bool {
false
}
}
fn collect_dirty(oa: &mut ObjectAnims, root: &mut ObjectNode) -> Vec<Rect> {
let mut rects = Vec::new();
oa.tick(root, &mut |r| rects.push(r));
rects
}
#[test]
fn bind_and_tick_advances_tween_applies_value() {
let mut root = TinyWidget::node(0, 0, 100, 100);
let mut oa = ObjectAnims::new();
let applied: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(Vec::new()));
let a = applied.clone();
let tween = Tween::new(0, 10, 2); oa.bind(
&mut root,
tween,
Box::new(move |v| {
a.borrow_mut().push(v);
None
}),
0,
None,
);
collect_dirty(&mut oa, &mut root); collect_dirty(&mut oa, &mut root); collect_dirty(&mut oa, &mut root);
assert_eq!(
*applied.borrow(),
vec![5, 10],
"apply called with tween values"
);
assert!(
root.anims.as_ref().is_none_or(|s| s.entries.is_empty()),
"entry removed after completion"
);
}
#[test]
fn delay_window_no_apply_until_expired() {
let mut root = TinyWidget::node(0, 0, 100, 100);
let mut oa = ObjectAnims::new();
let applied: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(Vec::new()));
let a = applied.clone();
let tween = Tween::new(0, 100, 3);
oa.bind(
&mut root,
tween,
Box::new(move |v| {
a.borrow_mut().push(v);
None
}),
2, None,
);
collect_dirty(&mut oa, &mut root); collect_dirty(&mut oa, &mut root); assert!(applied.borrow().is_empty(), "no apply during delay");
let rects1 = collect_dirty(&mut oa, &mut root); assert!(!applied.borrow().is_empty(), "apply fires after delay");
assert_eq!(*applied.borrow(), vec![33], "first value at elapsed=1");
let _ = rects1;
}
#[test]
fn dirty_rects_reach_sink() {
let mut root = TinyWidget::node(0, 0, 100, 100);
let mut oa = ObjectAnims::new();
let r = Rect {
x: 5,
y: 5,
width: 10,
height: 10,
};
let tween = Tween::new(0, 1, 10).with_loop(LoopMode::Repeat(0)); oa.bind(&mut root, tween, Box::new(move |_v| Some(r)), 0, None);
let rects = collect_dirty(&mut oa, &mut root);
assert_eq!(rects, vec![r], "dirty rect pushed to sink");
}
#[test]
fn completion_fires_on_complete_once() {
let mut root = TinyWidget::node(0, 0, 100, 100);
let mut oa = ObjectAnims::new();
let fired: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
let f = fired.clone();
let tween = Tween::new(0, 1, 1); oa.bind(
&mut root,
tween,
Box::new(|_v| None),
0,
Some(Box::new(move || *f.borrow_mut() += 1)),
);
collect_dirty(&mut oa, &mut root); collect_dirty(&mut oa, &mut root); collect_dirty(&mut oa, &mut root);
assert_eq!(*fired.borrow(), 1, "on_complete fired exactly once");
}
#[test]
fn cancel_does_not_fire_on_complete() {
let mut root = TinyWidget::node(0, 0, 100, 100);
let mut oa = ObjectAnims::new();
let fired: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
let f = fired.clone();
let tween = Tween::new(0, 1, 100); let id = oa.bind(
&mut root,
tween,
Box::new(|_v| None),
0,
Some(Box::new(move || *f.borrow_mut() += 1)),
);
collect_dirty(&mut oa, &mut root);
let found = oa.cancel(&mut root, id);
assert!(found, "cancel returned true for live id");
collect_dirty(&mut oa, &mut root);
assert_eq!(*fired.borrow(), 0, "on_complete NOT called on cancel");
}
#[test]
fn detach_cancellation_apply_not_called_after_detach() {
let mut root = TinyWidget::node(0, 0, 100, 100);
let child = TinyWidget::node(0, 0, 10, 10);
root.append_child(child);
let mut oa = ObjectAnims::new();
let apply_called: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
let complete_called: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
let ac = apply_called.clone();
let cc = complete_called.clone();
let tween = Tween::new(0, 10, 100); oa.bind(
root.children_mut().first_mut().unwrap(),
tween,
Box::new(move |_v| {
*ac.borrow_mut() += 1;
None
}),
0,
Some(Box::new(move || *cc.borrow_mut() += 1)),
);
let _detached = root.detach_child(0).expect("child present");
collect_dirty(&mut oa, &mut root);
collect_dirty(&mut oa, &mut root);
assert_eq!(*apply_called.borrow(), 0, "apply not called after detach");
assert_eq!(
*complete_called.borrow(),
0,
"on_complete not fired after detach"
);
}
#[test]
fn hide_does_not_cancel_animation() {
let mut root = TinyWidget::node(0, 0, 100, 100);
let mut oa = ObjectAnims::new();
let apply_count: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
let ac = apply_count.clone();
let tween = Tween::new(0, 10, 10).with_loop(LoopMode::Repeat(0));
oa.bind(
&mut root,
tween,
Box::new(move |_v| {
*ac.borrow_mut() += 1;
None
}),
0,
None,
);
root.set_flag(ObjectFlags::HIDDEN, true);
collect_dirty(&mut oa, &mut root);
collect_dirty(&mut oa, &mut root);
collect_dirty(&mut oa, &mut root);
assert_eq!(
*apply_count.borrow(),
3,
"animation advances even when node is hidden"
);
}
#[test]
fn five_concurrent_anims_produce_at_most_five_dirty_rects() {
let mut root = TinyWidget::node(0, 0, 100, 100);
let mut oa = ObjectAnims::new();
for i in 0..5i32 {
let r = Rect {
x: i * 10,
y: 0,
width: 10,
height: 10,
};
let tween = Tween::new(0, 1, 50).with_loop(LoopMode::Repeat(0));
oa.bind(&mut root, tween, Box::new(move |_v| Some(r)), 0, None);
}
for _ in 0..20 {
let rects = collect_dirty(&mut oa, &mut root);
assert!(
rects.len() <= 5,
"at most 5 dirty rects per tick, got {}",
rects.len()
);
}
}
#[test]
fn pause_and_resume_anim() {
let mut root = TinyWidget::node(0, 0, 100, 100);
let mut oa = ObjectAnims::new();
let values: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(Vec::new()));
let v = values.clone();
let tween = Tween::new(0, 10, 10).with_loop(LoopMode::Repeat(0));
let id = oa.bind(
&mut root,
tween,
Box::new(move |val| {
v.borrow_mut().push(val);
None
}),
0,
None,
);
collect_dirty(&mut oa, &mut root); let count_after_first_tick = values.borrow().len();
assert_eq!(count_after_first_tick, 1);
assert!(oa.pause_anim(&mut root, id));
collect_dirty(&mut oa, &mut root); collect_dirty(&mut oa, &mut root);
assert_eq!(values.borrow().len(), 1, "no advance while paused");
assert!(oa.resume_anim(&mut root, id));
collect_dirty(&mut oa, &mut root); assert_eq!(values.borrow().len(), 2, "resumes after un-pause");
}
#[test]
fn cancel_unknown_id_returns_false() {
let mut root = TinyWidget::node(0, 0, 100, 100);
let mut oa = ObjectAnims::new();
let unknown = ObjectAnimId(999);
assert!(!oa.cancel(&mut root, unknown));
}
#[test]
fn deeply_nested_child_animation_advances() {
let mut root = TinyWidget::node(0, 0, 100, 100);
let child = TinyWidget::node(0, 0, 50, 50);
root.append_child(child);
let grandchild = TinyWidget::node(0, 0, 20, 20);
root.children_mut()[0].append_child(grandchild);
let mut oa = ObjectAnims::new();
let fired: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
let f = fired.clone();
let tween = Tween::new(0, 1, 1).with_easing(Easing::Linear);
oa.bind(
&mut root.children_mut()[0].children_mut()[0],
tween,
Box::new(move |_v| {
*f.borrow_mut() += 1;
None
}),
0,
None,
);
collect_dirty(&mut oa, &mut root);
assert_eq!(
*fired.borrow(),
1,
"grandchild animation advanced by root tick"
);
}
}