Skip to main content

cranpose_core/
callbacks.rs

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