Skip to main content

lgui_core/core/component/
effect.rs

1use std::{any::Any, cell::RefCell, collections::HashMap};
2
3use super::{ComponentTree, HookId};
4
5pub type UiEffect = Box<dyn FnOnce() + 'static>;
6
7pub trait IntoEffectCleanup {
8    fn into_cleanup(self) -> Option<UiEffect>;
9}
10
11impl IntoEffectCleanup for () {
12    fn into_cleanup(self) -> Option<UiEffect> {
13        None
14    }
15}
16
17impl<F> IntoEffectCleanup for F
18where
19    F: FnOnce() + 'static,
20{
21    fn into_cleanup(self) -> Option<UiEffect> {
22        Some(Box::new(self))
23    }
24}
25
26struct EffectEntry {
27    deps: Box<dyn Any>,
28    generation: u64,
29    cleanup: Option<UiEffect>,
30}
31
32struct StagedEffect {
33    id: HookId,
34    deps: Box<dyn Any>,
35    deps_equal: fn(&dyn Any, &dyn Any) -> bool,
36    run: Box<dyn FnOnce() -> Option<UiEffect> + 'static>,
37}
38
39struct PendingEffect {
40    id: HookId,
41    generation: u64,
42    run: Box<dyn FnOnce() -> Option<UiEffect> + 'static>,
43}
44
45#[derive(Default)]
46pub struct EffectRegistry {
47    entries: RefCell<HashMap<HookId, EffectEntry>>,
48    staged: RefCell<Vec<StagedEffect>>,
49    pending_cleanups: RefCell<Vec<UiEffect>>,
50    pending_effects: RefCell<Vec<PendingEffect>>,
51}
52
53impl EffectRegistry {
54    pub fn new() -> Self {
55        Self::default()
56    }
57
58    pub fn register<D, F, R>(&self, id: HookId, deps: D, effect: F)
59    where
60        D: Clone + PartialEq + 'static,
61        F: FnOnce() -> R + 'static,
62        R: IntoEffectCleanup,
63    {
64        self.register_erased(
65            id,
66            Box::new(deps),
67            deps_equal::<D>,
68            Box::new(move || effect().into_cleanup()),
69        );
70    }
71
72    pub(super) fn register_erased(
73        &self,
74        id: HookId,
75        deps: Box<dyn Any>,
76        deps_equal: fn(&dyn Any, &dyn Any) -> bool,
77        run: Box<dyn FnOnce() -> Option<UiEffect> + 'static>,
78    ) {
79        let mut staged = self.staged.borrow_mut();
80        if staged.iter().any(|candidate| candidate.id == id) {
81            panic!("effect hook `{id}` was registered more than once in one render");
82        }
83        staged.push(StagedEffect {
84            id,
85            deps,
86            deps_equal,
87            run,
88        });
89    }
90
91    pub fn begin_frame(&self) {
92        self.staged.borrow_mut().clear();
93    }
94
95    pub fn abort_frame(&self) {
96        self.staged.borrow_mut().clear();
97    }
98
99    pub fn end_frame(&self, components: &ComponentTree) {
100        let staged = self.staged.borrow_mut().drain(..).collect::<Vec<_>>();
101        for candidate in staged {
102            if !components.is_alive(candidate.id.component()) {
103                continue;
104            }
105            let generation = {
106                let mut entries = self.entries.borrow_mut();
107                match entries.get_mut(&candidate.id) {
108                    Some(entry) => {
109                        if (candidate.deps_equal)(entry.deps.as_ref(), candidate.deps.as_ref()) {
110                            continue;
111                        }
112                        if let Some(cleanup) = entry.cleanup.take() {
113                            self.pending_cleanups.borrow_mut().push(cleanup);
114                        }
115                        entry.deps = candidate.deps;
116                        entry.generation = entry.generation.wrapping_add(1);
117                        entry.generation
118                    }
119                    None => {
120                        entries.insert(
121                            candidate.id,
122                            EffectEntry {
123                                deps: candidate.deps,
124                                generation: 1,
125                                cleanup: None,
126                            },
127                        );
128                        1
129                    }
130                }
131            };
132            self.pending_effects.borrow_mut().push(PendingEffect {
133                id: candidate.id,
134                generation,
135                run: candidate.run,
136            });
137        }
138
139        let removed = {
140            let mut entries = self.entries.borrow_mut();
141            let mut removed_ids: Vec<HookId> = entries
142                .keys()
143                .filter(|id| !components.is_alive(id.component()))
144                .cloned()
145                .collect();
146            removed_ids.sort_unstable();
147            removed_ids
148                .into_iter()
149                .filter_map(|id| entries.remove(&id).and_then(|entry| entry.cleanup))
150                .collect::<Vec<_>>()
151        };
152        self.pending_cleanups.borrow_mut().extend(removed);
153        self.pending_effects
154            .borrow_mut()
155            .retain(|pending| components.is_alive(pending.id.component()));
156    }
157
158    pub fn run_pending(&self) {
159        let cleanups = self
160            .pending_cleanups
161            .borrow_mut()
162            .drain(..)
163            .collect::<Vec<_>>();
164        for cleanup in cleanups {
165            cleanup();
166        }
167
168        let effects = self
169            .pending_effects
170            .borrow_mut()
171            .drain(..)
172            .collect::<Vec<_>>();
173        for pending in effects {
174            let is_current = self
175                .entries
176                .borrow()
177                .get(&pending.id)
178                .is_some_and(|entry| entry.generation == pending.generation);
179            if !is_current {
180                continue;
181            }
182            let cleanup = (pending.run)();
183            if let Some(entry) = self.entries.borrow_mut().get_mut(&pending.id) {
184                if entry.generation == pending.generation {
185                    entry.cleanup = cleanup;
186                }
187            }
188        }
189    }
190
191    pub fn clear(&self) {
192        self.staged.borrow_mut().clear();
193        self.pending_effects.borrow_mut().clear();
194        let mut cleanups = self
195            .pending_cleanups
196            .borrow_mut()
197            .drain(..)
198            .collect::<Vec<_>>();
199        cleanups.extend(
200            self.entries
201                .borrow_mut()
202                .drain()
203                .filter_map(|(_, entry)| entry.cleanup),
204        );
205        for cleanup in cleanups {
206            cleanup();
207        }
208    }
209}
210
211fn deps_equal<D>(left: &dyn Any, right: &dyn Any) -> bool
212where
213    D: PartialEq + 'static,
214{
215    let left = left
216        .downcast_ref::<D>()
217        .unwrap_or_else(|| panic!("effect dependency type changed between renders"));
218    let right = right
219        .downcast_ref::<D>()
220        .unwrap_or_else(|| panic!("effect dependency type changed during render"));
221    left == right
222}
223
224#[cfg(test)]
225#[path = "effect_test.rs"]
226mod tests;