cranpose_core/
callbacks.rs1use 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
9impl<T> ParamState<T> {
10 pub fn update(&mut self, new_value: &T) -> bool
11 where
12 T: PartialEq + Clone,
13 {
14 match self.value.as_mut() {
15 Some(old) if old == new_value => false,
16 Some(old) => {
17 old.clone_from(new_value);
18 true
19 }
20 None => {
21 self.value = Some(new_value.clone());
22 true
23 }
24 }
25 }
26
27 pub fn value(&self) -> Option<T>
28 where
29 T: Clone,
30 {
31 self.value.clone()
32 }
33}
34
35pub struct ParamSlot<T> {
38 val: RefCell<Option<T>>,
39}
40
41impl<T> Default for ParamSlot<T> {
42 fn default() -> Self {
43 Self {
44 val: RefCell::new(None),
45 }
46 }
47}
48
49impl<T> ParamSlot<T> {
50 pub fn set(&self, v: T) {
51 *self.val.borrow_mut() = Some(v);
52 }
53
54 pub fn take(&self) -> Option<T> {
56 self.val.borrow_mut().take()
57 }
58}
59
60type CallbackCell = Rc<RefCell<Option<Box<dyn FnMut()>>>>;
61type CallbackScopeCell = Rc<RefCell<Option<RecomposeScope>>>;
62
63struct CallbackScopeGuard {
64 core: Rc<ComposerCore>,
65}
66
67impl CallbackScopeGuard {
68 fn push(composer: &Composer, scope: RecomposeScope) -> Self {
69 composer.core.scope_stack.borrow_mut().push(scope);
70 Self {
71 core: composer.clone_core(),
72 }
73 }
74}
75
76impl Drop for CallbackScopeGuard {
77 fn drop(&mut self) {
78 self.core.scope_stack.borrow_mut().pop();
79 }
80}
81
82fn with_callback_scope<R>(scope: &CallbackScopeCell, f: impl FnOnce() -> R) -> R {
83 let captured_scope = scope.borrow().clone();
84 if let Some(saved_scope) = captured_scope
85 && let Some(composer) = composer_context::current_composer()
86 {
87 let _scope_guard = CallbackScopeGuard::push(&composer, saved_scope);
88 return f();
89 }
90
91 f()
92}
93
94fn callback_owner_is_active(scope: &CallbackScopeCell) -> bool {
95 scope
96 .borrow()
97 .as_ref()
98 .is_none_or(RecomposeScope::is_effectively_active)
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 pub fn new() -> Self {
114 Self::default()
115 }
116
117 pub fn update<F>(&self, f: F)
119 where
120 F: FnMut() + 'static,
121 {
122 self.update_boxed(Box::new(f));
123 }
124
125 pub fn update_boxed(&self, f: Box<dyn FnMut() + 'static>) {
129 *self.rc.borrow_mut() = Some(f);
130 *self.creator_scope.borrow_mut() =
131 composer_context::try_with_composer(callback_owner_scope).flatten();
132 }
133
134 pub fn clone_rc(&self) -> impl Fn() + 'static + use<> {
136 let rc = self.rc.clone();
137 let creator_scope = self.creator_scope.clone();
138 move || {
139 if !callback_owner_is_active(&creator_scope) {
140 return;
141 }
142 with_callback_scope(&creator_scope, || {
143 if let Some(callback) = rc.borrow_mut().as_mut() {
144 callback();
145 }
146 });
147 }
148 }
149}
150
151impl Default for CallbackHolder {
152 fn default() -> Self {
153 Self {
154 rc: Rc::new(RefCell::new(None)),
155 creator_scope: Rc::new(RefCell::new(None)),
156 }
157 }
158}
159
160#[derive(Clone)]
163pub struct CallbackHolder1<A: 'static> {
164 #[allow(clippy::type_complexity)]
165 rc: Rc<RefCell<Option<Box<dyn FnMut(A)>>>>,
166 creator_scope: CallbackScopeCell,
167}
168
169impl<A: 'static> CallbackHolder1<A> {
170 pub fn new() -> Self {
172 Self::default()
173 }
174
175 pub fn update<F>(&self, f: F)
177 where
178 F: FnMut(A) + 'static,
179 {
180 *self.rc.borrow_mut() = Some(Box::new(f));
181 *self.creator_scope.borrow_mut() =
182 composer_context::try_with_composer(callback_owner_scope).flatten();
183 }
184
185 pub fn clone_rc(&self) -> impl Fn(A) + 'static {
187 let rc = self.rc.clone();
188 let creator_scope = self.creator_scope.clone();
189 move |arg| {
190 if !callback_owner_is_active(&creator_scope) {
191 return;
192 }
193 with_callback_scope(&creator_scope, || {
194 if let Some(callback) = rc.borrow_mut().as_mut() {
195 callback(arg);
196 }
197 });
198 }
199 }
200}
201
202impl<A: 'static> Default for CallbackHolder1<A> {
203 fn default() -> Self {
204 Self {
205 rc: Rc::new(RefCell::new(None)),
206 creator_scope: Rc::new(RefCell::new(None)),
207 }
208 }
209}
210
211pub struct ReturnSlot<T> {
212 value: Option<T>,
213}
214
215impl<T: Clone> ReturnSlot<T> {
216 pub fn store(&mut self, value: T) {
217 self.value = Some(value);
218 }
219
220 pub fn get(&self) -> Option<T> {
221 self.value.clone()
222 }
223}
224
225impl<T> Default for ParamState<T> {
226 fn default() -> Self {
227 Self { value: None }
228 }
229}
230
231impl<T> Default for ReturnSlot<T> {
232 fn default() -> Self {
233 Self { value: None }
234 }
235}
236
237#[cfg(test)]
238mod callback_holder_tests {
239 use std::{cell::Cell, rc::Rc};
240
241 use super::{CallbackHolder, CallbackHolder1, ParamSlot};
242 use crate::{RecomposeScope, runtime::TestRuntime};
243
244 #[test]
245 fn param_slot_take_reports_absence_instead_of_panicking() {
246 let slot = ParamSlot::default();
247
248 assert_eq!(slot.take(), None);
249 slot.set(11);
250 assert_eq!(slot.take(), Some(11));
251 assert_eq!(slot.take(), None);
252 }
253
254 #[test]
255 fn callback_holder_default_forwarder_is_noop() {
256 let forwarder = CallbackHolder::new().clone_rc();
257 forwarder();
258 }
259
260 #[test]
261 fn callback_holder_forwarder_uses_latest_callback() {
262 let holder = CallbackHolder::new();
263 let total = Rc::new(Cell::new(0));
264 let forwarder = holder.clone_rc();
265
266 let first_total = Rc::clone(&total);
267 holder.update(move || first_total.set(first_total.get() + 1));
268 forwarder();
269
270 let second_total = Rc::clone(&total);
271 holder.update(move || second_total.set(second_total.get() + 10));
272 forwarder();
273
274 assert_eq!(total.get(), 11);
275 }
276
277 #[test]
278 fn callback_holder_does_not_invoke_after_creator_scope_deactivation() {
279 let runtime = TestRuntime::new();
280 let scope = RecomposeScope::new_for_test(runtime.handle());
281 let holder = CallbackHolder::new();
282 let invocations = Rc::new(Cell::new(0));
283 let invocations_for_callback = Rc::clone(&invocations);
284 holder.update(move || invocations_for_callback.set(invocations_for_callback.get() + 1));
285 holder.creator_scope.replace(Some(scope.clone()));
286 let forwarder = holder.clone_rc();
287
288 forwarder();
289 assert_eq!(invocations.get(), 1);
290
291 scope.deactivate();
292 forwarder();
293 assert_eq!(
294 invocations.get(),
295 1,
296 "callbacks cannot outlive the active composition scope that owns them",
297 );
298 }
299
300 #[test]
301 fn callback_holder_does_not_invoke_under_inactive_creator_ancestor() {
302 let runtime = TestRuntime::new();
303 let ancestor = RecomposeScope::new_for_test(runtime.handle());
304 let creator = RecomposeScope::new_for_test(runtime.handle());
305 creator.set_parent_scope(Some(ancestor.clone()));
306 let holder = CallbackHolder::new();
307 let invocations = Rc::new(Cell::new(0));
308 let invocations_for_callback = Rc::clone(&invocations);
309 holder.update(move || invocations_for_callback.set(invocations_for_callback.get() + 1));
310 holder.creator_scope.replace(Some(creator));
311 let forwarder = holder.clone_rc();
312
313 forwarder();
314 assert_eq!(invocations.get(), 1);
315
316 ancestor.deactivate();
317 forwarder();
318 assert_eq!(
319 invocations.get(),
320 1,
321 "callbacks cannot run beneath an inactive composition ancestor",
322 );
323 }
324
325 #[test]
326 fn callback_holder1_default_forwarder_is_noop() {
327 let forwarder = CallbackHolder1::<i32>::new().clone_rc();
328 forwarder(7);
329 }
330
331 #[test]
332 fn callback_holder1_forwarder_uses_latest_callback() {
333 let holder = CallbackHolder1::<i32>::new();
334 let total = Rc::new(Cell::new(0));
335 let forwarder = holder.clone_rc();
336
337 let first_total = Rc::clone(&total);
338 holder.update(move |value| first_total.set(first_total.get() + value));
339 forwarder(2);
340
341 let second_total = Rc::clone(&total);
342 holder.update(move |value| second_total.set(second_total.get() + value * 5));
343 forwarder(3);
344
345 assert_eq!(total.get(), 17);
346 }
347
348 #[test]
349 fn callback_holder1_does_not_invoke_after_creator_scope_deactivation() {
350 let runtime = TestRuntime::new();
351 let scope = RecomposeScope::new_for_test(runtime.handle());
352 let holder = CallbackHolder1::<i32>::new();
353 let total = Rc::new(Cell::new(0));
354 let total_for_callback = Rc::clone(&total);
355 holder.update(move |value| total_for_callback.set(total_for_callback.get() + value));
356 holder.creator_scope.replace(Some(scope.clone()));
357 let forwarder = holder.clone_rc();
358
359 forwarder(3);
360 assert_eq!(total.get(), 3);
361
362 scope.deactivate();
363 forwarder(5);
364 assert_eq!(
365 total.get(),
366 3,
367 "argument callbacks cannot outlive their active composition owner",
368 );
369 }
370}