Skip to main content

repose_core/
scope.rs

1use std::any::Any;
2use std::cell::{Cell, RefCell};
3use std::rc::{Rc, Weak};
4
5use crate::effects::Dispose;
6
7thread_local! {
8    static CURRENT_SCOPE: RefCell<Option<Weak<ScopeInner>>> = const { RefCell::new(None) };
9}
10
11pub struct Scope {
12    inner: Rc<ScopeInner>,
13}
14
15struct ScopeInner {
16    disposers: RefCell<Vec<Box<dyn FnOnce()>>>,
17    children: RefCell<Vec<Scope>>,
18    memo_cache: RefCell<std::collections::HashMap<String, Box<dyn Any>>>,
19    disposed: Cell<bool>,
20}
21
22impl Default for Scope {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl Scope {
29    pub fn new() -> Self {
30        Self {
31            inner: Rc::new(ScopeInner {
32                disposers: RefCell::new(Vec::new()),
33                children: RefCell::new(Vec::new()),
34                memo_cache: RefCell::new(std::collections::HashMap::new()),
35                disposed: Cell::new(false),
36            }),
37        }
38    }
39
40    pub fn run<R>(&self, f: impl FnOnce() -> R) -> R {
41        CURRENT_SCOPE.with(|current| {
42            let prev = current.borrow().clone();
43            *current.borrow_mut() = Some(Rc::downgrade(&self.inner));
44            let result = f();
45            *current.borrow_mut() = prev;
46            result
47        })
48    }
49
50    pub fn add_disposer(&self, disposer: impl FnOnce() + 'static) {
51        self.inner.disposers.borrow_mut().push(Box::new(disposer));
52    }
53
54    /// Returns a cached value from this scope's memo cache, or creates it with
55    /// `init` and stores it. The value persists for the lifetime of this scope
56    /// (i.e., until the scope key is no longer composed or the root is replaced).
57    pub fn memo<T: 'static>(&self, key: &str, init: impl FnOnce() -> T) -> Rc<T> {
58        let mut cache = self.inner.memo_cache.borrow_mut();
59        if let Some(existing) = cache.get(key) {
60            if let Some(v) = existing.downcast_ref::<Rc<T>>() {
61                return v.clone();
62            }
63        }
64        let val: Rc<T> = Rc::new(init());
65        cache.insert(key.to_string(), Box::new(val.clone()));
66        val
67    }
68
69    pub fn child(&self) -> Scope {
70        let child = Scope::new();
71        self.inner.children.borrow_mut().push(child.clone());
72        child
73    }
74
75    pub fn dispose(self) {
76        if self.inner.disposed.replace(true) {
77            return; // already disposed (or being dropped)
78        }
79        // Dispose children first
80        let children = std::mem::take(&mut *self.inner.children.borrow_mut());
81        for child in children {
82            child.dispose();
83        }
84
85        // Run disposers
86        let disposers = std::mem::take(&mut *self.inner.disposers.borrow_mut());
87        for disposer in disposers {
88            disposer();
89        }
90    }
91}
92
93impl Clone for Scope {
94    fn clone(&self) -> Self {
95        Self {
96            inner: self.inner.clone(),
97        }
98    }
99}
100
101pub fn current_scope() -> Option<Scope> {
102    CURRENT_SCOPE.with(|current| {
103        current
104            .borrow()
105            .as_ref()
106            .and_then(|weak| weak.upgrade().map(|inner| Scope { inner }))
107    })
108}
109
110/// Access this scope's memo cache from anywhere inside a `scope!` body.
111/// Returns the cached value for `key`, or creates it with `init` and stores it.
112/// The value persists until the scope is disposed.
113///
114/// Unlike `remember_with_key`, this is scoped to the current composition scope
115/// and is automatically cleaned up when the scope is no longer composed.
116pub fn scope_memo<T: 'static>(key: &str, init: impl FnOnce() -> T) -> Rc<T> {
117    match current_scope() {
118        Some(scope) => scope.memo(key, init),
119        None => Rc::new(init()),
120    }
121}
122
123/// Scoped effect that auto-cleans up.
124///
125/// Runs `f()` immediately and registers the returned `Dispose` to run when the
126/// current scope is disposed.
127pub fn scoped_effect<F>(f: F)
128where
129    F: FnOnce() -> Dispose + 'static,
130{
131    if let Some(scope) = current_scope() {
132        let cleanup = f();
133        scope.add_disposer(move || cleanup.run());
134    } else {
135        // No scope, run setup now, but drop cleanup (legacy "leak" behavior).
136        let _cleanup = f();
137    }
138}
139
140impl Drop for ScopeInner {
141    fn drop(&mut self) {
142        if self.disposed.replace(true) {
143            return; // already disposed via explicit dispose() call
144        }
145        let children = std::mem::take(&mut *self.children.borrow_mut());
146        for child in children {
147            drop(child);
148        }
149
150        let disposers = std::mem::take(&mut *self.disposers.borrow_mut());
151        for disposer in disposers {
152            disposer();
153        }
154    }
155}