Skip to main content

pocopine_core/
context.rs

1//! Parent-scope context — RFC-027, typed-key revision per RFC-030.
2//!
3//! A component can `provide(&KEY, value)` on its own scope; any
4//! descendant can `inject(&KEY)` and walk the scope-parent chain
5//! to find the first matching entry. The key is an `InjectKey<T>`
6//! (a Rust cousin of `Symbol("name")` + Vue 3's `InjectionKey<T>`):
7//! unique by construction, typed in the value it carries, with a
8//! debug name that shows up in devtools.
9//!
10//! Parent relationships are tracked here explicitly (not through
11//! the DOM), so teleported children and slot-materialised content
12//! still resolve to their *authoring* parent — regardless of where
13//! they physically render.
14
15use std::any::Any;
16use std::cell::RefCell;
17use std::collections::HashMap;
18use std::marker::PhantomData;
19use std::sync::atomic::{AtomicU64, Ordering};
20
21use crate::reactive::ScopeId;
22use crate::scope::current_scope_id;
23
24/// Process-local counter for minting fresh `InjectKey` ids. Starts
25/// at 1 so 0 is reserved for "unset"-style sentinels if we ever
26/// need one. Monotonic, never reused — matches Symbol identity
27/// semantics (two `Symbol("foo")` calls return distinct symbols).
28static NEXT_KEY_ID: AtomicU64 = AtomicU64::new(1);
29
30/// Opaque, typed, unique context key. Created once per logical
31/// slot (module-scope static via [`create_context!`] / the
32/// deprecated [`inject_key!`], or runtime via `ContextKey::new`).
33/// The `T` type parameter pins the value type so [`inject`] returns
34/// `Option<T>` with no turbofish at the callsite.
35///
36/// `PhantomData<fn() -> T>` (contravariant in `T`) keeps the type
37/// parameter in the signature without requiring `T: 'static` in
38/// unrelated positions; `T: 'static` is enforced on use via
39/// [`provide`] / [`inject`].
40pub struct ContextKey<T: 'static> {
41    id: u64,
42    name: &'static str,
43    _t: PhantomData<fn() -> T>,
44}
45
46/// Deprecated alias kept for migration. New code should use
47/// [`ContextKey`] (declared via [`create_context!`]).
48#[deprecated(note = "use create_context! / ContextKey instead (RFC 056 §6.3)")]
49pub type InjectKey<T> = ContextKey<T>;
50
51impl<T: 'static> ContextKey<T> {
52    /// Mint a fresh unique key. Two calls — even with the same
53    /// `name` — yield keys that never collide. `name` is a debug
54    /// label only.
55    pub fn new(name: &'static str) -> Self {
56        Self {
57            id: NEXT_KEY_ID.fetch_add(1, Ordering::Relaxed),
58            name,
59            _t: PhantomData,
60        }
61    }
62
63    /// Debug label, surfaces in devtools + error messages.
64    pub fn name(&self) -> &'static str {
65        self.name
66    }
67
68    /// Unique process-local id; stable for the key's lifetime.
69    /// Used as the HashMap key inside the provides table.
70    pub fn id(&self) -> u64 {
71        self.id
72    }
73
74    /// Method-style provide. Equivalent to [`provide(&self, value)`](provide)
75    /// but reads naturally on a typed key declared with
76    /// [`create_context!`] (RFC 056 §6.4):
77    ///
78    /// ```ignore
79    /// ROOT.provide(this::<Self>());
80    /// ```
81    pub fn provide(&self, value: T)
82    where
83        T: Any + 'static,
84    {
85        provide(self, value);
86    }
87
88    /// Method-style inject. Equivalent to [`inject(&self)`](inject).
89    pub fn inject(&self) -> Option<T>
90    where
91        T: Clone + Any + 'static,
92    {
93        inject(self)
94    }
95}
96
97// `Copy` is the canonical form for an opaque token; `Clone` follows
98// automatically via `{ *self }` (per clippy's non-canonical-clone
99// lint — don't spell out field-by-field when `Copy` is available).
100impl<T: 'static> Copy for ContextKey<T> {}
101impl<T: 'static> Clone for ContextKey<T> {
102    fn clone(&self) -> Self {
103        *self
104    }
105}
106
107impl<T: 'static> std::fmt::Debug for ContextKey<T> {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        f.debug_struct("ContextKey")
110            .field("name", &self.name)
111            .field("id", &self.id)
112            .finish()
113    }
114}
115
116/// Entries are keyed by `InjectKey::id()` — a `u64`. Debug names
117/// aren't part of the key; they only drive diagnostics.
118type ProvideMap = HashMap<u64, Box<dyn Any>>;
119
120thread_local! {
121    /// Child → parent map, populated by the mount when a new
122    /// scope is minted. Cleared on `Scope::remove`.
123    static PARENTS: RefCell<HashMap<ScopeId, ScopeId>> =
124        RefCell::new(HashMap::new());
125
126    /// Scope → (key.id → boxed value). Populated by `provide`,
127    /// queried by `inject` along the parent chain.
128    static PROVIDES: RefCell<HashMap<ScopeId, ProvideMap>> =
129        RefCell::new(HashMap::new());
130}
131
132/// Record that `parent` is the scope that enclosed `child` at
133/// mount time. Called by the mount right after minting the
134/// child's scope.
135pub fn set_parent(child: ScopeId, parent: ScopeId) {
136    PARENTS.with(|p| {
137        p.borrow_mut().insert(child, parent);
138    });
139}
140
141/// Return the parent scope id for `scope`, if one was recorded.
142pub fn parent_of(scope: ScopeId) -> Option<ScopeId> {
143    PARENTS.with(|p| p.borrow().get(&scope).copied())
144}
145
146/// Store `value` under `key` on the current scope.
147///
148/// Panics outside a handler / lifecycle context — a provide call
149/// that couldn't identify its scope is always a programming error
150/// and we'd rather surface it loudly than silently drop.
151pub fn provide<T: Any + 'static>(key: &ContextKey<T>, value: T) {
152    let scope =
153        current_scope_id().expect("pocopine::provide called outside a handler / lifecycle context");
154    PROVIDES.with(|p| {
155        p.borrow_mut()
156            .entry(scope)
157            .or_default()
158            .insert(key.id(), Box::new(value));
159    });
160}
161
162/// Walk up the scope chain starting at the current scope and
163/// return a clone of the first provided value whose key matches.
164/// Type is inferred from the key — no turbofish.
165///
166/// Returns `None` when no ancestor provided this key, or when the
167/// stored value's type doesn't match the key's `T` (which should
168/// be impossible through the public API — the key's type guards
169/// the provide side — but stays as a belt-and-braces guard against
170/// `Any::downcast_ref` inconsistencies across crate boundaries).
171///
172/// Panics outside a handler / lifecycle context.
173pub fn inject<T: Clone + Any + 'static>(key: &ContextKey<T>) -> Option<T> {
174    let mut scope =
175        current_scope_id().expect("pocopine::inject called outside a handler / lifecycle context");
176    loop {
177        let hit = PROVIDES.with(|p| {
178            let map = p.borrow();
179            map.get(&scope)
180                .and_then(|entries| entries.get(&key.id()))
181                .and_then(|any| any.downcast_ref::<T>())
182                .cloned()
183        });
184        if let Some(v) = hit {
185            return Some(v);
186        }
187        match parent_of(scope) {
188            Some(parent) => scope = parent,
189            None => return None,
190        }
191    }
192}
193
194/// Devtools-only accessor: every (key-id, provider-scope) pair
195/// resolvable from `scope`. Walks the same parent chain as
196/// [`inject`] but collects instead of returning on the first hit,
197/// so the panel can show the full chain. The key's debug `name`
198/// isn't recoverable from its id alone — pair it with a separate
199/// key-id → name registry if needed; for now the panel shows the
200/// numeric id + the provider scope id.
201///
202/// Note: this is a best-effort introspection. Keys minted at
203/// runtime via [`InjectKey::new`] have module-independent debug
204/// names that aren't registered anywhere; panels using this helper
205/// should treat names as optional.
206#[cfg(feature = "devtools")]
207pub fn inject_chain(scope: ScopeId) -> Vec<(u64, ScopeId)> {
208    let mut out: Vec<(u64, ScopeId)> = Vec::new();
209    let mut cur = scope;
210    loop {
211        PROVIDES.with(|p| {
212            if let Some(entries) = p.borrow().get(&cur) {
213                for key_id in entries.keys() {
214                    out.push((*key_id, cur));
215                }
216            }
217        });
218        match parent_of(cur) {
219            Some(parent) => cur = parent,
220            None => break,
221        }
222    }
223    out
224}
225
226/// Drop all provide entries + the parent pointer for `scope`.
227/// Called from `Scope::remove` alongside the other per-scope
228/// side-table cleaners.
229pub fn clear_scope(scope: ScopeId) {
230    PARENTS.with(|p| {
231        p.borrow_mut().remove(&scope);
232    });
233    PROVIDES.with(|p| {
234        p.borrow_mut().remove(&scope);
235    });
236}
237
238/// Marker trait paired with each [`ContextKey<T>`] declared via
239/// [`create_context!`]. Lets `Inject<KEY, T>` (RFC 056 §6.5) name a
240/// key at type level on stable Rust without const generics.
241///
242/// The marker type lives in the *type* namespace and shares its
243/// identifier with the value-namespace static, e.g. `ROOT` resolves
244/// to the marker type in `Inject<ROOT, …>` and to the `LazyLock`
245/// static everywhere else (`ROOT.provide(...)`, `ROOT.inject()`).
246pub trait ContextMarker: 'static {
247    type Value: Clone + Any + 'static;
248    fn key() -> &'static ContextKey<Self::Value>;
249}
250
251/// Define a module-scope [`ContextKey<T>`] plus the matching
252/// [`ContextMarker`] type. The key's debug name is derived from
253/// `module_path!()` plus the identifier so collisions across crates
254/// stay impossible even if two crates pick the same local identifier
255/// (RFC 056 §6.3 — the successor to `inject_key!`).
256///
257/// Expands to two items:
258/// * `static <name>: LazyLock<ContextKey<T>>` (value namespace) so
259///   `<name>.provide(...)` / `<name>.inject()` work.
260/// * `struct <name> {}` (type namespace) so `Inject<<name>, T>` can
261///   name the key at the type level via [`ContextMarker`].
262///
263/// ```ignore
264/// pocopine::create_context!(pub(crate) ROOT: Handle<PineDialogRoot>);
265/// // later:
266/// ROOT.provide(this::<PineDialogRoot>());
267/// let root = ROOT.inject();
268///
269/// fn on_click(&self, root: Inject<ROOT, Handle<PineDialogRoot>>) {
270///     root.update(|dialog| dialog.close());
271/// }
272/// ```
273#[macro_export]
274macro_rules! create_context {
275    ($vis:vis $name:ident : $ty:ty) => {
276        $vis static $name: ::std::sync::LazyLock<$crate::context::ContextKey<$ty>> =
277            ::std::sync::LazyLock::new(|| {
278                $crate::context::ContextKey::new(
279                    concat!(module_path!(), "::", stringify!($name))
280                )
281            });
282
283        #[allow(non_camel_case_types)]
284        $vis struct $name {}
285
286        impl $crate::context::ContextMarker for $name {
287            type Value = $ty;
288            fn key() -> &'static $crate::context::ContextKey<$ty> {
289                &*$name
290            }
291        }
292    };
293}
294
295/// Deprecated alias for [`create_context!`]. Kept so existing call
296/// sites keep building during the RFC 056 migration window. New code
297/// should reach for [`create_context!`].
298#[macro_export]
299macro_rules! inject_key {
300    ($vis:vis $name:ident : $ty:ty) => {
301        $crate::create_context!($vis $name : $ty);
302    };
303}