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}