Skip to main content

cranpose_core/
callbacks.rs

1use std::{cell::RefCell, rc::Rc};
2
3use crate::{composer_context, Composer, ComposerCore, RecomposeScope};
4
5pub struct ParamState<T> {
6    pub(crate) value: Option<T>,
7}
8
9impl<T> ParamState<T> {
10    pub fn update(&mut self, new_value: &T) -> bool
11    where
12        T: PartialEq + Clone,
13    {
14        match self.value.as_mut() {
15            Some(old) if old == new_value => false,
16            Some(old) => {
17                old.clone_from(new_value);
18                true
19            }
20            None => {
21                self.value = Some(new_value.clone());
22                true
23            }
24        }
25    }
26
27    pub fn value(&self) -> Option<T>
28    where
29        T: Clone,
30    {
31        self.value.clone()
32    }
33}
34
35/// ParamSlot holds function/closure parameters by ownership (no PartialEq/Clone required).
36/// Used by the `#[composable]` macro to store Fn-like parameters in the slot table.
37pub struct ParamSlot<T> {
38    val: RefCell<Option<T>>,
39}
40
41impl<T> Default for ParamSlot<T> {
42    fn default() -> Self {
43        Self {
44            val: RefCell::new(None),
45        }
46    }
47}
48
49impl<T> ParamSlot<T> {
50    pub fn set(&self, v: T) {
51        *self.val.borrow_mut() = Some(v);
52    }
53
54    /// Takes the value out temporarily for a recomposition callback.
55    pub fn take(&self) -> Option<T> {
56        self.val.borrow_mut().take()
57    }
58}
59
60type CallbackCell = Rc<RefCell<Option<Box<dyn FnMut()>>>>;
61type CallbackScopeCell = Rc<RefCell<Option<RecomposeScope>>>;
62
63struct CallbackScopeGuard {
64    core: Rc<ComposerCore>,
65}
66
67impl CallbackScopeGuard {
68    fn push(composer: &Composer, scope: RecomposeScope) -> Self {
69        composer.core.scope_stack.borrow_mut().push(scope);
70        Self {
71            core: composer.clone_core(),
72        }
73    }
74}
75
76impl Drop for CallbackScopeGuard {
77    fn drop(&mut self) {
78        self.core.scope_stack.borrow_mut().pop();
79    }
80}
81
82fn with_callback_scope<R>(scope: &CallbackScopeCell, f: impl FnOnce() -> R) -> R {
83    let captured_scope = scope.borrow().clone();
84    if let Some(saved_scope) = captured_scope {
85        if let Some(composer) = composer_context::current_composer() {
86            let _scope_guard = CallbackScopeGuard::push(&composer, saved_scope);
87            return f();
88        }
89    }
90
91    f()
92}
93
94fn callback_owner_is_active(scope: &CallbackScopeCell) -> bool {
95    scope
96        .borrow()
97        .as_ref()
98        .is_none_or(RecomposeScope::is_effectively_active)
99}
100
101fn callback_owner_scope(composer: &Composer) -> Option<RecomposeScope> {
102    composer.core.scope_stack.borrow().last().cloned()
103}
104
105#[derive(Clone)]
106pub struct CallbackHolder {
107    rc: CallbackCell,
108    creator_scope: CallbackScopeCell,
109}
110
111impl CallbackHolder {
112    /// Create a new holder with a no-op callback so that callers can immediately invoke it.
113    pub fn new() -> Self {
114        Self::default()
115    }
116
117    /// Replace the stored callback with a new closure provided by the caller.
118    pub fn update<F>(&self, f: F)
119    where
120        F: FnMut() + 'static,
121    {
122        self.update_boxed(Box::new(f));
123    }
124
125    /// Boxed form of [`Self::update`]: lets generated composable helpers take
126    /// callbacks type-erased at the public-fn boundary, so helper bodies are
127    /// compiled once instead of once per caller closure type.
128    pub fn update_boxed(&self, f: Box<dyn FnMut() + 'static>) {
129        *self.rc.borrow_mut() = Some(f);
130        *self.creator_scope.borrow_mut() =
131            composer_context::try_with_composer(callback_owner_scope).flatten();
132    }
133
134    /// Produce a forwarder closure that keeps the holder alive and forwards calls to it.
135    pub fn clone_rc(&self) -> impl Fn() + 'static {
136        let rc = self.rc.clone();
137        let creator_scope = self.creator_scope.clone();
138        move || {
139            if !callback_owner_is_active(&creator_scope) {
140                return;
141            }
142            with_callback_scope(&creator_scope, || {
143                if let Some(callback) = rc.borrow_mut().as_mut() {
144                    callback();
145                }
146            });
147        }
148    }
149}
150
151impl Default for CallbackHolder {
152    fn default() -> Self {
153        Self {
154            rc: Rc::new(RefCell::new(None)),
155            creator_scope: Rc::new(RefCell::new(None)),
156        }
157    }
158}
159
160/// CallbackHolder1 keeps the latest single-argument callback closure alive across recompositions.
161/// It mirrors [`CallbackHolder`] but supports callbacks that receive one argument.
162#[derive(Clone)]
163pub struct CallbackHolder1<A: 'static> {
164    #[allow(clippy::type_complexity)]
165    rc: Rc<RefCell<Option<Box<dyn FnMut(A)>>>>,
166    creator_scope: CallbackScopeCell,
167}
168
169impl<A: 'static> CallbackHolder1<A> {
170    /// Create a new holder with a no-op callback so callers can invoke it immediately.
171    pub fn new() -> Self {
172        Self::default()
173    }
174
175    /// Replace the stored callback with a new closure provided by the caller.
176    pub fn update<F>(&self, f: F)
177    where
178        F: FnMut(A) + 'static,
179    {
180        *self.rc.borrow_mut() = Some(Box::new(f));
181        *self.creator_scope.borrow_mut() =
182            composer_context::try_with_composer(callback_owner_scope).flatten();
183    }
184
185    /// Produce a forwarder closure that keeps the holder alive and forwards calls to it.
186    pub fn clone_rc(&self) -> impl Fn(A) + 'static {
187        let rc = self.rc.clone();
188        let creator_scope = self.creator_scope.clone();
189        move |arg| {
190            if !callback_owner_is_active(&creator_scope) {
191                return;
192            }
193            with_callback_scope(&creator_scope, || {
194                if let Some(callback) = rc.borrow_mut().as_mut() {
195                    callback(arg);
196                }
197            });
198        }
199    }
200}
201
202impl<A: 'static> Default for CallbackHolder1<A> {
203    fn default() -> Self {
204        Self {
205            rc: Rc::new(RefCell::new(None)),
206            creator_scope: Rc::new(RefCell::new(None)),
207        }
208    }
209}
210
211pub struct ReturnSlot<T> {
212    value: Option<T>,
213}
214
215impl<T: Clone> ReturnSlot<T> {
216    pub fn store(&mut self, value: T) {
217        self.value = Some(value);
218    }
219
220    pub fn get(&self) -> Option<T> {
221        self.value.clone()
222    }
223}
224
225impl<T> Default for ParamState<T> {
226    fn default() -> Self {
227        Self { value: None }
228    }
229}
230
231impl<T> Default for ReturnSlot<T> {
232    fn default() -> Self {
233        Self { value: None }
234    }
235}
236
237#[cfg(test)]
238mod callback_holder_tests {
239    use std::{cell::Cell, rc::Rc};
240
241    use super::{CallbackHolder, CallbackHolder1, ParamSlot};
242    use crate::{runtime::TestRuntime, RecomposeScope};
243
244    #[test]
245    fn param_slot_take_reports_absence_instead_of_panicking() {
246        let slot = ParamSlot::default();
247
248        assert_eq!(slot.take(), None);
249        slot.set(11);
250        assert_eq!(slot.take(), Some(11));
251        assert_eq!(slot.take(), None);
252    }
253
254    #[test]
255    fn callback_holder_default_forwarder_is_noop() {
256        let forwarder = CallbackHolder::new().clone_rc();
257        forwarder();
258    }
259
260    #[test]
261    fn callback_holder_forwarder_uses_latest_callback() {
262        let holder = CallbackHolder::new();
263        let total = Rc::new(Cell::new(0));
264        let forwarder = holder.clone_rc();
265
266        let first_total = Rc::clone(&total);
267        holder.update(move || first_total.set(first_total.get() + 1));
268        forwarder();
269
270        let second_total = Rc::clone(&total);
271        holder.update(move || second_total.set(second_total.get() + 10));
272        forwarder();
273
274        assert_eq!(total.get(), 11);
275    }
276
277    #[test]
278    fn callback_holder_does_not_invoke_after_creator_scope_deactivation() {
279        let runtime = TestRuntime::new();
280        let scope = RecomposeScope::new_for_test(runtime.handle());
281        let holder = CallbackHolder::new();
282        let invocations = Rc::new(Cell::new(0));
283        let invocations_for_callback = Rc::clone(&invocations);
284        holder.update(move || invocations_for_callback.set(invocations_for_callback.get() + 1));
285        holder.creator_scope.replace(Some(scope.clone()));
286        let forwarder = holder.clone_rc();
287
288        forwarder();
289        assert_eq!(invocations.get(), 1);
290
291        scope.deactivate();
292        forwarder();
293        assert_eq!(
294            invocations.get(),
295            1,
296            "callbacks cannot outlive the active composition scope that owns them",
297        );
298    }
299
300    #[test]
301    fn callback_holder_does_not_invoke_under_inactive_creator_ancestor() {
302        let runtime = TestRuntime::new();
303        let ancestor = RecomposeScope::new_for_test(runtime.handle());
304        let creator = RecomposeScope::new_for_test(runtime.handle());
305        creator.set_parent_scope(Some(ancestor.clone()));
306        let holder = CallbackHolder::new();
307        let invocations = Rc::new(Cell::new(0));
308        let invocations_for_callback = Rc::clone(&invocations);
309        holder.update(move || invocations_for_callback.set(invocations_for_callback.get() + 1));
310        holder.creator_scope.replace(Some(creator));
311        let forwarder = holder.clone_rc();
312
313        forwarder();
314        assert_eq!(invocations.get(), 1);
315
316        ancestor.deactivate();
317        forwarder();
318        assert_eq!(
319            invocations.get(),
320            1,
321            "callbacks cannot run beneath an inactive composition ancestor",
322        );
323    }
324
325    #[test]
326    fn callback_holder1_default_forwarder_is_noop() {
327        let forwarder = CallbackHolder1::<i32>::new().clone_rc();
328        forwarder(7);
329    }
330
331    #[test]
332    fn callback_holder1_forwarder_uses_latest_callback() {
333        let holder = CallbackHolder1::<i32>::new();
334        let total = Rc::new(Cell::new(0));
335        let forwarder = holder.clone_rc();
336
337        let first_total = Rc::clone(&total);
338        holder.update(move |value| first_total.set(first_total.get() + value));
339        forwarder(2);
340
341        let second_total = Rc::clone(&total);
342        holder.update(move |value| second_total.set(second_total.get() + value * 5));
343        forwarder(3);
344
345        assert_eq!(total.get(), 17);
346    }
347
348    #[test]
349    fn callback_holder1_does_not_invoke_after_creator_scope_deactivation() {
350        let runtime = TestRuntime::new();
351        let scope = RecomposeScope::new_for_test(runtime.handle());
352        let holder = CallbackHolder1::<i32>::new();
353        let total = Rc::new(Cell::new(0));
354        let total_for_callback = Rc::clone(&total);
355        holder.update(move |value| total_for_callback.set(total_for_callback.get() + value));
356        holder.creator_scope.replace(Some(scope.clone()));
357        let forwarder = holder.clone_rc();
358
359        forwarder(3);
360        assert_eq!(total.get(), 3);
361
362        scope.deactivate();
363        forwarder(5);
364        assert_eq!(
365            total.get(),
366            3,
367            "argument callbacks cannot outlive their active composition owner",
368        );
369    }
370}