Skip to main content

tpt_appfront_core/
context.rs

1//! Tree-scoped shared state (a lightweight Context/DI primitive).
2//!
3//! Deeply-nested components often need shared state (theme, current user, a
4//! router) without threading a `Signal` through every constructor argument.
5//! This module provides a `Context<T>` plus a provider stack so a component
6//! can *provide* a value and any descendant can *consume* the nearest one:
7//!
8//! ```ignore
9//! let theme = Context::new(Signal::new(Theme::Dark));
10//! provide_context(&theme, || {
11//!     // inside here, `use_context::<Theme>()` returns `theme`
12//!     let t = use_context::<Theme>();
13//!     container(|c| c.text(format!("theme: {:?}", t.get())))
14//! });
15//! ```
16//!
17//! The stack is thread-local and keyed by the value's type `T`, so a provider
18//! shadows any outer provider of the same type for the duration of its scope
19//! closure. This is backend-agnostic: it neither reads nor writes the DOM,
20//! canvas, or any `UITree` field — it only coordinates during the synchronous
21//! build of the tree (builder closures run nested on the call stack, which is
22//! exactly the scope a provider should cover).
23
24use crate::signal::Signal;
25use std::any::{Any, TypeId};
26use std::cell::RefCell;
27use std::collections::HashMap;
28use std::rc::Rc;
29
30/// A piece of shared, reactive state that can be provided to a subtree and
31/// consumed by any descendant.
32///
33/// Internally wraps a [`Signal<T>`] so consumers see updates live, exactly like
34/// any other signal in the reactive core.
35pub struct Context<T> {
36    signal: Signal<T>,
37}
38
39impl<T> Clone for Context<T> {
40    fn clone(&self) -> Self {
41        Context {
42            signal: self.signal.clone(),
43        }
44    }
45}
46
47impl<T: Clone + 'static> Context<T> {
48    /// Creates a context from an initial value.
49    pub fn new(value: T) -> Self {
50        Context {
51            signal: Signal::new(value),
52        }
53    }
54
55    /// Creates a context directly around an existing [`Signal`].
56    pub fn from_signal(signal: Signal<T>) -> Self {
57        Context { signal }
58    }
59
60    /// Reads the current value.
61    pub fn get(&self) -> T
62    where
63        T: Clone,
64    {
65        self.signal.get()
66    }
67
68    /// Updates the value.
69    pub fn set(&self, value: T) {
70        self.signal.set(value);
71    }
72
73    /// Returns the underlying signal so consumers can subscribe to changes.
74    pub fn signal(&self) -> Signal<T> {
75        self.signal.clone()
76    }
77}
78
79thread_local! {
80    /// Per-type stack of providers. The top of each type's stack is the
81    /// nearest provider visible to code currently running.
82    static PROVIDERS: RefCell<HashMap<TypeId, Vec<Rc<dyn Any>>>> =
83        RefCell::new(HashMap::new());
84}
85
86/// Provides `ctx` to the subtree built inside `scope`, then restores the
87/// previous provider afterwards. Any `use_context::<T>()` call made while
88/// `scope` runs resolves to `ctx`.
89pub fn provide_context<T: 'static>(ctx: &Context<T>, scope: impl FnOnce()) {
90    let key = TypeId::of::<T>();
91    PROVIDERS.with(|p| {
92        p.borrow_mut()
93            .entry(key)
94            .or_insert_with(Vec::new)
95            .push(Rc::new(ctx.clone()) as Rc<dyn Any>);
96    });
97    scope();
98    PROVIDERS.with(|p| {
99        let mut map = p.borrow_mut();
100        if let Some(stack) = map.get_mut(&key) {
101            stack.pop();
102            if stack.is_empty() {
103                map.remove(&key);
104            }
105        }
106    });
107}
108
109/// Returns the nearest [`Context<T>`] provided by an enclosing
110/// [`provide_context`], or `None` if no provider of type `T` is in scope.
111pub fn use_context<T: 'static>() -> Option<Context<T>> {
112    let key = TypeId::of::<T>();
113    PROVIDERS.with(|p| {
114        let stack = p.borrow();
115        stack
116            .get(&key)
117            .and_then(|s| s.last())
118            .and_then(|rc| rc.downcast_ref::<Context<T>>())
119            .cloned()
120    })
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[derive(Debug, Clone, PartialEq)]
128    struct Theme {
129        dark: bool,
130    }
131
132    #[derive(Debug, Clone, PartialEq)]
133    struct User {
134        name: String,
135    }
136
137    #[test]
138    fn use_context_returns_none_when_unprovided() {
139        assert!(use_context::<Theme>().is_none());
140    }
141
142    #[test]
143    fn provide_then_use_resolves_nearest() {
144        let outer = Context::new(Theme { dark: false });
145        let mut observed = None;
146        provide_context(&outer, || {
147            observed = use_context::<Theme>().map(|c| c.get());
148        });
149        assert_eq!(observed, Some(Theme { dark: false }));
150    }
151
152    #[test]
153    fn inner_provider_shadows_outer() {
154        let outer = Context::new(Theme { dark: false });
155        let inner = Context::new(Theme { dark: true });
156        let mut outer_before = None;
157        let mut inner_seen = None;
158        let mut outer_after = None;
159        provide_context(&outer, || {
160            outer_before = use_context::<Theme>().map(|c| c.get());
161            provide_context(&inner, || {
162                inner_seen = use_context::<Theme>().map(|c| c.get());
163            });
164            // After the inner scope ends, the outer provider is visible again.
165            outer_after = use_context::<Theme>().map(|c| c.get());
166        });
167        assert_eq!(inner_seen, Some(Theme { dark: true }));
168        assert_eq!(outer_before, Some(Theme { dark: false }));
169        assert_eq!(outer_after, Some(Theme { dark: false }));
170    }
171
172    #[test]
173    fn different_types_are_independent() {
174        let theme = Context::new(Theme { dark: true });
175        let user = Context::new(User {
176            name: "ada".to_string(),
177        });
178        provide_context(&theme, || {
179            provide_context(&user, || {
180                assert!(use_context::<Theme>().is_some());
181                assert!(use_context::<User>().is_some());
182                assert!(use_context::<i32>().is_none());
183            });
184        });
185    }
186
187    #[test]
188    fn context_updates_are_visible_to_consumers() {
189        let theme = Context::new(Theme { dark: false });
190        let observed = Rc::new(RefCell::new(None));
191        let obs = observed.clone();
192        provide_context(&theme, || {
193            if let Some(c) = use_context::<Theme>() {
194                *obs.borrow_mut() = Some(c.get());
195                c.set(Theme { dark: true });
196                *obs.borrow_mut() = Some(c.get());
197            }
198        });
199        assert_eq!(*observed.borrow(), Some(Theme { dark: true }));
200    }
201}