hikari-animation 0.2.0

Animation hooks, easing functions and transition utilities for the Hikari design system
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
//! Animation lifecycle management system for WASM
//!
//! Provides automatic lifecycle management for animations, including
//! cleanup on component unmount, automatic stopping, and resource management.
//! Includes support for:
//! - Normal completion callbacks
//! - Exception/interruption handling (component unmount, element removal)
//! - Element handle-based target element monitoring
//! - Animation state callbacks (onComplete, onError, onCancel)

use std::collections::HashMap;
use std::rc::Rc;

use tairitsu_vdom::Platform;

use super::builder::AnimationBuilder;
use super::state::AnimationDataStore as AnimationState;

/// Element handle type (u64 for WIT bindings)
pub type ElementHandle = u64;

/// Callback types for animation lifecycle events
pub enum LifecycleCallback {
    /// Called when animation completes normally
    OnComplete(Box<dyn FnOnce()>),
    /// Called when animation is interrupted (e.g., component unmount)
    OnInterrupt(Box<dyn FnOnce()>),
    /// Called when animation encounters an error
    OnError(Box<dyn FnOnce()>),
}

/// Animation entry with full lifecycle information
struct AnimationEntry {
    stop_fn: Box<dyn FnOnce()>,
    cleanup_fn: Option<Box<dyn FnOnce()>>,
    callbacks: Vec<LifecycleCallback>,
    _target_element: Option<ElementHandle>, // Reserved for future element lifecycle monitoring
    created_at: std::time::Instant,
}

/// Animation registry for managing active animations
///
/// This registry tracks all active animations and provides
/// automatic cleanup when components unmount or animations complete.
/// Includes support for:
/// - Element handle-based target element monitoring
/// - Lifecycle callbacks
/// - Timeout-based cleanup
pub struct AnimationRegistry {
    /// Map of animation IDs to their animation entries
    animations: HashMap<String, AnimationEntry>,
    /// Next animation ID counter
    next_id: u64,
    /// Default timeout for animations (None = no timeout)
    default_timeout_ms: Option<u64>,
}

impl AnimationRegistry {
    /// Create a new animation registry
    pub fn new() -> Self {
        Self {
            animations: HashMap::new(),
            next_id: 0,
            default_timeout_ms: None,
        }
    }

    /// Create a new animation registry with default timeout
    ///
    /// # Arguments
    ///
    /// * `timeout_ms` - Default timeout in milliseconds (None = no timeout)
    pub fn new_with_timeout(timeout_ms: Option<u64>) -> Self {
        Self {
            animations: HashMap::new(),
            next_id: 0,
            default_timeout_ms: timeout_ms,
        }
    }

    /// Register a new animation and return its ID
    ///
    /// # Arguments
    ///
    /// * `stop_fn` - Function to call when stopping animation
    /// * `cleanup_fn` - Optional cleanup function (e.g., to remove event listeners)
    /// * `target_element` - Element handle for monitoring
    ///
    /// Returns: Unique animation ID
    pub fn register_animation(
        &mut self,
        stop_fn: Box<dyn FnOnce()>,
        cleanup_fn: Option<Box<dyn FnOnce()>>,
        target_element: Option<ElementHandle>,
    ) -> String {
        let id = format!("animation_{}", self.next_id);
        self.next_id += 1;

        self.animations.insert(
            id.clone(),
            AnimationEntry {
                stop_fn,
                cleanup_fn,
                callbacks: Vec::new(),
                _target_element: target_element,
                created_at: std::time::Instant::now(),
            },
        );
        id
    }

    /// Register animation with callbacks
    ///
    /// # Arguments
    ///
    /// * `stop_fn` - Function to call when stopping animation
    /// * `callbacks` - Lifecycle callbacks
    /// * `target_element` - Element handle for monitoring
    pub fn register_with_callbacks(
        &mut self,
        stop_fn: Box<dyn FnOnce()>,
        callbacks: Vec<LifecycleCallback>,
        target_element: Option<ElementHandle>,
    ) -> String {
        let id = format!("animation_{}", self.next_id);
        self.next_id += 1;

        self.animations.insert(
            id.clone(),
            AnimationEntry {
                stop_fn,
                cleanup_fn: None,
                callbacks,
                _target_element: target_element,
                created_at: std::time::Instant::now(),
            },
        );
        id
    }

