Skip to main content

presentar_core/
runtime.rs

1//! Command runtime for executing side effects.
2//!
3//! This module provides the infrastructure to execute `Command` values
4//! produced by state updates.
5
6use crate::state::Command;
7use std::collections::HashMap;
8use std::sync::{Arc, Mutex};
9
10/// Router trait for navigation commands.
11pub trait Router: Send + Sync {
12    /// Navigate to a route.
13    fn navigate(&self, route: &str);
14
15    /// Get the current route.
16    fn current_route(&self) -> String;
17}
18
19/// Storage trait for state persistence commands.
20pub trait Storage: Send + Sync {
21    /// Save data to storage.
22    fn save(&self, key: &str, data: &[u8]);
23
24    /// Load data from storage.
25    fn load(&self, key: &str) -> Option<Vec<u8>>;
26
27    /// Remove data from storage.
28    fn remove(&self, key: &str);
29
30    /// Check if a key exists.
31    fn contains(&self, key: &str) -> bool;
32}
33
34/// In-memory storage for testing.
35#[derive(Debug, Default)]
36pub struct MemoryStorage {
37    data: Mutex<HashMap<String, Vec<u8>>>,
38}
39
40impl MemoryStorage {
41    /// Create a new empty memory storage.
42    #[must_use]
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Get the number of stored items.
48    #[must_use]
49    pub fn len(&self) -> usize {
50        self.data
51            .lock()
52            .expect("MemoryStorage mutex poisoned")
53            .len()
54    }
55
56    /// Check if storage is empty.
57    #[must_use]
58    pub fn is_empty(&self) -> bool {
59        self.data
60            .lock()
61            .expect("MemoryStorage mutex poisoned")
62            .is_empty()
63    }
64
65    /// Clear all stored data.
66    pub fn clear(&self) {
67        self.data
68            .lock()
69            .expect("MemoryStorage mutex poisoned")
70            .clear();
71    }
72}
73
74impl Storage for MemoryStorage {
75    fn save(&self, key: &str, data: &[u8]) {
76        self.data
77            .lock()
78            .expect("MemoryStorage mutex poisoned")
79            .insert(key.to_string(), data.to_vec());
80    }
81
82    fn load(&self, key: &str) -> Option<Vec<u8>> {
83        self.data
84            .lock()
85            .expect("MemoryStorage mutex poisoned")
86            .get(key)
87            .cloned()
88    }
89
90    fn remove(&self, key: &str) {
91        self.data
92            .lock()
93            .expect("MemoryStorage mutex poisoned")
94            .remove(key);
95    }
96
97    fn contains(&self, key: &str) -> bool {
98        self.data
99            .lock()
100            .expect("MemoryStorage mutex poisoned")
101            .contains_key(key)
102    }
103}
104
105/// In-memory router for testing.
106#[derive(Debug)]
107pub struct MemoryRouter {
108    route: Mutex<String>,
109    history: Mutex<Vec<String>>,
110}
111
112impl Default for MemoryRouter {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118impl MemoryRouter {
119    /// Create a new memory router.
120    #[must_use]
121    pub fn new() -> Self {
122        Self {
123            route: Mutex::new("/".to_string()),
124            history: Mutex::new(vec!["/".to_string()]),
125        }
126    }
127
128    /// Get navigation history.
129    #[must_use]
130    pub fn history(&self) -> Vec<String> {
131        self.history
132            .lock()
133            .expect("MemoryRouter mutex poisoned")
134            .clone()
135    }
136
137    /// Get history length.
138    #[must_use]
139    pub fn history_len(&self) -> usize {
140        self.history
141            .lock()
142            .expect("MemoryRouter mutex poisoned")
143            .len()
144    }
145}
146
147impl Router for MemoryRouter {
148    fn navigate(&self, route: &str) {
149        let mut current = self.route.lock().expect("MemoryRouter mutex poisoned");
150        *current = route.to_string();
151        self.history
152            .lock()
153            .expect("MemoryRouter mutex poisoned")
154            .push(route.to_string());
155    }
156
157    fn current_route(&self) -> String {
158        self.route
159            .lock()
160            .expect("MemoryRouter mutex poisoned")
161            .clone()
162    }
163}
164
165/// Result of command execution.
166#[derive(Debug)]
167pub enum ExecutionResult<M> {
168    /// No result (`Command::None` or non-message-producing commands)
169    None,
170    /// A single message was produced
171    Message(M),
172    /// Multiple messages were produced
173    Messages(Vec<M>),
174    /// Command is pending (async)
175    Pending,
176}
177
178impl<M> ExecutionResult<M> {
179    /// Check if the result has no messages.
180    #[must_use]
181    pub const fn is_none(&self) -> bool {
182        matches!(self, Self::None)
183    }
184
185    /// Check if there are messages.
186    #[must_use]
187    pub const fn has_messages(&self) -> bool {
188        matches!(self, Self::Message(_) | Self::Messages(_))
189    }
190
191    /// Get messages as a vector.
192    pub fn into_messages(self) -> Vec<M> {
193        match self {
194            Self::None | Self::Pending => vec![],
195            Self::Message(m) => vec![m],
196            Self::Messages(ms) => ms,
197        }
198    }
199}
200
201/// Command executor configuration.
202pub struct ExecutorConfig<R, S> {
203    /// Router for navigation commands
204    pub router: Arc<R>,
205    /// Storage for persistence commands
206    pub storage: Arc<S>,
207}
208
209impl<R: Router, S: Storage> ExecutorConfig<R, S> {
210    /// Create a new executor config.
211    pub fn new(router: R, storage: S) -> Self {
212        Self {
213            router: Arc::new(router),
214            storage: Arc::new(storage),
215        }
216    }
217}
218
219/// Command executor for synchronous commands.
220///
221/// Note: Task commands require async execution and return `ExecutionResult::Pending`.
222pub struct CommandExecutor<R, S> {
223    config: ExecutorConfig<R, S>,
224}
225
226impl<R: Router, S: Storage> CommandExecutor<R, S> {
227    /// Create a new command executor.
228    pub const fn new(config: ExecutorConfig<R, S>) -> Self {
229        Self { config }
230    }
231
232    /// Execute a command synchronously.
233    ///
234    /// For async Task commands, this returns `ExecutionResult::Pending`.
235    /// Use `execute_blocking` to block on async tasks.
236    pub fn execute<M: Send>(&self, command: Command<M>) -> ExecutionResult<M> {
237        match command {
238            Command::None => ExecutionResult::None,
239            Command::Batch(commands) => {
240                let mut messages = Vec::new();
241                for cmd in commands {
242                    match self.execute(cmd) {
243                        ExecutionResult::None | ExecutionResult::Pending => {}
244                        ExecutionResult::Message(m) => messages.push(m),
245                        ExecutionResult::Messages(ms) => messages.extend(ms),
246                    }
247                }
248                if messages.is_empty() {
249                    ExecutionResult::None
250                } else {
251                    ExecutionResult::Messages(messages)
252                }
253            }
254            Command::Task(_) => {
255                // Async tasks can't be executed synchronously
256                ExecutionResult::Pending
257            }
258            Command::Navigate { route } => {
259                self.config.router.navigate(&route);
260                ExecutionResult::None
261            }
262            Command::SaveState { key } => {
263                // SaveState requires the actual state to be passed
264                // This is a limitation - we'd need state access
265                // For now, just record that we tried to save
266                // In practice, the runtime would have state access
267                let _ = key;
268                ExecutionResult::None
269            }
270            Command::LoadState { key, on_load } => {
271                let data = self.config.storage.load(&key);
272                let message = on_load(data);
273                ExecutionResult::Message(message)
274            }
275        }
276    }
277
278    /// Get the router.
279    pub fn router(&self) -> &R {
280        &self.config.router
281    }
282
283    /// Get the storage.
284    pub fn storage(&self) -> &S {
285        &self.config.storage
286    }
287}
288
289/// Create a default executor with memory-based backends.
290#[must_use]
291pub fn default_executor() -> CommandExecutor<MemoryRouter, MemoryStorage> {
292    CommandExecutor::new(ExecutorConfig::new(
293        MemoryRouter::new(),
294        MemoryStorage::new(),
295    ))
296}
297
298// =============================================================================
299// Focus Management
300// =============================================================================
301
302/// Focus direction for keyboard navigation.
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
304pub enum FocusDirection {
305    /// Move focus forward (Tab)
306    Forward,
307    /// Move focus backward (Shift+Tab)
308    Backward,
309    /// Move focus up (Arrow Up)
310    Up,
311    /// Move focus down (Arrow Down)
312    Down,
313    /// Move focus left (Arrow Left)
314    Left,
315    /// Move focus right (Arrow Right)
316    Right,
317}
318
319/// Manages keyboard focus for widgets.
320#[derive(Debug, Default)]
321pub struct FocusManager {
322    /// Currently focused widget ID
323    focused: Option<u64>,
324    /// Focus ring (ordered list of focusable widget IDs)
325    focus_ring: Vec<u64>,
326    /// Focus trap stack (for modals/dialogs)
327    traps: Vec<FocusTrap>,
328}
329
330/// A focus trap that restricts focus to a subset of widgets.
331#[derive(Debug)]
332pub struct FocusTrap {
333    /// Widget IDs in this trap
334    pub widget_ids: Vec<u64>,
335    /// Initial focused widget when trap was created
336    pub initial_focus: Option<u64>,
337}
338
339impl FocusManager {
340    /// Create a new focus manager.
341    #[must_use]
342    pub fn new() -> Self {
343        Self::default()
344    }
345
346    /// Set the focus ring (ordered list of focusable widgets).
347    pub fn set_focus_ring(&mut self, widget_ids: Vec<u64>) {
348        self.focus_ring = widget_ids;
349    }
350
351    /// Get the currently focused widget ID.
352    #[must_use]
353    pub const fn focused(&self) -> Option<u64> {
354        self.focused
355    }
356
357    /// Set focus to a specific widget.
358    pub fn focus(&mut self, widget_id: u64) -> bool {
359        let available = self.available_focus_ring();
360        if available.contains(&widget_id) {
361            self.focused = Some(widget_id);
362            true
363        } else {
364            false
365        }
366    }
367
368    /// Clear focus.
369    pub fn blur(&mut self) {
370        self.focused = None;
371    }
372
373    /// Move focus in a direction.
374    pub fn move_focus(&mut self, direction: FocusDirection) -> Option<u64> {
375        let ring = self.available_focus_ring();
376        if ring.is_empty() {
377            return None;
378        }
379
380        let current_idx = self
381            .focused
382            .and_then(|f| ring.iter().position(|&id| id == f));
383
384        let next_idx = match direction {
385            FocusDirection::Forward | FocusDirection::Down | FocusDirection::Right => {
386                match current_idx {
387                    Some(idx) => (idx + 1) % ring.len(),
388                    None => 0,
389                }
390            }
391            FocusDirection::Backward | FocusDirection::Up | FocusDirection::Left => {
392                match current_idx {
393                    Some(0) | None => ring.len() - 1,
394                    Some(idx) => idx - 1,
395                }
396            }
397        };
398
399        let next_id = ring[next_idx];
400        self.focused = Some(next_id);
401        Some(next_id)
402    }
403
404    /// Push a focus trap (for modals/dialogs).
405    pub fn push_trap(&mut self, widget_ids: Vec<u64>) {
406        let initial = self.focused;
407        self.traps.push(FocusTrap {
408            widget_ids,
409            initial_focus: initial,
410        });
411        // Focus first item in trap
412        if let Some(first) = self.available_focus_ring().first().copied() {
413            self.focused = Some(first);
414        }
415    }
416
417    /// Pop the current focus trap.
418    pub fn pop_trap(&mut self) -> Option<FocusTrap> {
419        let trap = self.traps.pop();
420        // Restore previous focus
421        if let Some(ref t) = trap {
422            self.focused = t.initial_focus;
423        }
424        trap
425    }
426
427    /// Check if focus is currently trapped.
428    #[must_use]
429    pub fn is_trapped(&self) -> bool {
430        !self.traps.is_empty()
431    }
432
433    /// Get the available focus ring (respecting traps).
434    fn available_focus_ring(&self) -> Vec<u64> {
435        if let Some(trap) = self.traps.last() {
436            trap.widget_ids.clone()
437        } else {
438            self.focus_ring.clone()
439        }
440    }
441
442    /// Check if a widget is focusable.
443    #[must_use]
444    pub fn is_focusable(&self, widget_id: u64) -> bool {
445        self.available_focus_ring().contains(&widget_id)
446    }
447}
448
449// =============================================================================
450// Animation & Timer System
451// =============================================================================
452
453/// Easing functions for smooth animations.
454#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
455pub enum EasingFunction {
456    /// Linear interpolation (no easing)
457    #[default]
458    Linear,
459    /// Quadratic ease in
460    EaseInQuad,
461    /// Quadratic ease out
462    EaseOutQuad,
463    /// Quadratic ease in-out
464    EaseInOutQuad,
465    /// Cubic ease in
466    EaseInCubic,
467    /// Cubic ease out
468    EaseOutCubic,
469    /// Cubic ease in-out
470    EaseInOutCubic,
471    /// Elastic ease out (spring-like)
472    EaseOutElastic,
473    /// Bounce ease out
474    EaseOutBounce,
475}
476
477impl EasingFunction {
478    /// Apply the easing function to a normalized time value (0.0 to 1.0).
479    #[must_use]
480    #[allow(clippy::suboptimal_flops)]
481    pub fn apply(self, t: f32) -> f32 {
482        let t = t.clamp(0.0, 1.0);
483        match self {
484            Self::Linear => t,
485            Self::EaseInQuad => t * t,
486            Self::EaseOutQuad => 1.0 - (1.0 - t) * (1.0 - t),
487            Self::EaseInOutQuad => {
488                if t < 0.5 {
489                    2.0 * t * t
490                } else {
491                    1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
492                }
493            }
494            Self::EaseInCubic => t * t * t,
495            Self::EaseOutCubic => 1.0 - (1.0 - t).powi(3),
496            Self::EaseInOutCubic => {
497                if t < 0.5 {
498                    4.0 * t * t * t
499                } else {
500                    1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
501                }
502            }
503            Self::EaseOutElastic => {
504                if t == 0.0 || t == 1.0 {
505                    t
506                } else {
507                    let c4 = (2.0 * std::f32::consts::PI) / 3.0;
508                    2.0_f32.powf(-10.0 * t) * ((t * 10.0 - 0.75) * c4).sin() + 1.0
509                }
510            }
511            Self::EaseOutBounce => {
512                let n1 = 7.5625;
513                let d1 = 2.75;
514                if t < 1.0 / d1 {
515                    n1 * t * t
516                } else if t < 2.0 / d1 {
517                    let t = t - 1.5 / d1;
518                    n1 * t * t + 0.75
519                } else if t < 2.5 / d1 {
520                    let t = t - 2.25 / d1;
521                    n1 * t * t + 0.9375
522                } else {
523                    let t = t - 2.625 / d1;
524                    n1 * t * t + 0.984_375
525                }
526            }
527        }
528    }
529}
530
531/// A tween that interpolates between two values over time.
532#[derive(Debug, Clone)]
533pub struct Tween<T> {
534    /// Starting value
535    pub from: T,
536    /// Ending value
537    pub to: T,
538    /// Duration in milliseconds
539    pub duration_ms: u32,
540    /// Easing function
541    pub easing: EasingFunction,
542    /// Current elapsed time in milliseconds
543    elapsed_ms: u32,
544}
545
546impl<T: Clone> Tween<T> {
547    /// Create a new tween.
548    pub fn new(from: T, to: T, duration_ms: u32) -> Self {
549        Self {
550            from,
551            to,
552            duration_ms,
553            easing: EasingFunction::default(),
554            elapsed_ms: 0,
555        }
556    }
557
558    /// Set the easing function.
559    #[must_use]
560    pub const fn with_easing(mut self, easing: EasingFunction) -> Self {
561        self.easing = easing;
562        self
563    }
564
565    /// Get the normalized progress (0.0 to 1.0).
566    #[must_use]
567    pub fn progress(&self) -> f32 {
568        if self.duration_ms == 0 {
569            1.0
570        } else {
571            (self.elapsed_ms as f32 / self.duration_ms as f32).min(1.0)
572        }
573    }
574
575    /// Get the eased progress value.
576    #[must_use]
577    pub fn eased_progress(&self) -> f32 {
578        self.easing.apply(self.progress())
579    }
580
581    /// Check if the tween is complete.
582    #[must_use]
583    pub const fn is_complete(&self) -> bool {
584        self.elapsed_ms >= self.duration_ms
585    }
586
587    /// Advance the tween by delta milliseconds.
588    pub fn advance(&mut self, delta_ms: u32) {
589        self.elapsed_ms = self
590            .elapsed_ms
591            .saturating_add(delta_ms)
592            .min(self.duration_ms);
593    }
594
595    /// Reset the tween to the beginning.
596    pub fn reset(&mut self) {
597        self.elapsed_ms = 0;
598    }
599}
600
601impl Tween<f32> {
602    /// Get the current interpolated value.
603    #[must_use]
604    #[allow(clippy::suboptimal_flops)]
605    pub fn value(&self) -> f32 {
606        let t = self.eased_progress();
607        self.from + (self.to - self.from) * t
608    }
609}
610
611impl Tween<f64> {
612    /// Get the current interpolated value.
613    #[must_use]
614    #[allow(clippy::suboptimal_flops)]
615    pub fn value(&self) -> f64 {
616        let t = f64::from(self.eased_progress());
617        self.from + (self.to - self.from) * t
618    }
619}
620
621/// Animation state for tracking animation lifecycle.
622#[derive(Debug, Clone, Copy, PartialEq, Eq)]
623pub enum AnimationState {
624    /// Animation is idle/not started
625    Idle,
626    /// Animation is running
627    Running,
628    /// Animation is paused
629    Paused,
630    /// Animation has completed
631    Completed,
632}
633
634/// Unique identifier for an animation.
635pub type AnimationId = u64;
636
637/// An animation instance that can be managed by an Animator.
638#[derive(Debug)]
639pub struct AnimationInstance {
640    /// Unique ID
641    pub id: AnimationId,
642    /// Tween for the animation
643    pub tween: Tween<f32>,
644    /// Current state
645    pub state: AnimationState,
646    /// Loop count (0 = infinite, 1 = once, N = N times)
647    pub loop_count: u32,
648    /// Current loop iteration
649    pub current_loop: u32,
650    /// Whether to reverse on alternate loops (ping-pong)
651    pub alternate: bool,
652    /// Direction (true = forward, false = reverse)
653    forward: bool,
654}
655
656impl AnimationInstance {
657    /// Create a new animation instance.
658    pub fn new(id: AnimationId, from: f32, to: f32, duration_ms: u32) -> Self {
659        Self {
660            id,
661            tween: Tween::new(from, to, duration_ms),
662            state: AnimationState::Idle,
663            loop_count: 1,
664            current_loop: 0,
665            alternate: false,
666            forward: true,
667        }
668    }
669
670    /// Set easing function.
671    #[must_use]
672    pub const fn with_easing(mut self, easing: EasingFunction) -> Self {
673        self.tween = self.tween.with_easing(easing);
674        self
675    }
676
677    /// Set loop count (0 = infinite).
678    #[must_use]
679    pub const fn with_loop_count(mut self, count: u32) -> Self {
680        self.loop_count = count;
681        self
682    }
683
684    /// Enable ping-pong alternating.
685    #[must_use]
686    pub const fn with_alternate(mut self, alternate: bool) -> Self {
687        self.alternate = alternate;
688        self
689    }
690
691    /// Start the animation.
692    pub fn start(&mut self) {
693        self.state = AnimationState::Running;
694        self.current_loop = 0;
695        self.forward = true;
696        self.tween.reset();
697    }
698
699    /// Pause the animation.
700    pub fn pause(&mut self) {
701        if self.state == AnimationState::Running {
702            self.state = AnimationState::Paused;
703        }
704    }
705
706    /// Resume the animation.
707    pub fn resume(&mut self) {
708        if self.state == AnimationState::Paused {
709            self.state = AnimationState::Running;
710        }
711    }
712
713    /// Stop the animation.
714    pub fn stop(&mut self) {
715        self.state = AnimationState::Idle;
716        self.tween.reset();
717    }
718
719    /// Get the current value.
720    #[must_use]
721    #[allow(clippy::suboptimal_flops)]
722    pub fn value(&self) -> f32 {
723        if self.forward {
724            self.tween.value()
725        } else {
726            self.tween.from
727                + (self.tween.to - self.tween.from) * (1.0 - self.tween.eased_progress())
728        }
729    }
730
731    /// Advance the animation by delta milliseconds.
732    pub fn advance(&mut self, delta_ms: u32) {
733        if self.state != AnimationState::Running {
734            return;
735        }
736
737        self.tween.advance(delta_ms);
738
739        if self.tween.is_complete() {
740            // Handle looping
741            if self.loop_count == 0 || self.current_loop + 1 < self.loop_count {
742                self.current_loop += 1;
743                self.tween.reset();
744
745                if self.alternate {
746                    self.forward = !self.forward;
747                }
748            } else {
749                self.state = AnimationState::Completed;
750            }
751        }
752    }
753}
754
755/// Manages multiple animations.
756#[derive(Debug, Default)]
757pub struct Animator {
758    animations: Vec<AnimationInstance>,
759    next_id: AnimationId,
760}
761
762impl Animator {
763    /// Create a new animator.
764    #[must_use]
765    pub fn new() -> Self {
766        Self::default()
767    }
768
769    /// Create a new animation and return its ID.
770    pub fn create(&mut self, from: f32, to: f32, duration_ms: u32) -> AnimationId {
771        let id = self.next_id;
772        self.next_id += 1;
773        self.animations
774            .push(AnimationInstance::new(id, from, to, duration_ms));
775        id
776    }
777
778    /// Get an animation by ID.
779    #[must_use]
780    pub fn get(&self, id: AnimationId) -> Option<&AnimationInstance> {
781        self.animations.iter().find(|a| a.id == id)
782    }
783
784    /// Get a mutable animation by ID.
785    pub fn get_mut(&mut self, id: AnimationId) -> Option<&mut AnimationInstance> {
786        self.animations.iter_mut().find(|a| a.id == id)
787    }
788
789    /// Start an animation.
790    pub fn start(&mut self, id: AnimationId) {
791        if let Some(anim) = self.get_mut(id) {
792            anim.start();
793        }
794    }
795
796    /// Pause an animation.
797    pub fn pause(&mut self, id: AnimationId) {
798        if let Some(anim) = self.get_mut(id) {
799            anim.pause();
800        }
801    }
802
803    /// Resume an animation.
804    pub fn resume(&mut self, id: AnimationId) {
805        if let Some(anim) = self.get_mut(id) {
806            anim.resume();
807        }
808    }
809
810    /// Stop an animation.
811    pub fn stop(&mut self, id: AnimationId) {
812        if let Some(anim) = self.get_mut(id) {
813            anim.stop();
814        }
815    }
816
817    /// Remove an animation.
818    pub fn remove(&mut self, id: AnimationId) {
819        self.animations.retain(|a| a.id != id);
820    }
821
822    /// Advance all animations by delta milliseconds.
823    pub fn advance(&mut self, delta_ms: u32) {
824        for anim in &mut self.animations {
825            anim.advance(delta_ms);
826        }
827    }
828
829    /// Get the value of an animation.
830    #[must_use]
831    pub fn value(&self, id: AnimationId) -> Option<f32> {
832        self.get(id).map(AnimationInstance::value)
833    }
834
835    /// Get the number of animations.
836    #[must_use]
837    pub fn len(&self) -> usize {
838        self.animations.len()
839    }
840
841    /// Check if there are no animations.
842    #[must_use]
843    pub fn is_empty(&self) -> bool {
844        self.animations.is_empty()
845    }
846
847    /// Remove all completed animations.
848    pub fn cleanup_completed(&mut self) {
849        self.animations
850            .retain(|a| a.state != AnimationState::Completed);
851    }
852
853    /// Check if any animations are running.
854    #[must_use]
855    pub fn has_running(&self) -> bool {
856        self.animations
857            .iter()
858            .any(|a| a.state == AnimationState::Running)
859    }
860}
861
862/// A timer that fires at regular intervals.
863#[derive(Debug)]
864pub struct Timer {
865    /// Interval in milliseconds
866    pub interval_ms: u32,
867    /// Elapsed time since last tick
868    elapsed_ms: u32,
869    /// Whether the timer is running
870    running: bool,
871    /// Number of times the timer has fired
872    tick_count: u64,
873    /// Optional limit on tick count (0 = unlimited)
874    max_ticks: u64,
875}
876
877impl Timer {
878    /// Create a new timer with the given interval.
879    #[must_use]
880    pub const fn new(interval_ms: u32) -> Self {
881        Self {
882            interval_ms,
883            elapsed_ms: 0,
884            running: false,
885            tick_count: 0,
886            max_ticks: 0,
887        }
888    }
889
890    /// Set maximum tick count (0 = unlimited).
891    #[must_use]
892    pub const fn with_max_ticks(mut self, max: u64) -> Self {
893        self.max_ticks = max;
894        self
895    }
896
897    /// Start the timer.
898    pub fn start(&mut self) {
899        self.running = true;
900    }
901
902    /// Stop the timer.
903    pub fn stop(&mut self) {
904        self.running = false;
905    }
906
907    /// Reset the timer.
908    pub fn reset(&mut self) {
909        self.elapsed_ms = 0;
910        self.tick_count = 0;
911    }
912
913    /// Check if the timer is running.
914    #[must_use]
915    pub const fn is_running(&self) -> bool {
916        self.running
917    }
918
919    /// Get the tick count.
920    #[must_use]
921    pub const fn tick_count(&self) -> u64 {
922        self.tick_count
923    }
924
925    /// Advance the timer and return the number of ticks that occurred.
926    pub fn advance(&mut self, delta_ms: u32) -> u32 {
927        if !self.running || self.interval_ms == 0 {
928            return 0;
929        }
930
931        self.elapsed_ms += delta_ms;
932        let ticks = self.elapsed_ms / self.interval_ms;
933        self.elapsed_ms %= self.interval_ms;
934
935        // Apply ticks with limit check
936        let mut actual_ticks = 0;
937        for _ in 0..ticks {
938            if self.max_ticks > 0 && self.tick_count >= self.max_ticks {
939                self.running = false;
940                break;
941            }
942            self.tick_count += 1;
943            actual_ticks += 1;
944        }
945
946        actual_ticks
947    }
948
949    /// Get progress to next tick (0.0 to 1.0).
950    #[must_use]
951    pub fn progress(&self) -> f32 {
952        if self.interval_ms == 0 {
953            0.0
954        } else {
955            self.elapsed_ms as f32 / self.interval_ms as f32
956        }
957    }
958}
959
960/// Frame timer for 60fps animations.
961#[derive(Debug)]
962pub struct FrameTimer {
963    /// Target frame duration in microseconds (16667 for 60fps)
964    target_frame_us: u64,
965    /// Last frame timestamp in microseconds
966    last_frame_us: Option<u64>,
967    /// Accumulated frame time for averaging
968    frame_times: [u64; 60],
969    /// Current frame index
970    frame_index: usize,
971    /// Number of recorded frame deltas
972    delta_count: usize,
973    /// Total frames rendered
974    total_frames: u64,
975}
976
977impl Default for FrameTimer {
978    fn default() -> Self {
979        Self::new(60)
980    }
981}
982
983impl FrameTimer {
984    /// Create a new frame timer with target FPS.
985    #[must_use]
986    pub fn new(target_fps: u32) -> Self {
987        let target_frame_us = if target_fps > 0 {
988            1_000_000 / u64::from(target_fps)
989        } else {
990            16667
991        };
992        Self {
993            target_frame_us,
994            last_frame_us: None,
995            frame_times: [0; 60],
996            frame_index: 0,
997            delta_count: 0,
998            total_frames: 0,
999        }
1000    }
1001
1002    /// Record a frame with the current timestamp in microseconds.
1003    pub fn frame(&mut self, now_us: u64) {
1004        if let Some(last) = self.last_frame_us {
1005            let delta = now_us.saturating_sub(last);
1006            self.frame_times[self.frame_index] = delta;
1007            self.frame_index = (self.frame_index + 1) % 60;
1008            self.delta_count = (self.delta_count + 1).min(60);
1009        }
1010        self.last_frame_us = Some(now_us);
1011        self.total_frames += 1;
1012    }
1013
1014    /// Get the average frame time in microseconds.
1015    #[must_use]
1016    pub fn average_frame_time_us(&self) -> u64 {
1017        if self.delta_count == 0 {
1018            return self.target_frame_us;
1019        }
1020        let sum: u64 = self.frame_times[..self.delta_count].iter().sum();
1021        sum / self.delta_count as u64
1022    }
1023
1024    /// Get the current FPS.
1025    #[must_use]
1026    pub fn fps(&self) -> f32 {
1027        let avg = self.average_frame_time_us();
1028        if avg == 0 {
1029            0.0
1030        } else {
1031            1_000_000.0 / avg as f32
1032        }
1033    }
1034
1035    /// Check if we're hitting target FPS (within 10% tolerance).
1036    #[must_use]
1037    pub fn is_on_target(&self) -> bool {
1038        let avg = self.average_frame_time_us();
1039        let target = self.target_frame_us;
1040        // Within 10% of target
1041        avg <= target + target / 10
1042    }
1043
1044    /// Get the target frame time in milliseconds.
1045    #[must_use]
1046    pub fn target_frame_ms(&self) -> f32 {
1047        self.target_frame_us as f32 / 1000.0
1048    }
1049
1050    /// Get total frames rendered.
1051    #[must_use]
1052    pub const fn total_frames(&self) -> u64 {
1053        self.total_frames
1054    }
1055}
1056
1057// =============================================================================
1058// Data Refresh Manager
1059// =============================================================================
1060
1061/// Manages periodic data refresh for data sources.
1062#[derive(Debug)]
1063pub struct DataRefreshManager {
1064    /// Registered refresh tasks
1065    tasks: Vec<RefreshTask>,
1066    /// Current timestamp in milliseconds
1067    current_time_ms: u64,
1068}
1069
1070/// A scheduled data refresh task.
1071#[derive(Debug, Clone)]
1072pub struct RefreshTask {
1073    /// Data source key
1074    pub key: String,
1075    /// Refresh interval in milliseconds
1076    pub interval_ms: u64,
1077    /// Last refresh timestamp
1078    pub last_refresh_ms: u64,
1079    /// Whether task is active
1080    pub active: bool,
1081}
1082
1083impl DataRefreshManager {
1084    /// Create a new refresh manager.
1085    #[must_use]
1086    pub const fn new() -> Self {
1087        Self {
1088            tasks: Vec::new(),
1089            current_time_ms: 0,
1090        }
1091    }
1092
1093    /// Register a data source for periodic refresh.
1094    ///
1095    /// # Arguments
1096    ///
1097    /// * `key` - Data source identifier
1098    /// * `interval_ms` - Refresh interval in milliseconds
1099    pub fn register(&mut self, key: impl Into<String>, interval_ms: u64) {
1100        let key = key.into();
1101
1102        // Check if already registered
1103        if let Some(task) = self.tasks.iter_mut().find(|t| t.key == key) {
1104            task.interval_ms = interval_ms;
1105            task.active = true;
1106            return;
1107        }
1108
1109        self.tasks.push(RefreshTask {
1110            key,
1111            interval_ms,
1112            last_refresh_ms: 0,
1113            active: true,
1114        });
1115    }
1116
1117    /// Unregister a data source.
1118    pub fn unregister(&mut self, key: &str) {
1119        self.tasks.retain(|t| t.key != key);
1120    }
1121
1122    /// Pause refresh for a data source.
1123    pub fn pause(&mut self, key: &str) {
1124        if let Some(task) = self.tasks.iter_mut().find(|t| t.key == key) {
1125            task.active = false;
1126        }
1127    }
1128
1129    /// Resume refresh for a data source.
1130    pub fn resume(&mut self, key: &str) {
1131        if let Some(task) = self.tasks.iter_mut().find(|t| t.key == key) {
1132            task.active = true;
1133        }
1134    }
1135
1136    /// Update the manager with the current timestamp.
1137    ///
1138    /// Returns keys of data sources that need to be refreshed.
1139    pub fn update(&mut self, current_time_ms: u64) -> Vec<String> {
1140        self.current_time_ms = current_time_ms;
1141
1142        let mut to_refresh = Vec::new();
1143
1144        for task in &mut self.tasks {
1145            if !task.active {
1146                continue;
1147            }
1148
1149            let elapsed = current_time_ms.saturating_sub(task.last_refresh_ms);
1150            if elapsed >= task.interval_ms {
1151                to_refresh.push(task.key.clone());
1152                task.last_refresh_ms = current_time_ms;
1153            }
1154        }
1155
1156        to_refresh
1157    }
1158
1159    /// Force immediate refresh of a data source.
1160    pub fn force_refresh(&mut self, key: &str) -> bool {
1161        if let Some(task) = self.tasks.iter_mut().find(|t| t.key == key) {
1162            task.last_refresh_ms = 0;
1163            true
1164        } else {
1165            false
1166        }
1167    }
1168
1169    /// Get all registered tasks.
1170    #[must_use]
1171    pub fn tasks(&self) -> &[RefreshTask] {
1172        &self.tasks
1173    }
1174
1175    /// Get task by key.
1176    #[must_use]
1177    pub fn get_task(&self, key: &str) -> Option<&RefreshTask> {
1178        self.tasks.iter().find(|t| t.key == key)
1179    }
1180
1181    /// Check if a data source is due for refresh.
1182    #[must_use]
1183    pub fn is_due(&self, key: &str) -> bool {
1184        if let Some(task) = self.tasks.iter().find(|t| t.key == key) {
1185            if !task.active {
1186                return false;
1187            }
1188            let elapsed = self.current_time_ms.saturating_sub(task.last_refresh_ms);
1189            elapsed >= task.interval_ms
1190        } else {
1191            false
1192        }
1193    }
1194
1195    /// Get time until next refresh for a data source (in ms).
1196    #[must_use]
1197    pub fn time_until_refresh(&self, key: &str) -> Option<u64> {
1198        self.tasks.iter().find(|t| t.key == key).map(|task| {
1199            if !task.active {
1200                return u64::MAX;
1201            }
1202            let elapsed = self.current_time_ms.saturating_sub(task.last_refresh_ms);
1203            task.interval_ms.saturating_sub(elapsed)
1204        })
1205    }
1206}
1207
1208impl Default for DataRefreshManager {
1209    fn default() -> Self {
1210        Self::new()
1211    }
1212}
1213
1214// =============================================================================
1215// Widget Animation API
1216// =============================================================================
1217
1218/// Configuration for property transitions.
1219#[derive(Debug, Clone)]
1220pub struct TransitionConfig {
1221    /// Duration in milliseconds
1222    pub duration_ms: u32,
1223    /// Easing function
1224    pub easing: EasingFunction,
1225    /// Delay before starting in milliseconds
1226    pub delay_ms: u32,
1227}
1228
1229impl Default for TransitionConfig {
1230    fn default() -> Self {
1231        Self {
1232            duration_ms: 300,
1233            easing: EasingFunction::EaseInOutCubic,
1234            delay_ms: 0,
1235        }
1236    }
1237}
1238
1239impl TransitionConfig {
1240    /// Create a new transition configuration.
1241    #[must_use]
1242    pub const fn new(duration_ms: u32) -> Self {
1243        Self {
1244            duration_ms,
1245            easing: EasingFunction::EaseInOutCubic,
1246            delay_ms: 0,
1247        }
1248    }
1249
1250    /// Set the easing function.
1251    #[must_use]
1252    pub const fn with_easing(mut self, easing: EasingFunction) -> Self {
1253        self.easing = easing;
1254        self
1255    }
1256
1257    /// Set the delay.
1258    #[must_use]
1259    pub const fn with_delay(mut self, delay_ms: u32) -> Self {
1260        self.delay_ms = delay_ms;
1261        self
1262    }
1263
1264    /// Quick preset (150ms)
1265    #[must_use]
1266    pub const fn quick() -> Self {
1267        Self::new(150)
1268    }
1269
1270    /// Normal preset (300ms)
1271    #[must_use]
1272    pub const fn normal() -> Self {
1273        Self::new(300)
1274    }
1275
1276    /// Slow preset (500ms)
1277    #[must_use]
1278    pub const fn slow() -> Self {
1279        Self::new(500)
1280    }
1281}
1282
1283/// An animated property that smoothly transitions between values.
1284///
1285/// Use this in widget state to animate property changes automatically.
1286#[derive(Debug, Clone)]
1287pub struct AnimatedProperty<T> {
1288    /// Current visual value (what's rendered)
1289    current: T,
1290    /// Target value we're animating towards
1291    target: T,
1292    /// Starting value of current animation
1293    start: T,
1294    /// Transition configuration
1295    config: TransitionConfig,
1296    /// Elapsed time in milliseconds
1297    elapsed_ms: u32,
1298    /// Whether an animation is in progress
1299    animating: bool,
1300}
1301
1302impl<T: Clone + Default> Default for AnimatedProperty<T> {
1303    fn default() -> Self {
1304        Self::new(T::default())
1305    }
1306}
1307
1308impl<T: Clone> AnimatedProperty<T> {
1309    /// Create a new animated property with an initial value.
1310    pub fn new(value: T) -> Self {
1311        Self {
1312            current: value.clone(),
1313            target: value.clone(),
1314            start: value,
1315            config: TransitionConfig::default(),
1316            elapsed_ms: 0,
1317            animating: false,
1318        }
1319    }
1320
1321    /// Create with a custom transition config.
1322    pub fn with_config(value: T, config: TransitionConfig) -> Self {
1323        Self {
1324            current: value.clone(),
1325            target: value.clone(),
1326            start: value,
1327            config,
1328            elapsed_ms: 0,
1329            animating: false,
1330        }
1331    }
1332
1333    /// Get the current visual value.
1334    pub const fn get(&self) -> &T {
1335        &self.current
1336    }
1337
1338    /// Get the target value.
1339    pub const fn target(&self) -> &T {
1340        &self.target
1341    }
1342
1343    /// Check if currently animating.
1344    #[must_use]
1345    pub const fn is_animating(&self) -> bool {
1346        self.animating
1347    }
1348
1349    /// Set a new target value, starting an animation.
1350    pub fn set(&mut self, value: T) {
1351        self.start = self.current.clone();
1352        self.target = value;
1353        self.elapsed_ms = 0;
1354        self.animating = true;
1355    }
1356
1357    /// Set value immediately without animation.
1358    pub fn set_immediate(&mut self, value: T) {
1359        self.current = value.clone();
1360        self.target = value.clone();
1361        self.start = value;
1362        self.animating = false;
1363        self.elapsed_ms = 0;
1364    }
1365
1366    /// Get animation progress (0.0 to 1.0).
1367    #[must_use]
1368    pub fn progress(&self) -> f32 {
1369        if !self.animating {
1370            return 1.0;
1371        }
1372
1373        let total = self.config.duration_ms + self.config.delay_ms;
1374        if total == 0 {
1375            return 1.0;
1376        }
1377
1378        if self.elapsed_ms < self.config.delay_ms {
1379            return 0.0;
1380        }
1381
1382        let elapsed_after_delay = self.elapsed_ms - self.config.delay_ms;
1383        (elapsed_after_delay as f32 / self.config.duration_ms as f32).min(1.0)
1384    }
1385
1386    /// Get eased progress.
1387    #[must_use]
1388    pub fn eased_progress(&self) -> f32 {
1389        self.config.easing.apply(self.progress())
1390    }
1391}
1392
1393impl AnimatedProperty<f32> {
1394    /// Advance the animation by delta milliseconds.
1395    pub fn advance(&mut self, delta_ms: u32) {
1396        if !self.animating {
1397            return;
1398        }
1399
1400        self.elapsed_ms += delta_ms;
1401
1402        let t = self.eased_progress();
1403        self.current = (self.target - self.start).mul_add(t, self.start);
1404
1405        if self.progress() >= 1.0 {
1406            self.current = self.target;
1407            self.animating = false;
1408        }
1409    }
1410}
1411
1412impl AnimatedProperty<f64> {
1413    /// Advance the animation by delta milliseconds.
1414    pub fn advance(&mut self, delta_ms: u32) {
1415        if !self.animating {
1416            return;
1417        }
1418
1419        self.elapsed_ms += delta_ms;
1420
1421        let t = f64::from(self.eased_progress());
1422        self.current = (self.target - self.start).mul_add(t, self.start);
1423
1424        if self.progress() >= 1.0 {
1425            self.current = self.target;
1426            self.animating = false;
1427        }
1428    }
1429}
1430
1431impl AnimatedProperty<crate::Color> {
1432    /// Advance the animation by delta milliseconds.
1433    pub fn advance(&mut self, delta_ms: u32) {
1434        if !self.animating {
1435            return;
1436        }
1437
1438        self.elapsed_ms += delta_ms;
1439
1440        let t = self.eased_progress();
1441        self.current = crate::Color {
1442            r: (self.target.r - self.start.r).mul_add(t, self.start.r),
1443            g: (self.target.g - self.start.g).mul_add(t, self.start.g),
1444            b: (self.target.b - self.start.b).mul_add(t, self.start.b),
1445            a: (self.target.a - self.start.a).mul_add(t, self.start.a),
1446        };
1447
1448        if self.progress() >= 1.0 {
1449            self.current = self.target;
1450            self.animating = false;
1451        }
1452    }
1453}
1454
1455impl AnimatedProperty<crate::Point> {
1456    /// Advance the animation by delta milliseconds.
1457    pub fn advance(&mut self, delta_ms: u32) {
1458        if !self.animating {
1459            return;
1460        }
1461
1462        self.elapsed_ms += delta_ms;
1463
1464        let t = self.eased_progress();
1465        self.current = crate::Point {
1466            x: (self.target.x - self.start.x).mul_add(t, self.start.x),
1467            y: (self.target.y - self.start.y).mul_add(t, self.start.y),
1468        };
1469
1470        if self.progress() >= 1.0 {
1471            self.current = self.target;
1472            self.animating = false;
1473        }
1474    }
1475}
1476
1477impl AnimatedProperty<crate::Size> {
1478    /// Advance the animation by delta milliseconds.
1479    pub fn advance(&mut self, delta_ms: u32) {
1480        if !self.animating {
1481            return;
1482        }
1483
1484        self.elapsed_ms += delta_ms;
1485
1486        let t = self.eased_progress();
1487        self.current = crate::Size {
1488            width: (self.target.width - self.start.width).mul_add(t, self.start.width),
1489            height: (self.target.height - self.start.height).mul_add(t, self.start.height),
1490        };
1491
1492        if self.progress() >= 1.0 {
1493            self.current = self.target;
1494            self.animating = false;
1495        }
1496    }
1497}
1498
1499/// Spring animation configuration.
1500#[derive(Debug, Clone, Copy)]
1501pub struct SpringConfig {
1502    /// Spring stiffness (higher = faster oscillation)
1503    pub stiffness: f32,
1504    /// Damping (higher = less oscillation)
1505    pub damping: f32,
1506    /// Mass of the object
1507    pub mass: f32,
1508}
1509
1510impl Default for SpringConfig {
1511    fn default() -> Self {
1512        Self {
1513            stiffness: 100.0,
1514            damping: 10.0,
1515            mass: 1.0,
1516        }
1517    }
1518}
1519
1520impl SpringConfig {
1521    /// Create a new spring configuration.
1522    #[must_use]
1523    pub const fn new(stiffness: f32, damping: f32, mass: f32) -> Self {
1524        Self {
1525            stiffness,
1526            damping,
1527            mass,
1528        }
1529    }
1530
1531    /// Gentle spring preset.
1532    #[must_use]
1533    pub const fn gentle() -> Self {
1534        Self::new(100.0, 15.0, 1.0)
1535    }
1536
1537    /// Bouncy spring preset.
1538    #[must_use]
1539    pub const fn bouncy() -> Self {
1540        Self::new(300.0, 10.0, 1.0)
1541    }
1542
1543    /// Stiff spring preset.
1544    #[must_use]
1545    pub const fn stiff() -> Self {
1546        Self::new(500.0, 30.0, 1.0)
1547    }
1548}
1549
1550/// Spring-based animation for physics-like motion.
1551#[derive(Debug, Clone)]
1552pub struct SpringAnimation {
1553    /// Current position
1554    position: f32,
1555    /// Current velocity
1556    velocity: f32,
1557    /// Target position
1558    target: f32,
1559    /// Spring configuration
1560    config: SpringConfig,
1561    /// Velocity threshold for considering animation complete
1562    velocity_threshold: f32,
1563    /// Position threshold for considering animation complete
1564    position_threshold: f32,
1565}
1566
1567impl SpringAnimation {
1568    /// Create a new spring animation.
1569    #[must_use]
1570    pub fn new(initial: f32) -> Self {
1571        Self {
1572            position: initial,
1573            velocity: 0.0,
1574            target: initial,
1575            config: SpringConfig::default(),
1576            velocity_threshold: 0.01,
1577            position_threshold: 0.001,
1578        }
1579    }
1580
1581    /// Create with custom spring config.
1582    #[must_use]
1583    pub const fn with_config(initial: f32, config: SpringConfig) -> Self {
1584        Self {
1585            position: initial,
1586            velocity: 0.0,
1587            target: initial,
1588            config,
1589            velocity_threshold: 0.01,
1590            position_threshold: 0.001,
1591        }
1592    }
1593
1594    /// Get the current position.
1595    #[must_use]
1596    pub const fn position(&self) -> f32 {
1597        self.position
1598    }
1599
1600    /// Get the current velocity.
1601    #[must_use]
1602    pub const fn velocity(&self) -> f32 {
1603        self.velocity
1604    }
1605
1606    /// Get the target.
1607    #[must_use]
1608    pub const fn target(&self) -> f32 {
1609        self.target
1610    }
1611
1612    /// Set the target position.
1613    pub fn set_target(&mut self, target: f32) {
1614        self.target = target;
1615    }
1616
1617    /// Set position immediately without animation.
1618    pub fn set_immediate(&mut self, position: f32) {
1619        self.position = position;
1620        self.target = position;
1621        self.velocity = 0.0;
1622    }
1623
1624    /// Check if the animation is at rest.
1625    #[must_use]
1626    pub fn is_at_rest(&self) -> bool {
1627        let position_diff = (self.position - self.target).abs();
1628        let velocity_abs = self.velocity.abs();
1629        position_diff < self.position_threshold && velocity_abs < self.velocity_threshold
1630    }
1631
1632    /// Advance the spring animation by delta seconds.
1633    pub fn advance(&mut self, delta_s: f32) {
1634        if self.is_at_rest() {
1635            self.position = self.target;
1636            self.velocity = 0.0;
1637            return;
1638        }
1639
1640        // Spring physics: F = -kx - cv
1641        // a = F/m = (-kx - cv) / m
1642        let displacement = self.position - self.target;
1643        let spring_force = -self.config.stiffness * displacement;
1644        let damping_force = -self.config.damping * self.velocity;
1645        let acceleration = (spring_force + damping_force) / self.config.mass;
1646
1647        // Semi-implicit Euler integration
1648        self.velocity += acceleration * delta_s;
1649        self.position += self.velocity * delta_s;
1650    }
1651
1652    /// Advance by delta milliseconds.
1653    pub fn advance_ms(&mut self, delta_ms: u32) {
1654        self.advance(delta_ms as f32 / 1000.0);
1655    }
1656}