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 recomposition callback)
56    pub fn take(&self) -> T {
57        self.val
58            .borrow_mut()
59            .take()
60            .expect("ParamSlot take() called before set")
61    }
62}
63
64type CallbackCell = Rc<RefCell<Option<Box<dyn FnMut()>>>>;
65type CallbackScopeCell = Rc<RefCell<Option<RecomposeScope>>>;
66
67struct CallbackScopeGuard {
68    core: Rc<ComposerCore>,
69}
70
71impl CallbackScopeGuard {
72    fn push(composer: &Composer, scope: RecomposeScope) -> Self {
73        composer.core.scope_stack.borrow_mut().push(scope);
74        Self {
75            core: composer.clone_core(),
76        }
77    }
78}
79
80impl Drop for CallbackScopeGuard {
81    fn drop(&mut self) {
82        self.core.scope_stack.borrow_mut().pop();
83    }
84}
85
86fn with_callback_scope<R>(scope: &CallbackScopeCell, f: impl FnOnce() -> R) -> R {
87    let captured_scope = scope.borrow().clone();
88    let mut f = Some(f);
89    if let Some(saved_scope) = captured_scope {
90        if let Some(result) = composer_context::try_with_composer(|composer| {
91            let _scope_guard = CallbackScopeGuard::push(composer, saved_scope);
92            f.take().expect("callback closure already taken")()
93        }) {
94            return result;
95        }
96    }
97
98    f.take().expect("callback closure already taken")()
99}
100
101fn callback_owner_scope(composer: &Composer) -> Option<RecomposeScope> {
102    composer.core.scope_stack.borrow().last().cloned()
103}
104
105#[derive(Clone)]
106pub struct CallbackHolder {
107    rc: CallbackCell,
108    creator_scope: CallbackScopeCell,
109}
110
111impl CallbackHolder {
112    /// Create a new holder with a no-op callback so that callers can immediately invoke it.
113    pub fn new() -> Self {
114        Self::default()
115    }
116
117    /// Replace the stored callback with a new closure provided by the caller.
118    pub fn update<F>(&self, f: F)
119    where
120        F: FnMut() + 'static,
121    {
122        *self.rc.borrow_mut() = Some(Box::new(f));
123        *self.creator_scope.borrow_mut() =
124            composer_context::try_with_composer(callback_owner_scope).flatten();
125    }
126
127    /// Produce a forwarder closure that keeps the holder alive and forwards calls to it.
128    pub fn clone_rc(&self) -> impl Fn() + 'static {
129        let rc = self.rc.clone();
130        let creator_scope = self.creator_scope.clone();
131        move || {
132            with_callback_scope(&creator_scope, || {
133                if let Some(callback) = rc.borrow_mut().as_mut() {
134                    callback();
135                }
136            });
137        }
138    }
139}
140
141impl Default for CallbackHolder {
142    fn default() -> Self {
143        Self {
144            rc: Rc::new(RefCell::new(None)),
145            creator_scope: Rc::new(RefCell::new(None)),
146        }
147    }
148}
149
150/// CallbackHolder1 keeps the latest single-argument callback closure alive across recompositions.
151/// It mirrors [`CallbackHolder`] but supports callbacks that receive one argument.
152#[derive(Clone)]
153pub struct CallbackHolder1<A: 'static> {
154    #[allow(clippy::type_complexity)]
155    rc: Rc<RefCell<Option<Box<dyn FnMut(A)>>>>,
156    creator_scope: CallbackScopeCell,
157}
158
159impl<A: 'static> CallbackHolder1<A> {
160    /// Create a new holder with a no-op callback so callers can invoke it immediately.
161    pub fn new() -> Self {
162        Self::default()
163    }
164
165    /// Replace the stored callback with a new closure provided by the caller.
166    pub fn update<F>(&self, f: F)
167    where
168        F: FnMut(A) + 'static,
169    {
170        *self.rc.borrow_mut() = Some(Box::new(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(A) + 'static {
177        let rc = self.rc.clone();
178        let creator_scope = self.creator_scope.clone();
179        move |arg| {
180            with_callback_scope(&creator_scope, || {
181                if let Some(callback) = rc.borrow_mut().as_mut() {
182                    callback(arg);
183                }
184            });
185        }
186    }
187}
188
189impl<A: 'static> Default for CallbackHolder1<A> {
190    fn default() -> Self {
191        Self {
192            rc: Rc::new(RefCell::new(None)),
193            creator_scope: Rc::new(RefCell::new(None)),
194        }
195    }
196}
197
198pub struct ReturnSlot<T> {
199    value: Option<T>,
200}
201
202impl<T: Clone> ReturnSlot<T> {
203    pub fn store(&mut self, value: T) {
204        self.value = Some(value);
205    }
206
207    pub fn get(&self) -> Option<T> {
208        self.value.clone()
209    }
210}
211
212impl<T> Default for ParamState<T> {
213    fn default() -> Self {
214        Self { value: None }
215    }
216}
217
218impl<T> Default for ReturnSlot<T> {
219    fn default() -> Self {
220        Self { value: None }
221    }
222}
223
224#[cfg(test)]
225mod callback_holder_tests {
226    use super::{CallbackHolder, CallbackHolder1};
227    use std::cell::Cell;
228    use std::rc::Rc;
229
230    #[test]
231    fn callback_holder_default_forwarder_is_noop() {
232        let forwarder = CallbackHolder::new().clone_rc();
233        forwarder();
234    }
235
236    #[test]
237    fn callback_holder_forwarder_uses_latest_callback() {
238        let holder = CallbackHolder::new();
239        let total = Rc::new(Cell::new(0));
240        let forwarder = holder.clone_rc();
241
242        let first_total = Rc::clone(&total);
243        holder.update(move || first_total.set(first_total.get() + 1));
244        forwarder();
245
246        let second_total = Rc::clone(&total);
247        holder.update(move || second_total.set(second_total.get() + 10));
248        forwarder();
249
250        assert_eq!(total.get(), 11);
251    }
252
253    #[test]
254    fn callback_holder1_default_forwarder_is_noop() {
255        let forwarder = CallbackHolder1::<i32>::new().clone_rc();
256        forwarder(7);
257    }
258
259    #[test]
260    fn callback_holder1_forwarder_uses_latest_callback() {
261        let holder = CallbackHolder1::<i32>::new();
262        let total = Rc::new(Cell::new(0));
263        let forwarder = holder.clone_rc();
264
265        let first_total = Rc::clone(&total);
266        holder.update(move |value| first_total.set(first_total.get() + value));
267        forwarder(2);
268
269        let second_total = Rc::clone(&total);
270        holder.update(move |value| second_total.set(second_total.get() + value * 5));
271        forwarder(3);
272
273        assert_eq!(total.get(), 17);
274    }
275}