floem_reactive/
effect.rs

1use std::{any::Any, cell::RefCell, collections::HashSet, marker::PhantomData, mem, rc::Rc};
2
3use crate::{
4    id::Id,
5    runtime::{Runtime, RUNTIME},
6    scope::Scope,
7};
8
9#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Default)]
10pub(crate) enum EffectPriority {
11    #[default]
12    Normal,
13    High,
14}
15
16pub(crate) trait EffectTrait: Any {
17    fn id(&self) -> Id;
18    fn run(&self) -> bool;
19    fn add_observer(&self, id: Id);
20    fn clear_observers(&self) -> HashSet<Id>;
21    fn priority(&self) -> EffectPriority {
22        EffectPriority::Normal
23    }
24    fn as_any(&self) -> &dyn Any;
25}
26
27/// Handle for a running effect. Prefer `Effect::new` over `create_effect`.
28pub struct Effect<T, F>
29where
30    T: 'static,
31    F: Fn(Option<T>) -> T,
32{
33    id: Id,
34    f: F,
35    value: RefCell<Option<T>>,
36    observers: RefCell<HashSet<Id>>,
37    ts: PhantomData<()>,
38}
39
40impl<T, F> Drop for Effect<T, F>
41where
42    T: 'static,
43    F: Fn(Option<T>) -> T,
44{
45    fn drop(&mut self) {
46        if RUNTIME
47            .try_with(|runtime| runtime.remove_effect(self.id))
48            .is_ok()
49        {
50            self.id.dispose();
51        }
52    }
53}
54
55/// Create an Effect that runs the given function whenever the subscribed Signals in that
56/// function are updated.
57///
58/// The given function will be run immediately once and will track all signals that are
59/// subscribed in that run. On each subsequent run the list is cleared and then
60/// reconstructed based on the Signals that are subscribed during that run.
61#[deprecated(
62    since = "0.2.0",
63    note = "Use Effect::new instead; this will be removed in a future release"
64)]
65#[cfg_attr(debug_assertions, track_caller)]
66pub fn create_effect<T>(f: impl Fn(Option<T>) -> T + 'static)
67where
68    T: Any + 'static,
69{
70    Effect::new(f);
71}
72
73impl<T, F> Effect<T, F>
74where
75    T: Any + 'static,
76    F: Fn(Option<T>) -> T + 'static,
77{
78    #[allow(clippy::new_ret_no_self)]
79    #[cfg_attr(debug_assertions, track_caller)]
80    pub fn new(f: F) {
81        Runtime::assert_ui_thread();
82        let id = Id::next();
83        let effect: Rc<dyn EffectTrait> = Rc::new(Self {
84            id,
85            f,
86            value: RefCell::new(None),
87            observers: RefCell::new(HashSet::default()),
88            ts: PhantomData,
89        });
90        id.set_scope();
91        RUNTIME.with(|runtime| runtime.register_effect(&effect));
92
93        run_initial_effect(effect);
94    }
95}
96
97/// Internal updater effect handle. Prefer the associated constructors over the free functions.
98pub struct UpdaterEffect<T, I, C, U> {
99    id: Id,
100    compute: C,
101    on_change: U,
102    value: RefCell<Option<T>>,
103    observers: RefCell<HashSet<Id>>,
104    _phantom: PhantomData<I>,
105}
106
107impl<T, I, C, U> Drop for UpdaterEffect<T, I, C, U> {
108    fn drop(&mut self) {
109        if RUNTIME
110            .try_with(|runtime| runtime.remove_effect(self.id))
111            .is_ok()
112        {
113            self.id.dispose();
114        }
115    }
116}
117
118/// Create an effect updater that runs `on_change` when any signals that subscribe during the
119/// run of `compute` are updated. `compute` is immediately run only once, and its value is returned
120/// from the call to `create_updater`.
121impl UpdaterEffect<(), (), (), ()> {
122    /// Create an effect updater that runs `on_change` when any signals that subscribe during the
123    /// run of `compute` are updated. `compute` is immediately run only once, and its value is returned
124    /// from the call to `create_updater`.
125    #[allow(clippy::new_ret_no_self)]
126    #[cfg_attr(debug_assertions, track_caller)]
127    pub fn new<R>(compute: impl Fn() -> R + 'static, on_change: impl Fn(R) + 'static) -> R
128    where
129        R: 'static,
130    {
131        UpdaterEffect::new_stateful(move |_| (compute(), ()), move |r, _| on_change(r))
132    }
133
134    /// Create an effect updater that runs `on_change` when any signals within `compute` subscribe to
135    /// changes. `compute` is immediately run and its return value is returned.
136    #[allow(clippy::new_ret_no_self)]
137    #[cfg_attr(debug_assertions, track_caller)]
138    pub fn new_stateful<T, R>(
139        compute: impl Fn(Option<T>) -> (R, T) + 'static,
140        on_change: impl Fn(R, T) -> T + 'static,
141    ) -> R
142    where
143        T: Any + 'static,
144        R: 'static,
145    {
146        Runtime::assert_ui_thread();
147        let id = Id::next();
148        let effect = Rc::new(UpdaterEffect {
149            id,
150            compute,
151            on_change,
152            value: RefCell::new(None),
153            observers: RefCell::new(HashSet::default()),
154            _phantom: PhantomData,
155        });
156        id.set_scope();
157        let effect_dyn: Rc<dyn EffectTrait> = effect.clone();
158        RUNTIME.with(|runtime| runtime.register_effect(&effect_dyn));
159
160        run_initial_updater_effect(effect)
161    }
162}
163
164#[deprecated(
165    since = "0.2.0",
166    note = "Use UpdaterEffect::new instead; this will be removed in a future release"
167)]
168#[cfg_attr(debug_assertions, track_caller)]
169pub fn create_updater<R>(compute: impl Fn() -> R + 'static, on_change: impl Fn(R) + 'static) -> R
170where
171    R: 'static,
172{
173    UpdaterEffect::new(compute, on_change)
174}
175
176/// Create an effect updater that runs `on_change` when any signals within `compute` subscribe to
177/// changes. `compute` is immediately run and its return value is returned from `create_updater`.
178#[deprecated(
179    since = "0.2.0",
180    note = "Use UpdaterEffect::new_stateful instead; this will be removed in a future release"
181)]
182#[cfg_attr(debug_assertions, track_caller)]
183pub fn create_stateful_updater<T, R>(
184    compute: impl Fn(Option<T>) -> (R, T) + 'static,
185    on_change: impl Fn(R, T) -> T + 'static,
186) -> R
187where
188    T: Any + 'static,
189    R: 'static,
190{
191    UpdaterEffect::new_stateful(compute, on_change)
192}
193
194/// Signals that are wrapped with `untrack` will not subscribe to any effect.
195impl Effect<(), fn(Option<()>) -> ()> {
196    #[allow(clippy::new_ret_no_self)]
197    #[cfg_attr(debug_assertions, track_caller)]
198    pub fn untrack<T>(f: impl FnOnce() -> T) -> T {
199        Runtime::assert_ui_thread();
200        let prev_effect = RUNTIME.with(|runtime| runtime.current_effect.borrow_mut().take());
201        let result = f();
202        RUNTIME.with(|runtime| {
203            *runtime.current_effect.borrow_mut() = prev_effect;
204        });
205        result
206    }
207
208    #[allow(clippy::new_ret_no_self)]
209    #[cfg_attr(debug_assertions, track_caller)]
210    pub fn batch<T>(f: impl FnOnce() -> T) -> T {
211        let already_batching = RUNTIME.with(|runtime| {
212            let batching = runtime.batching.get();
213            if !batching {
214                runtime.batching.set(true);
215            }
216
217            batching
218        });
219
220        let result = f();
221        if !already_batching {
222            RUNTIME.with(|runtime| {
223                runtime.batching.set(false);
224                runtime.run_pending_effects();
225            });
226        }
227
228        result
229    }
230}
231
232#[deprecated(
233    since = "0.2.0",
234    note = "Use Effect::untrack instead; this will be removed in a future release"
235)]
236pub fn untrack<T>(f: impl FnOnce() -> T) -> T {
237    Effect::untrack(f)
238}
239
240#[deprecated(
241    since = "0.2.0",
242    note = "Use Effect::batch instead; this will be removed in a future release"
243)]
244pub fn batch<T>(f: impl FnOnce() -> T) -> T {
245    Effect::batch(f)
246}
247
248pub(crate) fn run_initial_effect(effect: Rc<dyn EffectTrait>) {
249    Runtime::assert_ui_thread();
250    let effect_id = effect.id();
251
252    RUNTIME.with(|runtime| {
253        *runtime.current_effect.borrow_mut() = Some(effect.clone());
254
255        let effect_scope = Scope(effect_id, PhantomData);
256        effect_scope.enter(|| {
257            effect.run();
258        });
259
260        *runtime.current_effect.borrow_mut() = None;
261    });
262}
263
264pub(crate) fn run_effect(effect: Rc<dyn EffectTrait>) {
265    Runtime::assert_ui_thread();
266    let effect_id = effect.id();
267    effect_id.dispose_children();
268
269    observer_clean_up(&effect);
270
271    RUNTIME.with(|runtime| {
272        *runtime.current_effect.borrow_mut() = Some(effect.clone());
273
274        let effect_scope = Scope(effect_id, PhantomData);
275        effect_scope.enter(move || {
276            effect.run();
277        });
278
279        *runtime.current_effect.borrow_mut() = None;
280    });
281}
282
283fn run_initial_updater_effect<T, I, C, U>(effect: Rc<UpdaterEffect<T, I, C, U>>) -> I
284where
285    T: 'static,
286    I: 'static,
287    C: Fn(Option<T>) -> (I, T) + 'static,
288    U: Fn(I, T) -> T + 'static,
289{
290    Runtime::assert_ui_thread();
291    let effect_id = effect.id();
292
293    let result = RUNTIME.with(|runtime| {
294        *runtime.current_effect.borrow_mut() = Some(effect.clone());
295
296        let effect_scope = Scope(effect_id, PhantomData);
297        let (result, new_value) = effect_scope.enter(|| (effect.compute)(None));
298
299        // set new value
300        *effect.value.borrow_mut() = Some(new_value);
301
302        *runtime.current_effect.borrow_mut() = None;
303
304        result
305    });
306
307    result
308}
309
310/// Do a observer clean up at the beginning of each effect run. It clears the effect
311/// from all the Signals that this effect subscribes to, and clears all the signals
312/// that's stored in this effect, so that the next effect run can re-track signals.
313pub(crate) fn observer_clean_up(effect: &Rc<dyn EffectTrait>) {
314    let effect_id = effect.id();
315    let observers = effect.clear_observers();
316    for observer in observers {
317        if let Some(signal) = observer.signal() {
318            signal.subscribers.lock().remove(&effect_id);
319        }
320    }
321}
322
323impl<T, F> EffectTrait for Effect<T, F>
324where
325    T: 'static,
326    F: Fn(Option<T>) -> T + 'static,
327{
328    fn id(&self) -> Id {
329        self.id
330    }
331
332    fn run(&self) -> bool {
333        let curr_value = self.value.borrow_mut().take();
334
335        // run the effect
336        let new_value = (self.f)(curr_value);
337
338        *self.value.borrow_mut() = Some(new_value);
339
340        true
341    }
342
343    fn add_observer(&self, id: Id) {
344        self.observers.borrow_mut().insert(id);
345    }
346
347    fn clear_observers(&self) -> HashSet<Id> {
348        mem::take(&mut *self.observers.borrow_mut())
349    }
350
351    fn as_any(&self) -> &dyn Any {
352        self
353    }
354}
355
356impl<T, I, C, U> EffectTrait for UpdaterEffect<T, I, C, U>
357where
358    T: 'static,
359    I: 'static,
360    C: Fn(Option<T>) -> (I, T) + 'static,
361    U: Fn(I, T) -> T + 'static,
362{
363    fn id(&self) -> Id {
364        self.id
365    }
366
367    fn run(&self) -> bool {
368        let curr_value = self.value.borrow_mut().take();
369
370        // run the effect
371        let (i, t) = (self.compute)(curr_value);
372        let new_value = (self.on_change)(i, t);
373
374        *self.value.borrow_mut() = Some(new_value);
375        true
376    }
377
378    fn add_observer(&self, id: Id) {
379        self.observers.borrow_mut().insert(id);
380    }
381
382    fn clear_observers(&self) -> HashSet<Id> {
383        mem::take(&mut *self.observers.borrow_mut())
384    }
385
386    fn as_any(&self) -> &dyn Any {
387        self
388    }
389}
390
391pub struct SignalTracker {
392    id: Id,
393    on_change: Rc<dyn Fn()>,
394}
395
396impl Drop for SignalTracker {
397    fn drop(&mut self) {
398        self.id.dispose();
399    }
400}
401
402#[deprecated(
403    since = "0.2.0",
404    note = "Use SignalTracker::new instead; this will be removed in a future release"
405)]
406pub fn create_tracker(on_change: impl Fn() + 'static) -> SignalTracker {
407    SignalTracker::new(on_change)
408}
409
410impl SignalTracker {
411    /// Creates a [SignalTracker] that subscribes to any changes in signals used within `on_change`.
412    pub fn new(on_change: impl Fn() + 'static) -> Self {
413        let id = Id::next();
414
415        SignalTracker {
416            id,
417            on_change: Rc::new(on_change),
418        }
419    }
420
421    /// Updates the tracking function used for [SignalTracker].
422    pub fn track<T: 'static>(&self, f: impl FnOnce() -> T) -> T {
423        Runtime::assert_ui_thread();
424        // Clear any previous tracking by disposing the old effect
425        self.id.dispose();
426
427        let prev_effect = RUNTIME.with(|runtime| runtime.current_effect.borrow_mut().take());
428
429        let tracking_effect: Rc<dyn EffectTrait> = Rc::new(TrackingEffect {
430            id: self.id,
431            observers: RefCell::new(HashSet::default()),
432            on_change: self.on_change.clone(),
433        });
434
435        RUNTIME.with(|runtime| {
436            runtime.register_effect(&tracking_effect);
437            *runtime.current_effect.borrow_mut() = Some(tracking_effect.clone());
438        });
439
440        let effect_scope = Scope(self.id, PhantomData);
441        let result = effect_scope.enter(|| {
442            effect_scope.track();
443            f()
444        });
445
446        RUNTIME.with(|runtime| {
447            *runtime.current_effect.borrow_mut() = prev_effect;
448        });
449
450        result
451    }
452}
453
454struct TrackingEffect {
455    id: Id,
456    observers: RefCell<HashSet<Id>>,
457    on_change: Rc<dyn Fn()>,
458}
459
460impl EffectTrait for TrackingEffect {
461    fn id(&self) -> Id {
462        self.id
463    }
464
465    fn run(&self) -> bool {
466        (self.on_change)();
467        true
468    }
469
470    fn add_observer(&self, id: Id) {
471        self.observers.borrow_mut().insert(id);
472    }
473
474    fn clear_observers(&self) -> HashSet<Id> {
475        mem::take(&mut *self.observers.borrow_mut())
476    }
477
478    fn as_any(&self) -> &dyn Any {
479        self
480    }
481}