Skip to main content

cranpose_core/
callbacks.rs

1use std::{cell::RefCell, rc::Rc};
2
3use crate::{Composer, ComposerCore, RecomposeScope, composer_context};
4
5/// The parameters of one composable call, kept in a slot between recompositions so the call can
6/// skip its body when they are unchanged. Generated by the `#[composable]` macro.
7pub struct ParamState<T> {
8    pub(crate) value: Option<T>,
9}
10
11/// A parameter that points at a shared allocation: two that point at the
12/// same one are equal without comparing what it holds.
13pub trait SharedParam {
14    /// Whether `self` and `other` point at the same allocation.
15    fn same_allocation(&self, other: &Self) -> bool;
16}
17
18impl<T: ?Sized> SharedParam for Rc<T> {
19    fn same_allocation(&self, other: &Self) -> bool {
20        Rc::ptr_eq(self, other)
21    }
22}
23
24impl<T: ?Sized> SharedParam for std::sync::Arc<T> {
25    fn same_allocation(&self, other: &Self) -> bool {
26        std::sync::Arc::ptr_eq(self, other)
27    }
28}
29
30impl<T> ParamState<T> {
31    /// Brings a call's stored parameters up to date: `fresh` stores them on
32    /// the call's first composition, and `refresh` updates each stored field
33    /// that differs and says whether any did. Unchanged fields are not
34    /// cloned again.
35    pub fn update_fields(
36        &mut self,
37        fresh: impl FnOnce() -> T,
38        refresh: impl FnOnce(&mut T) -> bool,
39    ) -> bool {
40        match self.value.as_mut() {
41            Some(stored) => refresh(stored),
42            None => {
43                self.value = Some(fresh());
44                true
45            }
46        }
47    }
48
49    /// Returns a clone of the stored parameters, or `None` before any composition stored them.
50    pub fn value(&self) -> Option<T>
51    where
52        T: Clone,
53    {
54        self.value.clone()
55    }
56}
57
58/// A call's stored parameters and the value its body last returned, held in
59/// one slot: a call that skips its body finds both with one lookup.
60pub struct ParamReturnState<T, R> {
61    params: ParamState<T>,
62    returned: Option<R>,
63}
64
65impl<T, R> ParamReturnState<T, R> {
66    /// [`ParamState::update_fields`] for the stored parameters.
67    pub fn update_fields(
68        &mut self,
69        fresh: impl FnOnce() -> T,
70        refresh: impl FnOnce(&mut T) -> bool,
71    ) -> bool {
72        self.params.update_fields(fresh, refresh)
73    }
74
75    /// Returns a clone of the stored parameters, or `None` before any composition stored them.
76    pub fn value(&self) -> Option<T>
77    where
78        T: Clone,
79    {
80        self.params.value()
81    }
82
83    /// The value the call's body returned last.
84    pub fn returned(&self) -> Option<R>
85    where
86        R: Clone,
87    {
88        self.returned.clone()
89    }
90
91    /// Stores the value the call's body returned, replacing the previous one.
92    pub fn store_return(&mut self, value: R) {
93        self.returned = Some(value);
94    }
95}
96
97impl<T, R> Default for ParamReturnState<T, R> {
98    fn default() -> Self {
99        Self {
100            params: ParamState::default(),
101            returned: None,
102        }
103    }
104}
105
106/// Updates one stored parameter to `new` and says whether it differed,
107/// reusing the stored value's allocations where `Clone::clone_from` can.
108pub fn refresh_param<T: PartialEq + Clone>(stored: &mut T, new: &T) -> bool {
109    if stored == new {
110        return false;
111    }
112    stored.clone_from(new);
113    true
114}
115
116/// [`refresh_param`] for an `Rc` or `Arc` parameter: the same allocation is
117/// unchanged without comparing its contents, which `PartialEq` on a pointer
118/// to a type that is not `Eq` otherwise walks in full on every recomposition.
119pub fn refresh_shared_param<T: SharedParam + PartialEq + Clone>(stored: &mut T, new: &T) -> bool {
120    if stored.same_allocation(new) || stored == new {
121        return false;
122    }
123    stored.clone_from(new);
124    true
125}
126
127/// ParamSlot holds function/closure parameters by ownership (no PartialEq/Clone required).
128/// Used by the `#[composable]` macro to store Fn-like parameters in the slot table.
129pub struct ParamSlot<T> {
130    val: RefCell<Option<T>>,
131}
132
133impl<T> Default for ParamSlot<T> {
134    fn default() -> Self {
135        Self {
136            val: RefCell::new(None),
137        }
138    }
139}
140
141impl<T> ParamSlot<T> {
142    /// Stores `v` as the held value, replacing the previous one.
143    pub fn set(&self, v: T) {
144        *self.val.borrow_mut() = Some(v);
145    }
146
147    /// Takes the value out temporarily for a recomposition callback.
148    pub fn take(&self) -> Option<T> {
149        self.val.borrow_mut().take()
150    }
151}
152
153type CallbackCell = Rc<RefCell<Option<Box<dyn FnMut()>>>>;
154type CallbackScopeCell = Rc<RefCell<Option<RecomposeScope>>>;
155
156struct CallbackScopeGuard {
157    core: Rc<ComposerCore>,
158}
159
160impl CallbackScopeGuard {
161    fn push(composer: &Composer, scope: RecomposeScope) -> Self {
162        composer.core.scope_stack.borrow_mut().push(scope);
163        Self {
164            core: composer.clone_core(),
165        }
166    }
167}
168
169impl Drop for CallbackScopeGuard {
170    fn drop(&mut self) {
171        self.core.scope_stack.borrow_mut().pop();
172    }
173}
174
175fn with_callback_scope<R>(scope: &CallbackScopeCell, f: impl FnOnce() -> R) -> R {
176    let captured_scope = scope.borrow().clone();
177    if let Some(saved_scope) = captured_scope
178        && let Some(composer) = composer_context::current_composer()
179    {
180        let _scope_guard = CallbackScopeGuard::push(&composer, saved_scope);
181        return f();
182    }
183
184    f()
185}
186
187fn callback_owner_is_active(scope: &CallbackScopeCell) -> bool {
188    scope
189        .borrow()
190        .as_ref()
191        .is_none_or(RecomposeScope::is_effectively_active)
192}
193
194fn callback_owner_scope(composer: &Composer) -> Option<RecomposeScope> {
195    composer.core.scope_stack.borrow().last().cloned()
196}
197
198/// Keeps the latest zero-argument callback alive across recompositions. The forwarder from
199/// `clone_rc` always calls the callback stored last, and does nothing once the scope that stored
200/// it is no longer active.
201#[derive(Clone)]
202pub struct CallbackHolder {
203    rc: CallbackCell,
204    creator_scope: CallbackScopeCell,
205}
206
207impl CallbackHolder {
208    /// Create a new holder with a no-op callback so that callers can immediately invoke it.
209    pub fn new() -> Self {
210        Self::default()
211    }
212
213    /// Replace the stored callback with a new closure provided by the caller.
214    pub fn update<F>(&self, f: F)
215    where
216        F: FnMut() + 'static,
217    {
218        self.update_boxed(Box::new(f));
219    }
220
221    /// Boxed form of [`Self::update`]: lets generated composable helpers take
222    /// callbacks type-erased at the public-fn boundary, so helper bodies are
223    /// compiled once instead of once per caller closure type.
224    pub fn update_boxed(&self, f: Box<dyn FnMut() + 'static>) {
225        *self.rc.borrow_mut() = Some(f);
226        *self.creator_scope.borrow_mut() =
227            composer_context::try_with_composer(callback_owner_scope).flatten();
228    }
229
230    /// Produce a forwarder closure that keeps the holder alive and forwards calls to it.
231    pub fn clone_rc(&self) -> impl Fn() + 'static + use<> {
232        let rc = self.rc.clone();
233        let creator_scope = self.creator_scope.clone();
234        move || {
235            if !callback_owner_is_active(&creator_scope) {
236                return;
237            }
238            with_callback_scope(&creator_scope, || {
239                if let Some(callback) = rc.borrow_mut().as_mut() {
240                    callback();
241                }
242            });
243        }
244    }
245}
246
247impl Default for CallbackHolder {
248    fn default() -> Self {
249        Self {
250            rc: Rc::new(RefCell::new(None)),
251            creator_scope: Rc::new(RefCell::new(None)),
252        }
253    }
254}
255
256/// CallbackHolder1 keeps the latest single-argument callback closure alive across recompositions.
257/// It mirrors [`CallbackHolder`] but supports callbacks that receive one argument.
258#[derive(Clone)]
259pub struct CallbackHolder1<A: 'static> {
260    #[expect(clippy::type_complexity)]
261    rc: Rc<RefCell<Option<Box<dyn FnMut(A)>>>>,
262    creator_scope: CallbackScopeCell,
263}
264
265impl<A: 'static> CallbackHolder1<A> {
266    /// Create a new holder with a no-op callback so callers can invoke it immediately.
267    pub fn new() -> Self {
268        Self::default()
269    }
270
271    /// Replace the stored callback with a new closure provided by the caller.
272    pub fn update<F>(&self, f: F)
273    where
274        F: FnMut(A) + 'static,
275    {
276        *self.rc.borrow_mut() = Some(Box::new(f));
277        *self.creator_scope.borrow_mut() =
278            composer_context::try_with_composer(callback_owner_scope).flatten();
279    }
280
281    /// Produce a forwarder closure that keeps the holder alive and forwards calls to it.
282    pub fn clone_rc(&self) -> impl Fn(A) + 'static {
283        let rc = self.rc.clone();
284        let creator_scope = self.creator_scope.clone();
285        move |arg| {
286            if !callback_owner_is_active(&creator_scope) {
287                return;
288            }
289            with_callback_scope(&creator_scope, || {
290                if let Some(callback) = rc.borrow_mut().as_mut() {
291                    callback(arg);
292                }
293            });
294        }
295    }
296}
297
298impl<A: 'static> Default for CallbackHolder1<A> {
299    fn default() -> Self {
300        Self {
301            rc: Rc::new(RefCell::new(None)),
302            creator_scope: Rc::new(RefCell::new(None)),
303        }
304    }
305}
306
307/// Holds the value a composable call returned, so a later composition that skips the call can
308/// return it again.
309pub struct ReturnSlot<T> {
310    value: Option<T>,
311}
312
313impl<T: Clone> ReturnSlot<T> {
314    /// Stores `value` as the call's latest return value.
315    pub fn store(&mut self, value: T) {
316        self.value = Some(value);
317    }
318
319    /// Returns a clone of the stored return value, or `None` if the call has not returned one yet.
320    pub fn get(&self) -> Option<T> {
321        self.value.clone()
322    }
323}
324
325impl<T> Default for ParamState<T> {
326    fn default() -> Self {
327        Self { value: None }
328    }
329}
330
331impl<T> Default for ReturnSlot<T> {
332    fn default() -> Self {
333        Self { value: None }
334    }
335}
336
337#[cfg(test)]
338#[path = "tests/callbacks_callback_holder_tests.rs"]
339mod callback_holder_tests;
340
341#[cfg(test)]
342#[path = "tests/callbacks_param_state_tests.rs"]
343mod param_state_tests;