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/// Updates one stored parameter to `new` and says whether it differed,
56/// reusing the stored value's allocations where `Clone::clone_from` can.
57pub fn refresh_param<T: PartialEq + Clone>(stored: &mut T, new: &T) -> bool {
58    if stored == new {
59        return false;
60    }
61    stored.clone_from(new);
62    true
63}
64
65/// [`refresh_param`] for an `Rc` or `Arc` parameter: the same allocation is
66/// unchanged without comparing its contents, which `PartialEq` on a pointer
67/// to a type that is not `Eq` otherwise walks in full on every recomposition.
68pub fn refresh_shared_param<T: SharedParam + PartialEq + Clone>(stored: &mut T, new: &T) -> bool {
69    if stored.same_allocation(new) || stored == new {
70        return false;
71    }
72    stored.clone_from(new);
73    true
74}
75
76/// ParamSlot holds function/closure parameters by ownership (no PartialEq/Clone required).
77/// Used by the `#[composable]` macro to store Fn-like parameters in the slot table.
78pub struct ParamSlot<T> {
79    val: RefCell<Option<T>>,
80}
81
82impl<T> Default for ParamSlot<T> {
83    fn default() -> Self {
84        Self {
85            val: RefCell::new(None),
86        }
87    }
88}
89
90impl<T> ParamSlot<T> {
91    pub fn set(&self, v: T) {
92        *self.val.borrow_mut() = Some(v);
93    }
94
95    /// Takes the value out temporarily for a recomposition callback.
96    pub fn take(&self) -> Option<T> {
97        self.val.borrow_mut().take()
98    }
99}
100
101type CallbackCell = Rc<RefCell<Option<Box<dyn FnMut()>>>>;
102type CallbackScopeCell = Rc<RefCell<Option<RecomposeScope>>>;
103
104struct CallbackScopeGuard {
105    core: Rc<ComposerCore>,
106}
107
108impl CallbackScopeGuard {
109    fn push(composer: &Composer, scope: RecomposeScope) -> Self {
110        composer.core.scope_stack.borrow_mut().push(scope);
111        Self {
112            core: composer.clone_core(),
113        }
114    }
115}
116
117impl Drop for CallbackScopeGuard {
118    fn drop(&mut self) {
119        self.core.scope_stack.borrow_mut().pop();
120    }
121}
122
123fn with_callback_scope<R>(scope: &CallbackScopeCell, f: impl FnOnce() -> R) -> R {
124    let captured_scope = scope.borrow().clone();
125    if let Some(saved_scope) = captured_scope
126        && let Some(composer) = composer_context::current_composer()
127    {
128        let _scope_guard = CallbackScopeGuard::push(&composer, saved_scope);
129        return f();
130    }
131
132    f()
133}
134
135fn callback_owner_is_active(scope: &CallbackScopeCell) -> bool {
136    scope
137        .borrow()
138        .as_ref()
139        .is_none_or(RecomposeScope::is_effectively_active)
140}
141
142fn callback_owner_scope(composer: &Composer) -> Option<RecomposeScope> {
143    composer.core.scope_stack.borrow().last().cloned()
144}
145
146#[derive(Clone)]
147pub struct CallbackHolder {
148    rc: CallbackCell,
149    creator_scope: CallbackScopeCell,
150}
151
152impl CallbackHolder {
153    /// Create a new holder with a no-op callback so that callers can immediately invoke it.
154    pub fn new() -> Self {
155        Self::default()
156    }
157
158    /// Replace the stored callback with a new closure provided by the caller.
159    pub fn update<F>(&self, f: F)
160    where
161        F: FnMut() + 'static,
162    {
163        self.update_boxed(Box::new(f));
164    }
165
166    /// Boxed form of [`Self::update`]: lets generated composable helpers take
167    /// callbacks type-erased at the public-fn boundary, so helper bodies are
168    /// compiled once instead of once per caller closure type.
169    pub fn update_boxed(&self, f: Box<dyn FnMut() + 'static>) {
170        *self.rc.borrow_mut() = Some(f);
171        *self.creator_scope.borrow_mut() =
172            composer_context::try_with_composer(callback_owner_scope).flatten();
173    }
174
175    /// Produce a forwarder closure that keeps the holder alive and forwards calls to it.
176    pub fn clone_rc(&self) -> impl Fn() + 'static + use<> {
177        let rc = self.rc.clone();
178        let creator_scope = self.creator_scope.clone();
179        move || {
180            if !callback_owner_is_active(&creator_scope) {
181                return;
182            }
183            with_callback_scope(&creator_scope, || {
184                if let Some(callback) = rc.borrow_mut().as_mut() {
185                    callback();
186                }
187            });
188        }
189    }
190}
191
192impl Default for CallbackHolder {
193    fn default() -> Self {
194        Self {
195            rc: Rc::new(RefCell::new(None)),
196            creator_scope: Rc::new(RefCell::new(None)),
197        }
198    }
199}
200
201/// CallbackHolder1 keeps the latest single-argument callback closure alive across recompositions.
202/// It mirrors [`CallbackHolder`] but supports callbacks that receive one argument.
203#[derive(Clone)]
204pub struct CallbackHolder1<A: 'static> {
205    #[expect(clippy::type_complexity)]
206    rc: Rc<RefCell<Option<Box<dyn FnMut(A)>>>>,
207    creator_scope: CallbackScopeCell,
208}
209
210impl<A: 'static> CallbackHolder1<A> {
211    /// Create a new holder with a no-op callback so callers can invoke it immediately.
212    pub fn new() -> Self {
213        Self::default()
214    }
215
216    /// Replace the stored callback with a new closure provided by the caller.
217    pub fn update<F>(&self, f: F)
218    where
219        F: FnMut(A) + 'static,
220    {
221        *self.rc.borrow_mut() = Some(Box::new(f));
222        *self.creator_scope.borrow_mut() =
223            composer_context::try_with_composer(callback_owner_scope).flatten();
224    }
225
226    /// Produce a forwarder closure that keeps the holder alive and forwards calls to it.
227    pub fn clone_rc(&self) -> impl Fn(A) + 'static {
228        let rc = self.rc.clone();
229        let creator_scope = self.creator_scope.clone();
230        move |arg| {
231            if !callback_owner_is_active(&creator_scope) {
232                return;
233            }
234            with_callback_scope(&creator_scope, || {
235                if let Some(callback) = rc.borrow_mut().as_mut() {
236                    callback(arg);
237                }
238            });
239        }
240    }
241}
242
243impl<A: 'static> Default for CallbackHolder1<A> {
244    fn default() -> Self {
245        Self {
246            rc: Rc::new(RefCell::new(None)),
247            creator_scope: Rc::new(RefCell::new(None)),
248        }
249    }
250}
251
252pub struct ReturnSlot<T> {
253    value: Option<T>,
254}
255
256impl<T: Clone> ReturnSlot<T> {
257    pub fn store(&mut self, value: T) {
258        self.value = Some(value);
259    }
260
261    pub fn get(&self) -> Option<T> {
262        self.value.clone()
263    }
264}
265
266impl<T> Default for ParamState<T> {
267    fn default() -> Self {
268        Self { value: None }
269    }
270}
271
272impl<T> Default for ReturnSlot<T> {
273    fn default() -> Self {
274        Self { value: None }
275    }
276}
277
278#[cfg(test)]
279#[path = "tests/callbacks_callback_holder_tests.rs"]
280mod callback_holder_tests;
281
282#[cfg(test)]
283#[path = "tests/callbacks_param_state_tests.rs"]
284mod param_state_tests;