Skip to main content

presentar_core/
lifecycle.rs

1#![allow(clippy::unwrap_used, clippy::disallowed_methods)]
2//! Widget lifecycle hooks for mount, update, and unmount callbacks.
3//!
4//! This module provides a system for managing widget lifecycle events,
5//! similar to React's useEffect or Vue's lifecycle hooks.
6
7use crate::widget::WidgetId;
8use std::collections::HashMap;
9
10/// Lifecycle phase for widgets.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum LifecyclePhase {
13    /// Widget is being created/mounted.
14    Mount,
15    /// Widget is being updated (props/state changed).
16    Update,
17    /// Widget is being removed/unmounted.
18    Unmount,
19    /// Before the paint phase.
20    BeforePaint,
21    /// After the paint phase.
22    AfterPaint,
23    /// Widget gained focus.
24    Focus,
25    /// Widget lost focus.
26    Blur,
27    /// Widget became visible.
28    Visible,
29    /// Widget became hidden.
30    Hidden,
31}
32
33/// A lifecycle callback that can be registered.
34pub type LifecycleCallback = Box<dyn FnMut(LifecycleEvent) + Send>;
35
36/// Event passed to lifecycle callbacks.
37#[derive(Debug, Clone)]
38pub struct LifecycleEvent {
39    /// Widget ID.
40    pub widget_id: WidgetId,
41    /// Phase of the lifecycle.
42    pub phase: LifecyclePhase,
43    /// Timestamp (frame number or monotonic counter).
44    pub timestamp: u64,
45}
46
47impl LifecycleEvent {
48    /// Create a new lifecycle event.
49    pub fn new(widget_id: WidgetId, phase: LifecyclePhase, timestamp: u64) -> Self {
50        Self {
51            widget_id,
52            phase,
53            timestamp,
54        }
55    }
56}
57
58/// Unique ID for a lifecycle hook registration.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
60pub struct HookId(pub u64);
61
62impl HookId {
63    /// Create a new hook ID.
64    pub const fn new(id: u64) -> Self {
65        Self(id)
66    }
67}
68
69/// Registration for a lifecycle hook.
70#[derive(Debug)]
71struct HookRegistration {
72    #[allow(dead_code)]
73    id: HookId,
74    widget_id: WidgetId,
75    phases: Vec<LifecyclePhase>,
76}
77
78/// Manager for widget lifecycle hooks.
79pub struct LifecycleManager {
80    /// Next hook ID.
81    next_id: u64,
82    /// Registered hooks (by hook ID).
83    hooks: HashMap<HookId, HookRegistration>,
84    /// Callbacks (by hook ID).
85    callbacks: HashMap<HookId, LifecycleCallback>,
86    /// Index of hooks by widget ID.
87    by_widget: HashMap<WidgetId, Vec<HookId>>,
88    /// Index of hooks by phase.
89    by_phase: HashMap<LifecyclePhase, Vec<HookId>>,
90    /// Current frame/timestamp.
91    timestamp: u64,
92    /// Pending events to dispatch.
93    pending_events: Vec<LifecycleEvent>,
94}
95
96impl LifecycleManager {
97    /// Create a new lifecycle manager.
98    pub fn new() -> Self {
99        Self {
100            next_id: 0,
101            hooks: HashMap::new(),
102            callbacks: HashMap::new(),
103            by_widget: HashMap::new(),
104            by_phase: HashMap::new(),
105            timestamp: 0,
106            pending_events: Vec::new(),
107        }
108    }
109
110    /// Register a lifecycle hook.
111    ///
112    /// Returns a hook ID that can be used to unregister the hook.
113    pub fn register(
114        &mut self,
115        widget_id: WidgetId,
116        phases: Vec<LifecyclePhase>,
117        callback: LifecycleCallback,
118    ) -> HookId {
119        let id = HookId::new(self.next_id);
120        self.next_id += 1;
121
122        let registration = HookRegistration {
123            id,
124            widget_id,
125            phases: phases.clone(),
126        };
127
128        self.hooks.insert(id, registration);
129        self.callbacks.insert(id, callback);
130
131        // Index by widget
132        self.by_widget.entry(widget_id).or_default().push(id);
133
134        // Index by phase
135        for phase in phases {
136            self.by_phase.entry(phase).or_default().push(id);
137        }
138
139        id
140    }
141
142    /// Register a mount hook.
143    pub fn on_mount(&mut self, widget_id: WidgetId, callback: LifecycleCallback) -> HookId {
144        self.register(widget_id, vec![LifecyclePhase::Mount], callback)
145    }
146
147    /// Register an unmount hook.
148    pub fn on_unmount(&mut self, widget_id: WidgetId, callback: LifecycleCallback) -> HookId {
149        self.register(widget_id, vec![LifecyclePhase::Unmount], callback)
150    }
151
152    /// Register an update hook.
153    pub fn on_update(&mut self, widget_id: WidgetId, callback: LifecycleCallback) -> HookId {
154        self.register(widget_id, vec![LifecyclePhase::Update], callback)
155    }
156
157    /// Register a focus hook.
158    pub fn on_focus(&mut self, widget_id: WidgetId, callback: LifecycleCallback) -> HookId {
159        self.register(widget_id, vec![LifecyclePhase::Focus], callback)
160    }
161
162    /// Register a blur hook.
163    pub fn on_blur(&mut self, widget_id: WidgetId, callback: LifecycleCallback) -> HookId {
164        self.register(widget_id, vec![LifecyclePhase::Blur], callback)
165    }
166
167    /// Unregister a hook.
168    pub fn unregister(&mut self, hook_id: HookId) -> bool {
169        if let Some(registration) = self.hooks.remove(&hook_id) {
170            self.callbacks.remove(&hook_id);
171
172            // Remove from widget index
173            if let Some(hooks) = self.by_widget.get_mut(&registration.widget_id) {
174                hooks.retain(|&id| id != hook_id);
175            }
176
177            // Remove from phase index
178            for phase in &registration.phases {
179                if let Some(hooks) = self.by_phase.get_mut(phase) {
180                    hooks.retain(|&id| id != hook_id);
181                }
182            }
183
184            true
185        } else {
186            false
187        }
188    }
189
190    /// Unregister all hooks for a widget.
191    pub fn unregister_widget(&mut self, widget_id: WidgetId) {
192        if let Some(hook_ids) = self.by_widget.remove(&widget_id) {
193            for hook_id in hook_ids {
194                if let Some(registration) = self.hooks.remove(&hook_id) {
195                    self.callbacks.remove(&hook_id);
196
197                    for phase in &registration.phases {
198                        if let Some(hooks) = self.by_phase.get_mut(phase) {
199                            hooks.retain(|&id| id != hook_id);
200                        }
201                    }
202                }
203            }
204        }
205    }
206
207    /// Emit a lifecycle event immediately.
208    pub fn emit(&mut self, widget_id: WidgetId, phase: LifecyclePhase) {
209        let event = LifecycleEvent::new(widget_id, phase, self.timestamp);
210
211        // Get hooks for this widget and phase
212        let widget_hooks = self.by_widget.get(&widget_id).cloned().unwrap_or_default();
213        let phase_hooks = self.by_phase.get(&phase).cloned().unwrap_or_default();
214
215        // Find intersection (hooks registered for both this widget and phase)
216        for hook_id in widget_hooks {
217            if phase_hooks.contains(&hook_id) {
218                if let Some(callback) = self.callbacks.get_mut(&hook_id) {
219                    callback(event.clone());
220                }
221            }
222        }
223    }
224
225    /// Queue a lifecycle event for later dispatch.
226    pub fn queue(&mut self, widget_id: WidgetId, phase: LifecyclePhase) {
227        let event = LifecycleEvent::new(widget_id, phase, self.timestamp);
228        self.pending_events.push(event);
229    }
230
231    /// Dispatch all pending events.
232    pub fn flush(&mut self) {
233        // See binding.rs::flush — `mem::take` is the allocation-free spelling
234        // of `drain(..).collect()`. clippy::drain_collect (new in 1.98).
235        let events: Vec<LifecycleEvent> = std::mem::take(&mut self.pending_events);
236
237        for event in events {
238            let widget_hooks = self
239                .by_widget
240                .get(&event.widget_id)
241                .cloned()
242                .unwrap_or_default();
243            let phase_hooks = self.by_phase.get(&event.phase).cloned().unwrap_or_default();
244
245            for hook_id in widget_hooks {
246                if phase_hooks.contains(&hook_id) {
247                    if let Some(callback) = self.callbacks.get_mut(&hook_id) {
248                        callback(event.clone());
249                    }
250                }
251            }
252        }
253    }
254
255    /// Get the number of pending events.
256    pub fn pending_count(&self) -> usize {
257        self.pending_events.len()
258    }
259
260    /// Advance the timestamp.
261    pub fn tick(&mut self) {
262        self.timestamp += 1;
263    }
264
265    /// Get the current timestamp.
266    pub fn timestamp(&self) -> u64 {
267        self.timestamp
268    }
269
270    /// Get the number of registered hooks.
271    pub fn hook_count(&self) -> usize {
272        self.hooks.len()
273    }
274
275    /// Check if a widget has any hooks.
276    pub fn has_hooks(&self, widget_id: WidgetId) -> bool {
277        self.by_widget
278            .get(&widget_id)
279            .is_some_and(|h| !h.is_empty())
280    }
281
282    /// Clear all hooks and events.
283    pub fn clear(&mut self) {
284        self.hooks.clear();
285        self.callbacks.clear();
286        self.by_widget.clear();
287        self.by_phase.clear();
288        self.pending_events.clear();
289    }
290}
291
292impl Default for LifecycleManager {
293    fn default() -> Self {
294        Self::new()
295    }
296}
297
298impl std::fmt::Debug for LifecycleManager {
299    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300        f.debug_struct("LifecycleManager")
301            .field("next_id", &self.next_id)
302            .field("hook_count", &self.hooks.len())
303            .field("timestamp", &self.timestamp)
304            .field("pending_count", &self.pending_events.len())
305            .finish()
306    }
307}
308
309/// Effect hook that runs a callback and optionally cleans up.
310pub struct Effect {
311    /// Effect function that returns an optional cleanup function.
312    effect: Option<Box<dyn FnOnce() -> Option<Box<dyn FnOnce() + Send>> + Send>>,
313    /// Cleanup function from the last run.
314    cleanup: Option<Box<dyn FnOnce() + Send>>,
315    /// Dependencies for determining when to re-run.
316    deps: Vec<u64>,
317}
318
319impl Effect {
320    /// Create a new effect.
321    pub fn new<F>(effect: F) -> Self
322    where
323        F: FnOnce() -> Option<Box<dyn FnOnce() + Send>> + Send + 'static,
324    {
325        Self {
326            effect: Some(Box::new(effect)),
327            cleanup: None,
328            deps: Vec::new(),
329        }
330    }
331
332    /// Create an effect with dependencies.
333    pub fn with_deps<F>(effect: F, deps: Vec<u64>) -> Self
334    where
335        F: FnOnce() -> Option<Box<dyn FnOnce() + Send>> + Send + 'static,
336    {
337        Self {
338            effect: Some(Box::new(effect)),
339            cleanup: None,
340            deps,
341        }
342    }
343
344    /// Check if dependencies changed.
345    pub fn deps_changed(&self, new_deps: &[u64]) -> bool {
346        if self.deps.len() != new_deps.len() {
347            return true;
348        }
349        self.deps.iter().zip(new_deps).any(|(a, b)| a != b)
350    }
351
352    /// Run the effect if dependencies changed.
353    pub fn run(&mut self, new_deps: Option<&[u64]>) -> bool {
354        // Check if we should run based on deps
355        let should_run = match new_deps {
356            Some(deps) if !self.deps_changed(deps) => false,
357            _ => true,
358        };
359
360        if !should_run {
361            return false;
362        }
363
364        // Run cleanup from previous effect
365        if let Some(cleanup) = self.cleanup.take() {
366            cleanup();
367        }
368
369        // Run the effect
370        if let Some(effect) = self.effect.take() {
371            self.cleanup = effect();
372        }
373
374        // Update deps
375        if let Some(deps) = new_deps {
376            self.deps = deps.to_vec();
377        }
378
379        true
380    }
381
382    /// Run cleanup without running the effect.
383    pub fn cleanup(&mut self) {
384        if let Some(cleanup) = self.cleanup.take() {
385            cleanup();
386        }
387    }
388
389    /// Check if the effect has a pending cleanup.
390    pub fn has_cleanup(&self) -> bool {
391        self.cleanup.is_some()
392    }
393}
394
395impl std::fmt::Debug for Effect {
396    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397        f.debug_struct("Effect")
398            .field("has_effect", &self.effect.is_some())
399            .field("has_cleanup", &self.cleanup.is_some())
400            .field("deps", &self.deps)
401            .finish()
402    }
403}
404
405/// Manager for effects with automatic cleanup.
406#[derive(Debug, Default)]
407pub struct EffectManager {
408    /// Effects by widget ID.
409    effects: HashMap<WidgetId, Vec<Effect>>,
410}
411
412impl EffectManager {
413    /// Create a new effect manager.
414    pub fn new() -> Self {
415        Self::default()
416    }
417
418    /// Add an effect for a widget.
419    pub fn add(&mut self, widget_id: WidgetId, effect: Effect) {
420        self.effects.entry(widget_id).or_default().push(effect);
421    }
422
423    /// Run all effects for a widget.
424    pub fn run_effects(&mut self, widget_id: WidgetId, deps: Option<&[u64]>) {
425        if let Some(effects) = self.effects.get_mut(&widget_id) {
426            for effect in effects {
427                effect.run(deps);
428            }
429        }
430    }
431
432    /// Clean up effects for a widget (e.g., on unmount).
433    pub fn cleanup_widget(&mut self, widget_id: WidgetId) {
434        if let Some(effects) = self.effects.get_mut(&widget_id) {
435            for effect in effects {
436                effect.cleanup();
437            }
438        }
439        self.effects.remove(&widget_id);
440    }
441
442    /// Get the number of widgets with effects.
443    pub fn widget_count(&self) -> usize {
444        self.effects.len()
445    }
446
447    /// Get the total number of effects.
448    pub fn effect_count(&self) -> usize {
449        self.effects.values().map(std::vec::Vec::len).sum()
450    }
451
452    /// Clear all effects.
453    pub fn clear(&mut self) {
454        for effects in self.effects.values_mut() {
455            for effect in effects {
456                effect.cleanup();
457            }
458        }
459        self.effects.clear();
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466    use std::sync::atomic::{AtomicUsize, Ordering};
467    use std::sync::Arc;
468
469    // LifecyclePhase tests
470    #[test]
471    fn test_lifecycle_phase_equality() {
472        assert_eq!(LifecyclePhase::Mount, LifecyclePhase::Mount);
473        assert_ne!(LifecyclePhase::Mount, LifecyclePhase::Unmount);
474    }
475
476    // LifecycleEvent tests
477    #[test]
478    fn test_lifecycle_event_new() {
479        let event = LifecycleEvent::new(WidgetId::new(1), LifecyclePhase::Mount, 42);
480        assert_eq!(event.widget_id, WidgetId::new(1));
481        assert_eq!(event.phase, LifecyclePhase::Mount);
482        assert_eq!(event.timestamp, 42);
483    }
484
485    // HookId tests
486    #[test]
487    fn test_hook_id() {
488        let id1 = HookId::new(1);
489        let id2 = HookId::new(1);
490        let id3 = HookId::new(2);
491
492        assert_eq!(id1, id2);
493        assert_ne!(id1, id3);
494    }
495
496    // LifecycleManager tests
497    #[test]
498    fn test_manager_new() {
499        let manager = LifecycleManager::new();
500        assert_eq!(manager.hook_count(), 0);
501        assert_eq!(manager.timestamp(), 0);
502    }
503
504    #[test]
505    fn test_manager_register() {
506        let mut manager = LifecycleManager::new();
507        let widget_id = WidgetId::new(1);
508
509        let hook_id = manager.register(widget_id, vec![LifecyclePhase::Mount], Box::new(|_| {}));
510
511        assert_eq!(manager.hook_count(), 1);
512        assert!(manager.has_hooks(widget_id));
513        assert!(!manager.has_hooks(WidgetId::new(999)));
514        assert_eq!(hook_id.0, 0);
515    }
516
517    #[test]
518    fn test_manager_on_mount() {
519        let mut manager = LifecycleManager::new();
520        let widget_id = WidgetId::new(1);
521
522        let _hook_id = manager.on_mount(widget_id, Box::new(|_| {}));
523        assert_eq!(manager.hook_count(), 1);
524    }
525
526    #[test]
527    fn test_manager_on_unmount() {
528        let mut manager = LifecycleManager::new();
529        let widget_id = WidgetId::new(1);
530
531        let _hook_id = manager.on_unmount(widget_id, Box::new(|_| {}));
532        assert_eq!(manager.hook_count(), 1);
533    }
534
535    #[test]
536    fn test_manager_emit() {
537        let counter = Arc::new(AtomicUsize::new(0));
538        let counter_clone = counter.clone();
539
540        let mut manager = LifecycleManager::new();
541        let widget_id = WidgetId::new(1);
542
543        manager.on_mount(
544            widget_id,
545            Box::new(move |_| {
546                counter_clone.fetch_add(1, Ordering::SeqCst);
547            }),
548        );
549
550        manager.emit(widget_id, LifecyclePhase::Mount);
551        assert_eq!(counter.load(Ordering::SeqCst), 1);
552
553        // Emit again
554        manager.emit(widget_id, LifecyclePhase::Mount);
555        assert_eq!(counter.load(Ordering::SeqCst), 2);
556    }
557
558    #[test]
559    fn test_manager_emit_wrong_phase() {
560        let counter = Arc::new(AtomicUsize::new(0));
561        let counter_clone = counter.clone();
562
563        let mut manager = LifecycleManager::new();
564        let widget_id = WidgetId::new(1);
565
566        manager.on_mount(
567            widget_id,
568            Box::new(move |_| {
569                counter_clone.fetch_add(1, Ordering::SeqCst);
570            }),
571        );
572
573        // Emit unmount instead of mount
574        manager.emit(widget_id, LifecyclePhase::Unmount);
575        assert_eq!(counter.load(Ordering::SeqCst), 0);
576    }
577
578    #[test]
579    fn test_manager_queue_and_flush() {
580        let counter = Arc::new(AtomicUsize::new(0));
581        let counter_clone = counter.clone();
582
583        let mut manager = LifecycleManager::new();
584        let widget_id = WidgetId::new(1);
585
586        manager.on_mount(
587            widget_id,
588            Box::new(move |_| {
589                counter_clone.fetch_add(1, Ordering::SeqCst);
590            }),
591        );
592
593        manager.queue(widget_id, LifecyclePhase::Mount);
594        manager.queue(widget_id, LifecyclePhase::Mount);
595        assert_eq!(manager.pending_count(), 2);
596        assert_eq!(counter.load(Ordering::SeqCst), 0);
597
598        manager.flush();
599        assert_eq!(manager.pending_count(), 0);
600        assert_eq!(counter.load(Ordering::SeqCst), 2);
601    }
602
603    #[test]
604    fn test_manager_unregister() {
605        let mut manager = LifecycleManager::new();
606        let widget_id = WidgetId::new(1);
607
608        let hook_id = manager.on_mount(widget_id, Box::new(|_| {}));
609        assert_eq!(manager.hook_count(), 1);
610
611        let removed = manager.unregister(hook_id);
612        assert!(removed);
613        assert_eq!(manager.hook_count(), 0);
614        assert!(!manager.has_hooks(widget_id));
615    }
616
617    #[test]
618    fn test_manager_unregister_widget() {
619        let mut manager = LifecycleManager::new();
620        let widget_id = WidgetId::new(1);
621
622        manager.on_mount(widget_id, Box::new(|_| {}));
623        manager.on_unmount(widget_id, Box::new(|_| {}));
624        manager.on_update(widget_id, Box::new(|_| {}));
625        assert_eq!(manager.hook_count(), 3);
626
627        manager.unregister_widget(widget_id);
628        assert_eq!(manager.hook_count(), 0);
629    }
630
631    #[test]
632    fn test_manager_tick() {
633        let mut manager = LifecycleManager::new();
634        assert_eq!(manager.timestamp(), 0);
635
636        manager.tick();
637        assert_eq!(manager.timestamp(), 1);
638
639        manager.tick();
640        manager.tick();
641        assert_eq!(manager.timestamp(), 3);
642    }
643
644    #[test]
645    fn test_manager_clear() {
646        let mut manager = LifecycleManager::new();
647        let widget_id = WidgetId::new(1);
648
649        manager.on_mount(widget_id, Box::new(|_| {}));
650        manager.queue(widget_id, LifecyclePhase::Mount);
651
652        manager.clear();
653        assert_eq!(manager.hook_count(), 0);
654        assert_eq!(manager.pending_count(), 0);
655    }
656
657    #[test]
658    fn test_manager_multiple_widgets() {
659        let counter1 = Arc::new(AtomicUsize::new(0));
660        let counter2 = Arc::new(AtomicUsize::new(0));
661        let c1 = counter1.clone();
662        let c2 = counter2.clone();
663
664        let mut manager = LifecycleManager::new();
665
666        manager.on_mount(
667            WidgetId::new(1),
668            Box::new(move |_| {
669                c1.fetch_add(1, Ordering::SeqCst);
670            }),
671        );
672        manager.on_mount(
673            WidgetId::new(2),
674            Box::new(move |_| {
675                c2.fetch_add(1, Ordering::SeqCst);
676            }),
677        );
678
679        manager.emit(WidgetId::new(1), LifecyclePhase::Mount);
680        assert_eq!(counter1.load(Ordering::SeqCst), 1);
681        assert_eq!(counter2.load(Ordering::SeqCst), 0);
682
683        manager.emit(WidgetId::new(2), LifecyclePhase::Mount);
684        assert_eq!(counter1.load(Ordering::SeqCst), 1);
685        assert_eq!(counter2.load(Ordering::SeqCst), 1);
686    }
687
688    // Effect tests
689    #[test]
690    fn test_effect_new() {
691        let effect = Effect::new(|| None);
692        assert!(!effect.has_cleanup());
693    }
694
695    #[test]
696    fn test_effect_with_deps() {
697        let effect = Effect::with_deps(|| None, vec![1, 2, 3]);
698        assert_eq!(effect.deps, vec![1, 2, 3]);
699    }
700
701    #[test]
702    fn test_effect_deps_changed() {
703        let effect = Effect::with_deps(|| None, vec![1, 2, 3]);
704
705        assert!(!effect.deps_changed(&[1, 2, 3]));
706        assert!(effect.deps_changed(&[1, 2, 4]));
707        assert!(effect.deps_changed(&[1, 2]));
708        assert!(effect.deps_changed(&[1, 2, 3, 4]));
709    }
710
711    #[test]
712    fn test_effect_run() {
713        let counter = Arc::new(AtomicUsize::new(0));
714        let c = counter.clone();
715
716        let mut effect = Effect::new(move || {
717            c.fetch_add(1, Ordering::SeqCst);
718            None
719        });
720
721        effect.run(None);
722        assert_eq!(counter.load(Ordering::SeqCst), 1);
723
724        // Effect can only run once (it's moved out)
725        effect.run(None);
726        assert_eq!(counter.load(Ordering::SeqCst), 1);
727    }
728
729    #[test]
730    fn test_effect_cleanup() {
731        let cleanup_counter = Arc::new(AtomicUsize::new(0));
732        let cc = cleanup_counter.clone();
733
734        let mut effect = Effect::new(move || {
735            let cc = cc;
736            let b: Box<dyn FnOnce() + Send> = Box::new(move || {
737                cc.fetch_add(1, Ordering::SeqCst);
738            });
739            Some(b)
740        });
741
742        effect.run(None);
743        assert!(effect.has_cleanup());
744        assert_eq!(cleanup_counter.load(Ordering::SeqCst), 0);
745
746        effect.cleanup();
747        assert!(!effect.has_cleanup());
748        assert_eq!(cleanup_counter.load(Ordering::SeqCst), 1);
749    }
750
751    // EffectManager tests
752    #[test]
753    fn test_effect_manager_new() {
754        let manager = EffectManager::new();
755        assert_eq!(manager.widget_count(), 0);
756        assert_eq!(manager.effect_count(), 0);
757    }
758
759    #[test]
760    fn test_effect_manager_add() {
761        let mut manager = EffectManager::new();
762        let widget_id = WidgetId::new(1);
763
764        manager.add(widget_id, Effect::new(|| None));
765        manager.add(widget_id, Effect::new(|| None));
766
767        assert_eq!(manager.widget_count(), 1);
768        assert_eq!(manager.effect_count(), 2);
769    }
770
771    #[test]
772    fn test_effect_manager_run_effects() {
773        let counter = Arc::new(AtomicUsize::new(0));
774        let c = counter.clone();
775
776        let mut manager = EffectManager::new();
777        let widget_id = WidgetId::new(1);
778
779        manager.add(
780            widget_id,
781            Effect::new(move || {
782                c.fetch_add(1, Ordering::SeqCst);
783                None
784            }),
785        );
786
787        manager.run_effects(widget_id, None);
788        assert_eq!(counter.load(Ordering::SeqCst), 1);
789    }
790
791    #[test]
792    fn test_effect_manager_cleanup_widget() {
793        let cleanup_counter = Arc::new(AtomicUsize::new(0));
794        let cc = cleanup_counter.clone();
795
796        let mut manager = EffectManager::new();
797        let widget_id = WidgetId::new(1);
798
799        manager.add(
800            widget_id,
801            Effect::new(move || {
802                let cc = cc;
803                let b: Box<dyn FnOnce() + Send> = Box::new(move || {
804                    cc.fetch_add(1, Ordering::SeqCst);
805                });
806                Some(b)
807            }),
808        );
809
810        manager.run_effects(widget_id, None);
811        assert_eq!(manager.effect_count(), 1);
812
813        manager.cleanup_widget(widget_id);
814        assert_eq!(manager.effect_count(), 0);
815        assert_eq!(cleanup_counter.load(Ordering::SeqCst), 1);
816    }
817
818    #[test]
819    fn test_effect_manager_clear() {
820        let mut manager = EffectManager::new();
821
822        manager.add(WidgetId::new(1), Effect::new(|| None));
823        manager.add(WidgetId::new(2), Effect::new(|| None));
824
825        manager.clear();
826        assert_eq!(manager.widget_count(), 0);
827        assert_eq!(manager.effect_count(), 0);
828    }
829}