Skip to main content

cranpose_core/
callbacks.rs

1use crate::composer_context;
2use crate::{Composer, ComposerCore, RecomposeScope};
3use std::cell::RefCell;
4use std::rc::Rc;
5
6pub struct ParamState<T> {
7    pub(crate) value: Option<T>,
8}
9
10impl<T> ParamState<T> {
11    pub fn update(&mut self, new_value: &T) -> bool
12    where
13        T: PartialEq + Clone,
14    {
15        match self.value.as_mut() {
16            Some(old) if old == new_value => false,
17            Some(old) => {
18                old.clone_from(new_value);
19                true
20            }
21            None => {
22                self.value = Some(new_value.clone());
23                true
24            }
25        }
26    }
27
28    pub fn value(&self) -> Option<T>
29    where
30        T: Clone,
31    {
32        self.value.clone()
33    }
34}
35
36/// ParamSlot holds function/closure parameters by ownership (no PartialEq/Clone required).
37/// Used by the #[composable] macro to store Fn-like parameters in the slot table.
38pub struct ParamSlot<T> {
39    val: RefCell<Option<T>>,
40}
41
42impl<T> Default for ParamSlot<T> {
43    fn default() -> Self {
44        Self {
45            val: RefCell::new(None),
46        }
47    }
48}
49
50impl<T> ParamSlot<T> {
51    pub fn set(&self, v: T) {
52        *self.val.borrow_mut() = Some(v);
53    }
54
55    /// Takes the value out temporarily for a recomposition callback.
56    pub fn take(&self) -> Option<T> {
57        self.val.borrow_mut().take()
58    }
59}
60
61type CallbackCell = Rc<RefCell<Option<Box<dyn FnMut()>>>>;
62type CallbackScopeCell = Rc<RefCell<Option<RecomposeScope>>>;
63
64struct CallbackScopeGuard {
65    core: Rc<ComposerCore>,
66}
67
68impl CallbackScopeGuard {
69    fn push(composer: &Composer, scope: RecomposeScope) -> Self {
70        composer.core.scope_stack.borrow_mut().push(scope);
71        Self {
72            core: composer.clone_core(),
73        }
74    }
75}
76
77impl Drop for CallbackScopeGuard {
78    fn drop(&mut self) {
79        self.core.scope_stack.borrow_mut().pop();
80    }
81}
82
83fn with_callback_scope<R>(scope: &CallbackScopeCell, f: impl FnOnce() -> R) -> R {
84    let captured_scope = scope.borrow().clone();
85    if let Some(saved_scope) = captured_scope {
86        if let Some(composer) = composer_context::current_composer() {
87            let _scope_guard = CallbackScopeGuard::push(&composer, saved_scope);
88            return f();
89        }
90    }
91
92    f()
93}
94
95fn callback_owner_scope(composer: &Composer) -> Option<RecomposeScope> {
96    composer.core.scope_stack.borrow().last().cloned()
97}
98
99#[derive(Clone)]
100pub struct CallbackHolder {
101    rc: CallbackCell,
102    creator_scope: CallbackScopeCell,
103}
104
105impl CallbackHolder {
106    /// Create a new holder with a no-op callback so that callers can immediately invoke it.
107    pub fn new() -> Self {
108        Self::default()
109    }
110
111    /// Replace the stored callback with a new closure provided by the caller.
112    pub fn update<F>(&self, f: F)
113    where
114        F: FnMut() + 'static,
115    {
116        self.update_boxed(Box::new(f));
117    }
118
119    /// Boxed form of [`Self::update`]: lets generated composable helpers take
120    /// callbacks type-erased at the public-fn boundary, so helper bodies are
121    /// compiled once instead of once per caller closure type.
122    pub fn update_boxed(&self, f: Box<dyn FnMut() + 'static>) {
123        *self.rc.borrow_mut() = Some(f);
124        *self.creator_scope.borrow_mut() =
125            composer_context::try_with_composer(callback_owner_scope).flatten();
126    }
127
128    /// Produce a forwarder closure that keeps the holder alive and forwards calls to it.
129    pub fn clone_rc(&self) -> impl Fn() + 'static {
130        let rc = self.rc.clone();
131        let creator_scope = self.creator_scope.clone();
132        move || {
133            with_callback_scope(&creator_scope, || {
134                if let Some(callback) = rc.borrow_mut().as_mut() {
135                    callback();
136                }
137            });
138        }
139    }
140}
141
142impl Default for CallbackHolder {
143    fn default() -> Self {
144        Self {
145            rc: Rc::new(RefCell::new(None)),
146            creator_scope: Rc::new(RefCell::new(None)),
147        }
148    }
149}
150
151/// CallbackHolder1 keeps the latest single-argument callback closure alive across recompositions.
152/// It mirrors [`CallbackHolder`] but supports callbacks that receive one argument.
153#[derive(Clone)]
154pub struct CallbackHolder1<A: 'static> {
155    #[allow(clippy::type_complexity)]
156    rc: Rc<RefCell<Option<Box<dyn FnMut(A)>>>>,
157    creator_scope: CallbackScopeCell,
158}
159
160impl<A: 'static> CallbackHolder1<A> {
161    /// Create a new holder with a no-op callback so callers can invoke it immediately.
162    pub fn new() -> Self {
163        Self::default()
164    }
165
166    /// Replace the stored callback with a new closure provided by the caller.
167    pub fn update<F>(&self, f: F)
168    where
169        F: FnMut(A) + 'static,
170    {
171        *self.rc.borrow_mut() = Some(Box::new(f));
172        *self.creator_scope.borrow_mut() =
173            composer_context::try_with_composer(callback_owner_scope).flatten();
174    }
175
176    /// Produce a forwarder closure that keeps the holder alive and forwards calls to it.
177    pub fn clone_rc(&self) -> impl Fn(A) + 'static {
178        let rc = self.rc.clone();
179        let creator_scope = self.creator_scope.clone();
180        move |arg| {
181            with_callback_scope(&creator_scope, || {
182                if let Some(callback) = rc.borrow_mut().as_mut() {
183                    callback(arg);
184                }
185            });
186        }
187    }
188}
189
190impl<A: 'static> Default for CallbackHolder1<A> {
191    fn default() -> Self {
192        Self {
193            rc: Rc::new(RefCell::new(None)),
194            creator_scope: Rc::new(RefCell::new(None)),
195        }
196    }
197}
198
199pub struct ReturnSlot<T> {
200    value: Option<T>,
201}
202
203impl<T: Clone> ReturnSlot<T> {
204    pub fn store(&mut self, value: T) {
205        self.value = Some(value);
206    }
207
208    pub fn get(&self) -> Option<T> {
209        self.value.clone()
210    }
211}
212
213impl<T> Default for ParamState<T> {
214    fn default() -> Self {
215        Self { value: None }
216    }
217}
218
219impl<T> Default for ReturnSlot<T> {
220    fn default() -> Self {
221        Self { value: None }
222    }
223}
224
225#[cfg(test)]
226mod callback_holder_tests {
227    use super::{CallbackHolder, CallbackHolder1, ParamSlot};
228    use std::cell::Cell;
229    use std::rc::Rc;
230
231    #[test]
232    fn param_slot_take_reports_absence_instead_of_panicking() {
233        let slot = ParamSlot::default();
234
235        assert_eq!(slot.take(), None);
236        slot.set(11);
237        assert_eq!(slot.take(), Some(11));
238        assert_eq!(slot.take(), None);
239    }
240
241    #[test]
242    fn callback_holder_default_forwarder_is_noop() {
243        let forwarder = CallbackHolder::new().clone_rc();
244        forwarder();
245    }
246
247    #[test]
248    fn callback_holder_forwarder_uses_latest_callback() {
249        let holder = CallbackHolder::new();
250        let total = Rc::new(Cell::new(0));
251        let forwarder = holder.clone_rc();
252
253        let first_total = Rc::clone(&total);
254        holder.update(move || first_total.set(first_total.get() + 1));
255        forwarder();
256
257        let second_total = Rc::clone(&total);
258        holder.update(move || second_total.set(second_total.get() + 10));
259        forwarder();
260
261        assert_eq!(total.get(), 11);
262    }
263
264    #[test]
265    fn callback_holder1_default_forwarder_is_noop() {
266        let forwarder = CallbackHolder1::<i32>::new().clone_rc();
267        forwarder(7);
268    }
269
270    #[test]
271    fn callback_holder1_forwarder_uses_latest_callback() {
272        let holder = CallbackHolder1::<i32>::new();
273        let total = Rc::new(Cell::new(0));
274        let forwarder = holder.clone_rc();
275
276        let first_total = Rc::clone(&total);
277        holder.update(move |value| first_total.set(first_total.get() + value));
278        forwarder(2);
279
280        let second_total = Rc::clone(&total);
281        holder.update(move |value| second_total.set(second_total.get() + value * 5));
282        forwarder(3);
283
284        assert_eq!(total.get(), 17);
285    }
286}