Skip to main content

cranpose_core/
disposable_effect.rs

1use crate::{Key, caller_location_key, effect_key, with_current_composer};
2
3#[derive(Default)]
4pub(crate) struct DisposableEffectState {
5    key: Option<effect_key::EffectKey>,
6    cleanup: Option<Box<dyn FnOnce()>>,
7}
8
9impl DisposableEffectState {
10    fn should_run<K: PartialEq + 'static>(&self, keys: &K) -> bool {
11        self.key.as_ref().is_none_or(|current| !current.holds(keys))
12    }
13
14    fn set_key(&mut self, key: effect_key::EffectKey) {
15        self.key = Some(key);
16    }
17
18    fn set_cleanup(&mut self, cleanup: Option<Box<dyn FnOnce()>>) {
19        self.cleanup = cleanup;
20    }
21
22    fn run_cleanup(&mut self) {
23        if let Some(cleanup) = self.cleanup.take() {
24            cleanup();
25        }
26    }
27}
28
29impl Drop for DisposableEffectState {
30    fn drop(&mut self) {
31        self.run_cleanup();
32    }
33}
34
35/// The value a [`DisposableEffect`] body receives. Its `on_dispose` method makes the result that
36/// the body returns.
37#[derive(Clone, Copy, Debug, Default)]
38pub struct DisposableEffectScope;
39
40/// What a [`DisposableEffect`] body returns: the cleanup to run when the effect is disposed. Make
41/// it with `DisposableEffectScope::on_dispose`.
42#[derive(Default)]
43pub struct DisposableEffectResult {
44    cleanup: Option<Box<dyn FnOnce()>>,
45}
46
47impl DisposableEffectScope {
48    // forwards on purpose: the scope's onDispose, as Compose names it
49    /// Returns a result that runs `cleanup` when the effect leaves the composition or its keys
50    /// change.
51    pub fn on_dispose(&self, cleanup: impl FnOnce() + 'static) -> DisposableEffectResult {
52        DisposableEffectResult::new(cleanup)
53    }
54}
55
56impl DisposableEffectResult {
57    /// Creates a result that runs `cleanup` when the effect is disposed.
58    pub fn new(cleanup: impl FnOnce() + 'static) -> Self {
59        Self {
60            cleanup: Some(Box::new(cleanup)),
61        }
62    }
63
64    fn into_cleanup(self) -> Option<Box<dyn FnOnce()>> {
65        self.cleanup
66    }
67}
68
69/// Queues `effect` to run each time this call site is composed, after the node changes of the pass
70/// are applied. Compose's `SideEffect`.
71#[expect(non_snake_case, reason = "names follow Compose's casing")]
72pub fn SideEffect(effect: impl FnOnce() + 'static) {
73    with_current_composer(|composer| composer.register_side_effect(effect));
74}
75
76/// Implements [`DisposableEffect`] for a call site that has already computed its group key. Call
77/// `DisposableEffect` instead.
78#[doc(hidden)]
79pub fn __disposable_effect_impl<K, F>(group_key: Key, keys: K, effect: F)
80where
81    K: PartialEq + 'static,
82    F: FnOnce(DisposableEffectScope) -> DisposableEffectResult + 'static,
83{
84    with_current_composer(|composer| {
85        let state = composer.remember_effect::<DisposableEffectState>(group_key);
86        if state.with(|state| state.should_run(&keys)) {
87            let key = effect_key::EffectKey::new(keys);
88            state.update(|state| {
89                state.run_cleanup();
90                state.set_key(key);
91            });
92            let mut effect_opt = Some(effect);
93            composer.register_side_effect(move || {
94                if let Some(effect) = effect_opt.take() {
95                    let result = effect(DisposableEffectScope);
96                    state.update(|state| state.set_cleanup(result.into_cleanup()));
97                }
98            });
99        }
100    });
101}
102
103/// Runs `effect` when this call site first enters composition, and again
104/// whenever `keys` no longer equals the value it ran with last time; the
105/// previous run's [`DisposableEffectScope::on_dispose`] cleanup, if any,
106/// runs first, and also when the call site leaves composition entirely.
107///
108/// `keys` may be a tuple to depend on more than one value, matching Jetpack
109/// Compose's `DisposableEffect(vararg keys)`.
110#[expect(non_snake_case, reason = "names follow Compose's casing")]
111#[track_caller]
112pub fn DisposableEffect<K, F>(keys: K, effect: F)
113where
114    K: PartialEq + 'static,
115    F: FnOnce(DisposableEffectScope) -> DisposableEffectResult + 'static,
116{
117    __disposable_effect_impl(caller_location_key(), keys, effect);
118}
119
120/// Clones values into new bindings and evaluates a body with those bindings in scope, which saves
121/// writing out the clones before a `move` closure. Each alias before the semicolon is either a
122/// name, which clones the variable of that name, or `name = expression`, which clones the value of
123/// the expression. The body follows the semicolon.
124#[macro_export]
125macro_rules! clone_captures {
126    ($($alias:ident $(= $value:expr)?),+ $(,)?; $body:expr) => {{
127        $(let $alias = $crate::clone_captures!(@clone $alias $(= $value)?);)+
128        $body
129    }};
130    (@clone $alias:ident = $value:expr) => {
131        ($value).clone()
132    };
133    (@clone $alias:ident) => {
134        $alias.clone()
135    };
136}