1use cvkg_core::{LayoutCache, LayoutView, Rect};
2use std::collections::HashMap;
3
4pub struct AnimationEngine {
6 pub active_transitions: HashMap<u64, cvkg_anim::physics::ViscousSpring>,
7 pub eviction_generation: u64,
9 pub transition_generation: HashMap<u64, u64>,
11 pub eviction_threshold: u64,
13}
14
15impl Default for AnimationEngine {
16 fn default() -> Self {
17 Self::new()
18 }
19}
20
21impl AnimationEngine {
22 pub fn new() -> Self {
24 Self {
25 active_transitions: HashMap::new(),
26 eviction_generation: 0,
27 transition_generation: HashMap::new(),
28 eviction_threshold: 300,
29 }
30 }
31
32 pub fn get_or_insert_engine(cache: &mut LayoutCache) -> &mut Self {
34 if cache.animators.is_none() {
35 cache.animators = Some(Box::new(AnimationEngine::new()));
36 }
37 cache
38 .animators
39 .as_mut()
40 .unwrap()
41 .downcast_mut::<AnimationEngine>()
42 .unwrap()
43 }
44
45 pub fn evict_stale_transitions(&mut self) {
47 self.eviction_generation += 1;
48 let threshold = self.eviction_threshold;
49 let current_gen = self.eviction_generation;
50 self.active_transitions.retain(|hash, spring| {
51 let recent = self
52 .transition_generation
53 .get(hash)
54 .map_or(false, |g| current_gen - *g < threshold);
55 let unsettled = spring.velocity_a.length_sq() > 0.0001 || spring.velocity_b.length_sq() > 0.0001;
56 recent || unsettled
57 });
58 self.transition_generation
59 .retain(|hash, _| self.active_transitions.contains_key(hash));
60 }
61}
62
63pub fn apply_layout_animations(
65 rects: Vec<Rect>,
66 subviews: &mut [&mut dyn LayoutView],
67 cache: &mut LayoutCache,
68) {
69 let mut transitions_to_update = Vec::new();
70
71 for (child, target_rect) in subviews.iter().zip(&rects) {
72 let hash = child.view_hash();
73 if hash != 0 {
74 if let Some(prev) = cache.previous_rects.get(&hash) {
75 let dx = (prev.x - target_rect.x).abs();
76 let dy = (prev.y - target_rect.y).abs();
77 let dw = (prev.width - target_rect.width).abs();
78 let dh = (prev.height - target_rect.height).abs();
79 let epsilon = 1e-3;
80 if dx > epsilon || dy > epsilon || dw > epsilon || dh > epsilon {
81 transitions_to_update.push((hash, *prev, *target_rect));
82 }
83 }
84 cache.previous_rects.insert(hash, *target_rect);
85 cache.previous_rects_generation.insert(hash, cache.eviction_generation);
86 }
87 }
88
89 let mut interpolated_rects = HashMap::new();
90 let delta = cache.delta_time;
91 let scale = cache.scale_factor;
92 let anim_engine = AnimationEngine::get_or_insert_engine(cache);
93
94 for (hash, prev, target_rect) in transitions_to_update {
95 let mut spring = if let Some(mut existing) = anim_engine.active_transitions.remove(&hash) {
96 existing.position_b =
97 cvkg_anim::physics::Vec3::new(target_rect.x, target_rect.y, target_rect.width);
98 existing
99 } else {
100 cvkg_anim::physics::ViscousSpring::new(
101 cvkg_anim::physics::Vec3::new(prev.x, prev.y, prev.width),
102 cvkg_anim::physics::Vec3::new(target_rect.x, target_rect.y, target_rect.width),
103 0.9,
104 1000.0,
105 )
106 };
107 spring.step(delta);
108
109 let speed = (spring.velocity_a.length_sq() + spring.velocity_b.length_sq()).sqrt();
110 let snap = |v: f32| (v * scale).round() / scale;
111
112 let (rx, ry, rw) = if speed < 0.05 {
113 (
114 snap(spring.position_a.x),
115 snap(spring.position_a.y),
116 snap(spring.position_a.z),
117 )
118 } else {
119 (
120 spring.position_a.x,
121 spring.position_a.y,
122 spring.position_a.z,
123 )
124 };
125
126 interpolated_rects.insert(
127 hash,
128 Rect {
129 x: rx,
130 y: ry,
131 width: rw,
132 height: target_rect.height,
133 },
134 );
135 anim_engine.active_transitions.insert(hash, spring);
136 anim_engine.transition_generation.insert(hash, anim_engine.eviction_generation);
137 }
138
139 cache.evict_stale_entries();
140
141 let anim_engine = AnimationEngine::get_or_insert_engine(cache);
142 anim_engine.evict_stale_transitions();
143
144 for (child, mut target_rect) in subviews.iter_mut().zip(rects) {
145 let hash = child.view_hash();
146 if let Some(interp) = interpolated_rects.get(&hash) {
147 target_rect = *interp;
148 }
149 let is_visible = if let Some(viewport) = cache.viewport {
150 target_rect.intersects(&viewport)
151 } else {
152 true
153 };
154 if is_visible {
155 crate::with_layout_cycle_guard_void(hash, || {
156 child.place_subviews(target_rect, &mut [], cache);
157 });
158 }
159 }
160}