    /// Check if target element still exists
    ///
    /// Note: In WIT environment, we can't directly check if an element is still in DOM
    /// This would need to be tracked separately or added to the WIT interface
    /// For now, we always return true (assuming element is valid)
    fn is_target_valid(&self, _entry: &AnimationEntry) -> bool {
        // WIT bindings don't provide a way to check if an element is still in DOM
        // This would need to be tracked separately
        true
    }

    /// Stop and remove an animation by ID
    ///
    /// # Arguments
    ///
    /// * `id` - Animation ID to stop
    /// * `reason` - Reason for stopping (for callback invocation)
    pub fn stop_animation(&mut self, id: &str, reason: &str) -> bool {
        if let Some(entry) = self.animations.remove(id) {
            // Collect callbacks to call
            let callbacks_to_call: Vec<_> = entry
                .callbacks
                .into_iter()
                .filter_map(|cb| match cb {
                    LifecycleCallback::OnInterrupt(f) => Some(f),
                    _ => None,
                })
                .collect();

            // Execute stop function
            (entry.stop_fn)();

            // Execute cleanup function if present
            if let Some(cleanup) = entry.cleanup_fn {
                cleanup();
            }

            // Call callbacks after cleanup
            for cb in callbacks_to_call {
                cb();
            }

            eprintln!("🛑 Animation {} stopped: {}", id, reason);
            true
        } else {
            false
        }
    }

    /// Stop all animations
    ///
    /// # Arguments
    ///
    /// * `reason` - Reason for stopping (for callback invocation)
    pub fn stop_all(&mut self, reason: &str) {
        let animations: Vec<(String, AnimationEntry)> = self.animations.drain().collect();

        for (id, entry) in animations {
            // Collect callbacks to call
            let callbacks_to_call: Vec<_> = entry
                .callbacks
                .into_iter()
                .filter_map(|cb| match cb {
                    LifecycleCallback::OnInterrupt(f) => Some(f),
                    _ => None,
                })
                .collect();

            // Execute stop function
            (entry.stop_fn)();

            // Execute cleanup function if present
            if let Some(cleanup) = entry.cleanup_fn {
                cleanup();
            }

            // Call callbacks after cleanup
            for cb in callbacks_to_call {
                cb();
            }

            eprintln!("🛑 Animation {} stopped: {}", id, reason);
        }
    }

    /// Clean up invalid/orphaned animations
    ///
    /// This should be called periodically to remove animations
    /// whose target elements no longer exist
    pub fn cleanup_invalid(&mut self) -> usize {
        let mut to_remove: Vec<String> = Vec::new();

        for (id, entry) in &self.animations {
            if !self.is_target_valid(entry) {
                to_remove.push(id.clone());
            }
        }

        for id in &to_remove {
            self.stop_animation(id, "target element removed");
        }

        if !to_remove.is_empty() {
            eprintln!("🧹 Cleaned up {} invalid animations", to_remove.len());
        }

        to_remove.len()
    }

    /// Check and stop animations that have exceeded timeout
    pub fn cleanup_timed_out(&mut self) -> usize {
        if let Some(timeout_ms) = self.default_timeout_ms {
            let timeout = std::time::Duration::from_millis(timeout_ms);
            let mut to_remove: Vec<String> = Vec::new();
            let now = std::time::Instant::now();

            for (id, entry) in &self.animations {
                if now.duration_since(entry.created_at) > timeout {
                    to_remove.push(id.clone());
                }
            }

            for id in &to_remove {
                self.stop_animation(id, "timeout exceeded");
            }

            to_remove.len()
        } else {
            0
        }
    }

    /// Get count of active animations
    pub fn active_count(&self) -> usize {
        self.animations.len()
    }

    /// Check if any animations are active
    pub fn has_active(&self) -> bool {
        !self.animations.is_empty()
    }

    /// Get all active animation IDs
    pub fn active_ids(&self) -> Vec<String> {
        self.animations.keys().cloned().collect()
    }

    /// Get animation info (for debugging)
    pub fn get_animation_info(&self, id: &str) -> Option<(std::time::Duration, usize)> {
        self.animations.get(id).map(|entry| {
            let duration = std::time::Instant::now().duration_since(entry.created_at);
            let callback_count = entry.callbacks.len();
            (duration, callback_count)
        })
    }
}

impl Default for AnimationRegistry {
    fn default() -> Self {
        Self::new()
    }
}

