Skip to main content

cranpose_core/
callbacks.rs

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