Skip to main content

cranpose_core/
callbacks.rs

1use std::{
2    cell::{Cell, RefCell},
3    rc::Rc,
4};
5
6use crate::{Composer, ComposerCore, Key, RecomposeScope, ValueSlotHandle, composer_context};
7
8/// The parameters of one composable call, kept in a slot between recompositions so the call can
9/// skip its body when they are unchanged. Generated by the `#[composable]` macro.
10pub struct ParamState<T> {
11    pub(crate) value: Option<T>,
12}
13
14/// A parameter that points at a shared allocation: two that point at the
15/// same one are equal without comparing what it holds.
16pub trait SharedParam {
17    /// Whether `self` and `other` point at the same allocation.
18    fn same_allocation(&self, other: &Self) -> bool;
19}
20
21impl<T: ?Sized> SharedParam for Rc<T> {
22    fn same_allocation(&self, other: &Self) -> bool {
23        Self::ptr_eq(self, other)
24    }
25}
26
27impl<T: ?Sized> SharedParam for std::sync::Arc<T> {
28    fn same_allocation(&self, other: &Self) -> bool {
29        Self::ptr_eq(self, other)
30    }
31}
32
33impl<T> ParamState<T> {
34    /// Brings a call's stored parameters up to date: `fresh` stores them on
35    /// the call's first composition, and `refresh` updates each stored field
36    /// that differs and says whether any did. Unchanged fields are not
37    /// cloned again.
38    pub fn update_fields(
39        &mut self,
40        fresh: impl FnOnce() -> T,
41        refresh: impl FnOnce(&mut T) -> bool,
42    ) -> bool {
43        match self.value.as_mut() {
44            Some(stored) => refresh(stored),
45            None => {
46                self.value = Some(fresh());
47                true
48            }
49        }
50    }
51
52    /// Returns a clone of the stored parameters, or `None` before any composition stored them.
53    pub fn value(&self) -> Option<T>
54    where
55        T: Clone,
56    {
57        self.value.clone()
58    }
59}
60
61/// A call's stored parameters and the value its body last returned, held in
62/// one slot: a call that skips its body finds both with one lookup.
63pub struct ParamReturnState<T, R> {
64    params: ParamState<T>,
65    returned: Option<R>,
66}
67
68impl<T, R> ParamReturnState<T, R> {
69    /// [`ParamState::update_fields`] for the stored parameters.
70    pub fn update_fields(
71        &mut self,
72        fresh: impl FnOnce() -> T,
73        refresh: impl FnOnce(&mut T) -> bool,
74    ) -> bool {
75        self.params.update_fields(fresh, refresh)
76    }
77
78    /// Returns a clone of the stored parameters, or `None` before any composition stored them.
79    pub fn value(&self) -> Option<T>
80    where
81        T: Clone,
82    {
83        self.params.value()
84    }
85
86    /// The value the call's body returned last.
87    pub fn returned(&self) -> Option<R>
88    where
89        R: Clone,
90    {
91        self.returned.clone()
92    }
93
94    /// Stores the value the call's body returned, replacing the previous one.
95    pub fn store_return(&mut self, value: R) {
96        self.returned = Some(value);
97    }
98}
99
100impl<T, R> Default for ParamReturnState<T, R> {
101    fn default() -> Self {
102        Self {
103            params: ParamState::default(),
104            returned: None,
105        }
106    }
107}
108
109/// Updates one stored parameter to `new` and says whether it differed,
110/// reusing the stored value's allocations where `Clone::clone_from` can.
111pub fn refresh_param<T: PartialEq + Clone>(stored: &mut T, new: &T) -> bool {
112    if stored == new {
113        return false;
114    }
115    stored.clone_from(new);
116    true
117}
118
119/// Stores `new` as a parameter without comparing, and says it changed: for a
120/// call that runs its body whatever its parameters, as one with a callback
121/// does, a comparison would decide nothing. A `Modifier` compares element by
122/// element.
123pub fn store_param<T: Clone>(stored: &mut T, new: &T) -> bool {
124    stored.clone_from(new);
125    true
126}
127
128/// [`refresh_param`] for an `Rc` or `Arc` parameter: the same allocation is
129/// unchanged without comparing its contents, which `PartialEq` on a pointer
130/// to a type that is not `Eq` otherwise walks in full on every recomposition.
131pub fn refresh_shared_param<T: SharedParam + PartialEq + Clone>(stored: &mut T, new: &T) -> bool {
132    if stored.same_allocation(new) || stored == new {
133        return false;
134    }
135    stored.clone_from(new);
136    true
137}
138
139/// ParamSlot holds function/closure parameters by ownership (no PartialEq/Clone required).
140/// Used by the `#[composable]` macro to store Fn-like parameters in the slot table.
141pub struct ParamSlot<T> {
142    val: RefCell<Option<T>>,
143}
144
145impl<T> Default for ParamSlot<T> {
146    fn default() -> Self {
147        Self {
148            val: RefCell::new(None),
149        }
150    }
151}
152
153impl<T> ParamSlot<T> {
154    /// Stores `v` as the held value, replacing the previous one.
155    pub fn set(&self, v: T) {
156        *self.val.borrow_mut() = Some(v);
157    }
158
159    /// Takes the value out temporarily for a recomposition callback.
160    pub fn take(&self) -> Option<T> {
161        self.val.borrow_mut().take()
162    }
163}
164
165type CallbackScopeCell = RefCell<Option<RecomposeScope>>;
166
167// What a holder shares with the forwarders it hands out: the callback stored
168// last, and its owner.
169struct CallbackShared<F: ?Sized> {
170    owner: CallbackOwner,
171    callback: RefCell<Option<Box<F>>>,
172}
173
174// The part of a held callback that does not depend on its signature, so
175// every forwarder runs one copy of the code that enters a call.
176#[derive(Default)]
177struct CallbackOwner {
178    // The scope that stored the callback.
179    creator_scope: CallbackScopeCell,
180    // Whether the callback ever ran outside an event handler.
181    renders: Cell<bool>,
182}
183
184// What a forwarder holds while the callback runs: the scope that stored it,
185// pushed on the composer's scope stack when a composer is installed.
186struct CallbackEntry {
187    _scope: Option<CallbackScopeGuard>,
188}
189
190impl CallbackOwner {
191    // Notes the run and enters the scope that stored the callback, while
192    // that scope is active. A run while composing, or outside the snapshot
193    // event handlers and effects run in, is a run during composition, layout
194    // or drawing, where what the callback does reaches the picture.
195    #[inline(never)]
196    fn enter(&self) -> Option<CallbackEntry> {
197        if !callback_owner_is_active(&self.creator_scope) {
198            return None;
199        }
200        if !self.renders.get()
201            && (composer_context::has_composer() || !crate::in_applied_snapshot())
202        {
203            self.renders.set(true);
204        }
205        let scope = self.creator_scope.borrow().clone().and_then(|scope| {
206            composer_context::current_composer()
207                .map(|composer| CallbackScopeGuard::push(&composer, scope))
208        });
209        Some(CallbackEntry { _scope: scope })
210    }
211}
212
213struct CallbackScopeGuard {
214    core: Rc<ComposerCore>,
215}
216
217impl CallbackScopeGuard {
218    fn push(composer: &Composer, scope: RecomposeScope) -> Self {
219        composer.core.scope_stack.borrow_mut().push(scope);
220        Self {
221            core: composer.clone_core(),
222        }
223    }
224}
225
226impl Drop for CallbackScopeGuard {
227    fn drop(&mut self) {
228        self.core.scope_stack.borrow_mut().pop();
229    }
230}
231
232fn callback_owner_is_active(scope: &CallbackScopeCell) -> bool {
233    scope
234        .borrow()
235        .as_ref()
236        .is_none_or(RecomposeScope::is_effectively_active)
237}
238
239fn callback_owner_scope(composer: &Composer) -> Option<RecomposeScope> {
240    composer.core.scope_stack.borrow().last().cloned()
241}
242
243/// Keeps a composable's callback parameter across recompositions: its forwarders always call the
244/// callback stored last. `F` is the callback's trait object, as `dyn FnMut(usize)`.
245pub struct CallbackHolder<F: ?Sized + 'static = dyn FnMut()> {
246    shared: Rc<CallbackShared<F>>,
247}
248
249impl<F: ?Sized + 'static> CallbackHolder<F> {
250    /// A holder with no callback yet; its forwarders do nothing until one is stored.
251    pub fn new() -> Self {
252        Self::default()
253    }
254
255    /// Stores `callback` in place of the one stored last. Generated composable helpers take
256    /// callbacks type-erased at the public-fn boundary, so helper bodies are compiled once
257    /// instead of once per caller closure type.
258    pub fn update_boxed(&self, callback: Box<F>) {
259        *self.shared.callback.borrow_mut() = Some(callback);
260        *self.shared.owner.creator_scope.borrow_mut() =
261            composer_context::try_with_composer(callback_owner_scope).flatten();
262    }
263
264    /// Whether the callback ever ran outside an event handler or an effect: during
265    /// composition, layout or drawing. A call whose other parameters are unchanged skips its
266    /// body when its new callbacks never did, because its forwarders call the new ones.
267    pub fn renders(&self) -> bool {
268        self.shared.owner.renders.get()
269    }
270
271    /// The handle a forwarder closure calls the stored callback through.
272    pub fn forwarder(&self) -> CallbackForwarder<F> {
273        CallbackForwarder {
274            shared: Rc::clone(&self.shared),
275        }
276    }
277
278    /// Stores a call's callback in its next parameter slot, and says
279    /// whether the callback stored there before ever ran outside an event
280    /// handler. One copy per callback signature serves every composable.
281    #[doc(hidden)]
282    pub fn __hold(
283        composer: &Composer,
284        source: Key,
285        callback: Box<F>,
286    ) -> (ValueSlotHandle<'_, Self>, bool) {
287        composer.__update_param_slot(source, Self::new, |holder: &mut Self| {
288            holder.update_boxed(callback);
289            holder.renders()
290        })
291    }
292
293    /// The holder in a call's next parameter slot, for a recomposition
294    /// that runs the call's body again.
295    #[doc(hidden)]
296    pub fn __slot(composer: &Composer, source: Key) -> ValueSlotHandle<'_, Self> {
297        composer.__use_param_slot(source, Self::new)
298    }
299
300    /// A forwarder for the holder in `slot`.
301    #[doc(hidden)]
302    pub fn __forwarder<'pass>(
303        composer: &'pass Composer,
304        slot: ValueSlotHandle<'pass, Self>,
305    ) -> CallbackForwarder<F> {
306        composer.with_slot_value(slot, Self::forwarder)
307    }
308}
309
310impl<F: ?Sized + 'static> Clone for CallbackHolder<F> {
311    fn clone(&self) -> Self {
312        Self {
313            shared: Rc::clone(&self.shared),
314        }
315    }
316}
317
318impl CallbackHolder {
319    /// Stores `callback` in place of the one stored last.
320    pub fn update(&self, callback: impl FnMut() + 'static) {
321        self.update_boxed(Box::new(callback));
322    }
323
324    /// A forwarder that calls the callback stored last.
325    pub fn clone_rc(&self) -> impl Fn() + 'static + use<> {
326        let forwarder = self.forwarder();
327        move || forwarder.call(|callback| callback())
328    }
329}
330
331impl<F: ?Sized + 'static> Default for CallbackHolder<F> {
332    fn default() -> Self {
333        Self {
334            shared: Rc::new(CallbackShared {
335                owner: CallbackOwner::default(),
336                callback: RefCell::new(None),
337            }),
338        }
339    }
340}
341
342/// A forwarder's handle on a [`CallbackHolder`], which calls the callback stored last in the
343/// scope that stored it.
344pub struct CallbackForwarder<F: ?Sized + 'static> {
345    shared: Rc<CallbackShared<F>>,
346}
347
348impl<F: ?Sized + 'static> CallbackForwarder<F> {
349    /// Calls the callback stored last; once the scope that stored it is no longer
350    /// active, does nothing.
351    pub fn call(&self, call: impl FnOnce(&mut F)) {
352        let Some(_entry) = self.shared.owner.enter() else {
353            return;
354        };
355        if let Some(callback) = self.shared.callback.borrow_mut().as_mut() {
356            call(callback);
357        }
358    }
359}
360
361impl<F: ?Sized + 'static> Clone for CallbackForwarder<F> {
362    fn clone(&self) -> Self {
363        Self {
364            shared: Rc::clone(&self.shared),
365        }
366    }
367}
368
369/// Holds the value a composable call returned, so a later composition that skips the call can
370/// return it again.
371pub struct ReturnSlot<T> {
372    value: Option<T>,
373}
374
375impl<T: Clone> ReturnSlot<T> {
376    /// Stores `value` as the call's latest return value.
377    pub fn store(&mut self, value: T) {
378        self.value = Some(value);
379    }
380
381    /// Returns a clone of the stored return value, or `None` if the call has not returned one yet.
382    pub fn get(&self) -> Option<T> {
383        self.value.clone()
384    }
385}
386
387impl<T> Default for ParamState<T> {
388    fn default() -> Self {
389        Self { value: None }
390    }
391}
392
393impl<T> Default for ReturnSlot<T> {
394    fn default() -> Self {
395        Self { value: None }
396    }
397}
398
399#[cfg(test)]
400#[path = "tests/callbacks_callback_holder_tests.rs"]
401mod callback_holder_tests;
402
403#[cfg(test)]
404#[path = "tests/callbacks_param_state_tests.rs"]
405mod param_state_tests;