cranpose_core/
callbacks.rs1use 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
36pub 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 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_is_active(scope: &CallbackScopeCell) -> bool {
96 scope
97 .borrow()
98 .as_ref()
99 .is_none_or(RecomposeScope::is_effectively_active)
100}
101
102fn callback_owner_scope(composer: &Composer) -> Option<RecomposeScope> {
103 composer.core.scope_stack.borrow().last().cloned()
104}
105
106#[derive(Clone)]
107pub struct CallbackHolder {
108 rc: CallbackCell,
109 creator_scope: CallbackScopeCell,
110}
111
112impl CallbackHolder {
113 pub fn new() -> Self {
115 Self::default()
116 }
117
118 pub fn update<F>(&self, f: F)
120 where
121 F: FnMut() + 'static,
122 {
123 self.update_boxed(Box::new(f));
124 }
125
126 pub fn update_boxed(&self, f: Box<dyn FnMut() + 'static>) {
130 *self.rc.borrow_mut() = Some(f);
131 *self.creator_scope.borrow_mut() =
132 composer_context::try_with_composer(callback_owner_scope).flatten();
133 }
134
135 pub fn clone_rc(&self) -> impl Fn() + 'static {
137 let rc = self.rc.clone();
138 let creator_scope = self.creator_scope.clone();
139 move || {
140 if !callback_owner_is_active(&creator_scope) {
141 return;
142 }
143 with_callback_scope(&creator_scope, || {
144 if let Some(callback) = rc.borrow_mut().as_mut() {
145 callback();
146 }
147 });
148 }
149 }
150}
151
152impl Default for CallbackHolder {
153 fn default() -> Self {
154 Self {
155 rc: Rc::new(RefCell::new(None)),
156 creator_scope: Rc::new(RefCell::new(None)),
157 }
158 }
159}
160
161#[derive(Clone)]
164pub struct CallbackHolder1<A: 'static> {
165 #[allow(clippy::type_complexity)]
166 rc: Rc<RefCell<Option<Box<dyn FnMut(A)>>>>,
167 creator_scope: CallbackScopeCell,
168}
169
170impl<A: 'static> CallbackHolder1<A> {
171 pub fn new() -> Self {
173 Self::default()
174 }
175
176 pub fn update<F>(&self, f: F)
178 where
179 F: FnMut(A) + 'static,
180 {
181 *self.rc.borrow_mut() = Some(Box::new(f));
182 *self.creator_scope.borrow_mut() =
183 composer_context::try_with_composer(callback_owner_scope).flatten();
184 }
185
186 pub fn clone_rc(&self) -> impl Fn(A) + 'static {
188 let rc = self.rc.clone();
189 let creator_scope = self.creator_scope.clone();
190 move |arg| {
191 if !callback_owner_is_active(&creator_scope) {
192 return;
193 }
194 with_callback_scope(&creator_scope, || {
195 if let Some(callback) = rc.borrow_mut().as_mut() {
196 callback(arg);
197 }
198 });
199 }
200 }
201}
202
203impl<A: 'static> Default for CallbackHolder1<A> {
204 fn default() -> Self {
205 Self {
206 rc: Rc::new(RefCell::new(None)),
207 creator_scope: Rc::new(RefCell::new(None)),
208 }
209 }
210}
211
212pub struct ReturnSlot<T> {
213 value: Option<T>,
214}
215
216impl<T: Clone> ReturnSlot<T> {
217 pub fn store(&mut self, value: T) {
218 self.value = Some(value);
219 }
220
221 pub fn get(&self) -> Option<T> {
222 self.value.clone()
223 }
224}
225
226impl<T> Default for ParamState<T> {
227 fn default() -> Self {
228 Self { value: None }
229 }
230}
231
232impl<T> Default for ReturnSlot<T> {
233 fn default() -> Self {
234 Self { value: None }
235 }
236}
237
238#[cfg(test)]
239mod callback_holder_tests {
240 use super::{CallbackHolder, CallbackHolder1, ParamSlot};
241 use crate::runtime::TestRuntime;
242 use crate::RecomposeScope;
243 use std::cell::Cell;
244 use std::rc::Rc;
245
246 #[test]
247 fn param_slot_take_reports_absence_instead_of_panicking() {
248 let slot = ParamSlot::default();
249
250 assert_eq!(slot.take(), None);
251 slot.set(11);
252 assert_eq!(slot.take(), Some(11));
253 assert_eq!(slot.take(), None);
254 }
255
256 #[test]
257 fn callback_holder_default_forwarder_is_noop() {
258 let forwarder = CallbackHolder::new().clone_rc();
259 forwarder();
260 }
261
262 #[test]
263 fn callback_holder_forwarder_uses_latest_callback() {
264 let holder = CallbackHolder::new();
265 let total = Rc::new(Cell::new(0));
266 let forwarder = holder.clone_rc();
267
268 let first_total = Rc::clone(&total);
269 holder.update(move || first_total.set(first_total.get() + 1));
270 forwarder();
271
272 let second_total = Rc::clone(&total);
273 holder.update(move || second_total.set(second_total.get() + 10));
274 forwarder();
275
276 assert_eq!(total.get(), 11);
277 }
278
279 #[test]
280 fn callback_holder_does_not_invoke_after_creator_scope_deactivation() {
281 let runtime = TestRuntime::new();
282 let scope = RecomposeScope::new_for_test(runtime.handle());
283 let holder = CallbackHolder::new();
284 let invocations = Rc::new(Cell::new(0));
285 let invocations_for_callback = Rc::clone(&invocations);
286 holder.update(move || invocations_for_callback.set(invocations_for_callback.get() + 1));
287 holder.creator_scope.replace(Some(scope.clone()));
288 let forwarder = holder.clone_rc();
289
290 forwarder();
291 assert_eq!(invocations.get(), 1);
292
293 scope.deactivate();
294 forwarder();
295 assert_eq!(
296 invocations.get(),
297 1,
298 "callbacks cannot outlive the active composition scope that owns them",
299 );
300 }
301
302 #[test]
303 fn callback_holder_does_not_invoke_under_inactive_creator_ancestor() {
304 let runtime = TestRuntime::new();
305 let ancestor = RecomposeScope::new_for_test(runtime.handle());
306 let creator = RecomposeScope::new_for_test(runtime.handle());
307 creator.set_parent_scope(Some(ancestor.clone()));
308 let holder = CallbackHolder::new();
309 let invocations = Rc::new(Cell::new(0));
310 let invocations_for_callback = Rc::clone(&invocations);
311 holder.update(move || invocations_for_callback.set(invocations_for_callback.get() + 1));
312 holder.creator_scope.replace(Some(creator));
313 let forwarder = holder.clone_rc();
314
315 forwarder();
316 assert_eq!(invocations.get(), 1);
317
318 ancestor.deactivate();
319 forwarder();
320 assert_eq!(
321 invocations.get(),
322 1,
323 "callbacks cannot run beneath an inactive composition ancestor",
324 );
325 }
326
327 #[test]
328 fn callback_holder1_default_forwarder_is_noop() {
329 let forwarder = CallbackHolder1::<i32>::new().clone_rc();
330 forwarder(7);
331 }
332
333 #[test]
334 fn callback_holder1_forwarder_uses_latest_callback() {
335 let holder = CallbackHolder1::<i32>::new();
336 let total = Rc::new(Cell::new(0));
337 let forwarder = holder.clone_rc();
338
339 let first_total = Rc::clone(&total);
340 holder.update(move |value| first_total.set(first_total.get() + value));
341 forwarder(2);
342
343 let second_total = Rc::clone(&total);
344 holder.update(move |value| second_total.set(second_total.get() + value * 5));
345 forwarder(3);
346
347 assert_eq!(total.get(), 17);
348 }
349
350 #[test]
351 fn callback_holder1_does_not_invoke_after_creator_scope_deactivation() {
352 let runtime = TestRuntime::new();
353 let scope = RecomposeScope::new_for_test(runtime.handle());
354 let holder = CallbackHolder1::<i32>::new();
355 let total = Rc::new(Cell::new(0));
356 let total_for_callback = Rc::clone(&total);
357 holder.update(move |value| total_for_callback.set(total_for_callback.get() + value));
358 holder.creator_scope.replace(Some(scope.clone()));
359 let forwarder = holder.clone_rc();
360
361 forwarder(3);
362 assert_eq!(total.get(), 3);
363
364 scope.deactivate();
365 forwarder(5);
366 assert_eq!(
367 total.get(),
368 3,
369 "argument callbacks cannot outlive their active composition owner",
370 );
371 }
372}