/// Animation manager for component-level lifecycle management
///
/// This manager provides automatic cleanup of animations when a component
/// unmounts, preventing memory leaks and orphaned animations.
pub struct AnimationManager {
    /// Registry for tracking animations
    registry: AnimationRegistry,
    /// Component IDs tracked by this manager
    component_ids: Vec<String>,
}

impl AnimationManager {
    /// Create a new animation manager
    pub fn new() -> Self {
        Self {
            registry: AnimationRegistry::new(),
            component_ids: Vec::new(),
        }
    }

    /// Create a new animation manager with timeout
    ///
    /// # Arguments
    ///
    /// * `timeout_ms` - Default timeout in milliseconds
    pub fn new_with_timeout(timeout_ms: Option<u64>) -> Self {
        Self {
            registry: AnimationRegistry::new_with_timeout(timeout_ms),
            component_ids: Vec::new(),
        }
    }

    /// Start a managed animation
    ///
    /// The animation will be automatically tracked and cleaned up
    /// when manager is dropped or `cleanup` is called.
    ///
    /// # Arguments
    ///
    /// * `builder` - AnimationBuilder to start
    /// * `cleanup_fn` - Optional cleanup function
    /// * `target_element` - Element handle for monitoring
    ///
    /// Returns: Animation ID for manual control if needed
    pub fn start_animation<P: Platform>(
        &mut self,
        builder: AnimationBuilder<P>,
        cleanup_fn: Option<Box<dyn FnOnce()>>,
        target_element: Option<ElementHandle>,
    ) -> String {
        let stop_fn = builder.start_continuous_animation();
        let id = self
            .registry
            .register_animation(stop_fn, cleanup_fn, target_element);

        // Track this animation
        self.component_ids.push(id.clone());
        id
    }

    /// Start a managed animation with initial state
    ///
    /// # Arguments
    ///
    /// * `elements` - Map of element names to element handles
    /// * `initial_state` - Initial animation state
    /// * `cleanup_fn` - Optional cleanup function
    /// * `target_element` - Element handle for monitoring
    ///
    /// Returns: Animation ID for manual control if needed
    pub fn start_animation_with_state<P: Platform>(
        &mut self,
        _platform: Rc<std::cell::RefCell<P>>,
        _elements: &HashMap<String, ElementHandle>,
        _initial_state: AnimationState,
        _cleanup_fn: Option<Box<dyn FnOnce()>>,
        _target_element: Option<ElementHandle>,
    ) -> String {
        // Note: This is a simplified version
        // The full implementation would create a builder from elements and state
        let id = format!("manual_{}", self.component_ids.len());
        self.component_ids.push(id.clone());
        id
    }

    /// Stop a specific animation
    ///
    /// # Arguments
    ///
    /// * `id` - Animation ID to stop
    pub fn stop_animation(&mut self, id: &str) -> bool {
        let stopped = self.registry.stop_animation(id, "manual stop");
        if stopped {
            // Remove from tracked component IDs
            self.component_ids.retain(|tracked_id| tracked_id != id);
        }
        stopped
    }

    /// Clean up all managed animations
    ///
    /// This is automatically called when manager is dropped.
    pub fn cleanup(&mut self) {
        let ids = self.component_ids.clone();
        for id in ids {
            self.registry.stop_animation(&id, "manager cleanup");
        }
        self.component_ids.clear();
    }

    /// Clean up invalid/orphaned animations
    ///
    /// This should be called periodically to remove animations
    /// whose target elements no longer exist
    pub fn cleanup_invalid(&mut self) -> usize {
        self.registry.cleanup_invalid()
    }

    /// Check and stop animations that have exceeded timeout
    pub fn cleanup_timed_out(&mut self) -> usize {
        self.registry.cleanup_timed_out()
    }

    /// Get count of managed animations
    pub fn managed_count(&self) -> usize {
        self.component_ids.len()
    }

    /// Get active animation IDs
    pub fn managed_ids(&self) -> Vec<String> {
        self.component_ids.clone()
    }

    /// Get animation info (for debugging)
    pub fn get_animation_info(&self, id: &str) -> Option<(std::time::Duration, usize)> {
        self.registry.get_animation_info(id)
    }
}

impl Default for AnimationManager {
    fn default() -> Self {
        Self::new()
    }
}

impl Drop for AnimationManager {
    fn drop(&mut self) {
        self.cleanup();
    }
}

// ===== Re-exports =====

// No explicit re-exports to avoid conflicts