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/// Stores `new` as a parameter without comparing, and says it changed: for a
117/// call that runs its body whatever its parameters, as one with a callback
118/// does, a comparison would decide nothing. A `Modifier` compares element by
119/// element.
120pub fn store_param<T: Clone>(stored: &mut T, new: &T) -> bool {
121    stored.clone_from(new);
122    true
123}
124
125/// [`refresh_param`] for an `Rc` or `Arc` parameter: the same allocation is
126/// unchanged without comparing its contents, which `PartialEq` on a pointer
127/// to a type that is not `Eq` otherwise walks in full on every recomposition.
128pub fn refresh_shared_param<T: SharedParam + PartialEq + Clone>(stored: &mut T, new: &T) -> bool {
129    if stored.same_allocation(new) || stored == new {
130        return false;
131    }
132    stored.clone_from(new);
133    true
134}
135
136/// ParamSlot holds function/closure parameters by ownership (no PartialEq/Clone required).
137/// Used by the `#[composable]` macro to store Fn-like parameters in the slot table.
138pub struct ParamSlot<T> {
139    val: RefCell<Option<T>>,
140}
141
142impl<T> Default for ParamSlot<T> {
143    fn default() -> Self {
144        Self {
145            val: RefCell::new(None),
146        }
147    }
148}
149
150impl<T> ParamSlot<T> {
151    /// Stores `v` as the held value, replacing the previous one.
152    pub fn set(&self, v: T) {
153        *self.val.borrow_mut() = Some(v);
154    }
155
156    /// Takes the value out temporarily for a recomposition callback.
157    pub fn take(&self) -> Option<T> {
158        self.val.borrow_mut().take()
159    }
160}
161
162type CallbackCell = Rc<RefCell<Option<Box<dyn FnMut()>>>>;
163type CallbackScopeCell = Rc<RefCell<Option<RecomposeScope>>>;
164
165struct CallbackScopeGuard {
166    core: Rc<ComposerCore>,
167}
168
169impl CallbackScopeGuard {
170    fn push(composer: &Composer, scope: RecomposeScope) -> Self {
171        composer.core.scope_stack.borrow_mut().push(scope);
172        Self {
173            core: composer.clone_core(),
174        }
175    }
176}
177
178impl Drop for CallbackScopeGuard {
179    fn drop(&mut self) {
180        self.core.scope_stack.borrow_mut().pop();
181    }
182}
183
184fn with_callback_scope<R>(scope: &CallbackScopeCell, f: impl FnOnce() -> R) -> R {
185    let captured_scope = scope.borrow().clone();
186    if let Some(saved_scope) = captured_scope
187        && let Some(composer) = composer_context::current_composer()
188    {
189        let _scope_guard = CallbackScopeGuard::push(&composer, saved_scope);
190        return f();
191    }
192
193    f()
194}
195
196fn callback_owner_is_active(scope: &CallbackScopeCell) -> bool {
197    scope
198        .borrow()
199        .as_ref()
200        .is_none_or(RecomposeScope::is_effectively_active)
201}
202
203fn callback_owner_scope(composer: &Composer) -> Option<RecomposeScope> {
204    composer.core.scope_stack.borrow().last().cloned()
205}
206
207/// Keeps the latest zero-argument callback alive across recompositions. The forwarder from
208/// `clone_rc` always calls the callback stored last, and does nothing once the scope that stored
209/// it is no longer active.
210#[derive(Clone)]
211pub struct CallbackHolder {
212    rc: CallbackCell,
213    creator_scope: CallbackScopeCell,
214}
215
216impl CallbackHolder {
217    /// Create a new holder with a no-op callback so that callers can immediately invoke it.
218    pub fn new() -> Self {
219        Self::default()
220    }
221
222    /// Replace the stored callback with a new closure provided by the caller.
223    pub fn update<F>(&self, f: F)
224    where
225        F: FnMut() + 'static,
226    {
227        self.update_boxed(Box::new(f));
228    }
229
230    /// Boxed form of [`Self::update`]: lets generated composable helpers take
231    /// callbacks type-erased at the public-fn boundary, so helper bodies are
232    /// compiled once instead of once per caller closure type.
233    pub fn update_boxed(&self, f: Box<dyn FnMut() + 'static>) {
234        *self.rc.borrow_mut() = Some(f);
235        *self.creator_scope.borrow_mut() =
236            composer_context::try_with_composer(callback_owner_scope).flatten();
237    }
238
239    /// Produce a forwarder closure that keeps the holder alive and forwards calls to it.
240    pub fn clone_rc(&self) -> impl Fn() + 'static + use<> {
241        let rc = self.rc.clone();
242        let creator_scope = self.creator_scope.clone();
243        move || {
244            if !callback_owner_is_active(&creator_scope) {
245                return;
246            }
247            with_callback_scope(&creator_scope, || {
248                if let Some(callback) = rc.borrow_mut().as_mut() {
249                    callback();
250                }
251            });
252        }
253    }
254}
255
256impl Default for CallbackHolder {
257    fn default() -> Self {
258        Self {
259            rc: Rc::new(RefCell::new(None)),
260            creator_scope: Rc::new(RefCell::new(None)),
261        }
262    }
263}
264
265/// CallbackHolder1 keeps the latest single-argument callback closure alive across recompositions.
266/// It mirrors [`CallbackHolder`] but supports callbacks that receive one argument.
267#[derive(Clone)]
268pub struct CallbackHolder1<A: 'static> {
269    #[expect(clippy::type_complexity)]
270    rc: Rc<RefCell<Option<Box<dyn FnMut(A)>>>>,
271    creator_scope: CallbackScopeCell,
272}
273
274impl<A: 'static> CallbackHolder1<A> {
275    /// Create a new holder with a no-op callback so callers can invoke it immediately.
276    pub fn new() -> Self {
277        Self::default()
278    }
279
280    /// Replace the stored callback with a new closure provided by the caller.
281    pub fn update<F>(&self, f: F)
282    where
283        F: FnMut(A) + 'static,
284    {
285        *self.rc.borrow_mut() = Some(Box::new(f));
286        *self.creator_scope.borrow_mut() =
287            composer_context::try_with_composer(callback_owner_scope).flatten();
288    }
289
290    /// Produce a forwarder closure that keeps the holder alive and forwards calls to it.
291    pub fn clone_rc(&self) -> impl Fn(A) + 'static {
292        let rc = self.rc.clone();
293        let creator_scope = self.creator_scope.clone();
294        move |arg| {
295            if !callback_owner_is_active(&creator_scope) {
296                return;
297            }
298            with_callback_scope(&creator_scope, || {
299                if let Some(callback) = rc.borrow_mut().as_mut() {
300                    callback(arg);
301                }
302            });
303        }
304    }
305}
306
307impl<A: 'static> Default for CallbackHolder1<A> {
308    fn default() -> Self {
309        Self {
310            rc: Rc::new(RefCell::new(None)),
311            creator_scope: Rc::new(RefCell::new(None)),
312        }
313    }
314}
315
316/// Holds the value a composable call returned, so a later composition that skips the call can
317/// return it again.
318pub struct ReturnSlot<T> {
319    value: Option<T>,
320}
321
322impl<T: Clone> ReturnSlot<T> {
323    /// Stores `value` as the call's latest return value.
324    pub fn store(&mut self, value: T) {
325        self.value = Some(value);
326    }
327
328    /// Returns a clone of the stored return value, or `None` if the call has not returned one yet.
329    pub fn get(&self) -> Option<T> {
330        self.value.clone()
331    }
332}
333
334impl<T> Default for ParamState<T> {
335    fn default() -> Self {
336        Self { value: None }
337    }
338}
339
340impl<T> Default for ReturnSlot<T> {
341    fn default() -> Self {
342        Self { value: None }
343    }
344}
345
346#[cfg(test)]
347#[path = "tests/callbacks_callback_holder_tests.rs"]
348mod callback_holder_tests;
349
350#[cfg(test)]
351#[path = "tests/callbacks_param_state_tests.rs"]
352mod param_state_tests;