Skip to main content

lumen_core/
node.rs

1//! Node handles and the per-tick DOM index for the dynamic query API.
2//!
3//! The read side of the scripting DOM surface (`query`, `get_by_id`,
4//! traversal) addresses live elements through a [`NodeHandle`]: an
5//! `Entity` plus its generation, so a stale handle resolves to nothing
6//! instead of aliasing a recycled entity. [`DomIndex`] is an immutable
7//! per-tick snapshot of the selector-reachable tree that the runtime
8//! rebuilds each frame and publishes into a process-shared cache; script
9//! hosts and the C-ABI read that snapshot without touching the live world.
10//!
11//! Selector matching itself lives in `lumen-ir` (the cascade matcher) and
12//! is driven from `lumen-script`, which can depend on both this crate and
13//! `lumen-ir`. This module holds only the handle types, the pure snapshot
14//! data, and the traversal that needs no selector engine.
15
16use bevy_ecs::entity::{Entities, Entity};
17use bevy_ecs::resource::Resource;
18use std::collections::HashMap;
19use std::sync::atomic::{AtomicU64, Ordering};
20use std::sync::{Arc, Mutex, OnceLock, RwLock};
21
22/// Opaque handle to a live element: an [`Entity`] plus the generation it
23/// carried when the handle was minted. Resolving a handle validates the
24/// generation, so a handle to a despawned entity returns `None` rather
25/// than addressing whatever entity later reused that index.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct NodeHandle {
28    /// The referenced entity (index + generation).
29    pub entity: Entity,
30    /// The entity's generation bits at mint time, kept for the packed
31    /// wire form and for cross-checking after an unpack.
32    pub generation: u32,
33}
34
35impl NodeHandle {
36    /// Mint a handle for `entity`, capturing its current generation.
37    pub fn new(entity: Entity) -> Self {
38        Self {
39            entity,
40            generation: entity.generation().to_bits(),
41        }
42    }
43
44    /// Pack into the `u64` wire form used by the C-ABI `LumenNode` and by
45    /// hosts whose value type can hold 64 bits. The layout is
46    /// `Entity::to_bits` (index in the low half, generation in the high
47    /// half); treat it as opaque and round-trip only through [`Self::unpack`].
48    pub fn pack(self) -> u64 {
49        self.entity.to_bits()
50    }
51
52    /// Reconstruct from the packed wire form. Returns `None` for bits that
53    /// never came from [`Self::pack`] (invalid index), never panicking.
54    pub fn unpack(bits: u64) -> Option<Self> {
55        Entity::try_from_bits(bits).map(Self::new)
56    }
57
58    /// Resolve against the live entity allocator: `Some(entity)` when the
59    /// exact `(index, generation)` is still alive, `None` when it has been
60    /// despawned (stale handle). Never panics.
61    pub fn validate(&self, entities: &Entities) -> Option<Entity> {
62        if entities.contains(self.entity) {
63            Some(self.entity)
64        } else {
65            None
66        }
67    }
68}
69
70/// Side-table mapping small `i32` ids to node handles, for script hosts
71/// whose value type cannot hold a 64-bit handle (candela's `Value` is
72/// internally `i32`). `intern` is idempotent per entity, so a handle
73/// keeps the same id across a tick; the sentinel id `0` means "no node".
74///
75/// Registered as a [`Resource`] for embedders that want a world-scoped
76/// table; the runtime also drives a process-global instance (see
77/// [`intern_node`] / [`resolve_node`]) so a `Send + Sync` host with no
78/// `&World` at call time can still mint ids.
79#[derive(Resource, Default, Debug)]
80pub struct NodeHandles {
81    /// `i32` id to packed handle bits. The bits are a real
82    /// [`NodeHandle::pack`] value for a live element, or a reserved spawn
83    /// token (see [`reserve_node_token`]) for a not-yet-materialized node.
84    forward: HashMap<i32, u64>,
85    reverse: HashMap<u64, i32>,
86    next: i32,
87}
88
89impl NodeHandles {
90    /// Intern `(entity, generation)`, returning a stable `i32` id (>= 1).
91    /// Re-interning the same entity returns the same id.
92    pub fn intern(&mut self, entity: Entity, generation: u32) -> i32 {
93        let _ = generation;
94        self.intern_raw(entity.to_bits())
95    }
96
97    /// Intern any packed handle (a real element's [`NodeHandle::pack`] or a
98    /// reserved spawn token), returning a stable `i32` id. Idempotent per
99    /// packed value.
100    pub fn intern_raw(&mut self, packed: u64) -> i32 {
101        if let Some(&id) = self.reverse.get(&packed) {
102            return id;
103        }
104        self.next = self.next.wrapping_add(1);
105        if self.next <= 0 {
106            self.next = 1;
107        }
108        let id = self.next;
109        self.forward.insert(id, packed);
110        self.reverse.insert(packed, id);
111        id
112    }
113
114    /// Resolve an interned id back to a live-element handle. Returns `None`
115    /// for `0`, an unknown id, or a reserved spawn token (which has no
116    /// entity until command-drain materializes it).
117    pub fn resolve(&self, id: i32) -> Option<NodeHandle> {
118        let raw = self.resolve_raw(id)?;
119        if is_reserved_token(raw) {
120            return None;
121        }
122        NodeHandle::unpack(raw)
123    }
124
125    /// Resolve an interned id to its packed bits (real handle or reserved
126    /// spawn token). `0` / unknown yields `None`.
127    pub fn resolve_raw(&self, id: i32) -> Option<u64> {
128        if id == 0 {
129            return None;
130        }
131        self.forward.get(&id).copied()
132    }
133}
134
135// ---------------------------------------------------------------------------
136// Reserved spawn tokens
137// ---------------------------------------------------------------------------
138//
139// `spawn(tag)` must return a handle synchronously so a fluent chain
140// (`spawn("div").set_class("row").append_to(parent)`) addresses one node
141// across a whole tick, while the real ECS entity only materializes at the
142// next command-drain. Script hosts hold no `&World` at call time, so they
143// cannot reserve a live `Entity`. Instead a spawn mints a process-global
144// reserved token: a `u64` with the top bit set so it never aliases a real
145// `Entity::to_bits` value. Each structural command carries this token; the
146// runtime's command applier maps token -> freshly spawned entity in FIFO
147// order, so the queued mutations land on the right node.
148
149/// Top bit marking a `u64` as a reserved spawn token rather than a packed
150/// [`NodeHandle`]. A real `Entity::to_bits` sets this bit only after a
151/// single index is recycled ~2^31 times, which does not happen in a UI
152/// session; handles are opaque and round-trip only through the provided
153/// helpers, per the wire-format contract.
154pub const RESERVED_TOKEN_FLAG: u64 = 1 << 63;
155
156static SPAWN_TOKEN_COUNTER: AtomicU64 = AtomicU64::new(1);
157
158/// Mint a fresh reserved spawn token. Unique within the process for the
159/// life of the run; consumed by the runtime's command applier, which maps
160/// it onto the entity it spawns.
161pub fn reserve_node_token() -> u64 {
162    let n = SPAWN_TOKEN_COUNTER.fetch_add(1, Ordering::Relaxed) & !RESERVED_TOKEN_FLAG;
163    RESERVED_TOKEN_FLAG | n
164}
165
166/// Whether `handle` is a reserved spawn token (top bit set) rather than a
167/// packed live-element handle.
168pub fn is_reserved_token(handle: u64) -> bool {
169    handle & RESERVED_TOKEN_FLAG != 0
170}
171
172/// Intern any packed handle (real or reserved token) in the process-global
173/// side-table, returning its `i32` id. Used by the candela host, whose
174/// values cannot carry a 64-bit handle.
175pub fn intern_node_raw(packed: u64) -> i32 {
176    node_handles()
177        .lock()
178        .map(|mut h| h.intern_raw(packed))
179        .unwrap_or(0)
180}
181
182/// Resolve an `i32` id to its packed bits (real handle or reserved token)
183/// against the process-global side-table.
184pub fn resolve_node_raw(id: i32) -> Option<u64> {
185    node_handles().lock().ok().and_then(|h| h.resolve_raw(id))
186}
187
188static NODE_HANDLES: OnceLock<Mutex<NodeHandles>> = OnceLock::new();
189
190fn node_handles() -> &'static Mutex<NodeHandles> {
191    NODE_HANDLES.get_or_init(|| Mutex::new(NodeHandles::default()))
192}
193
194/// Intern a handle in the process-global side-table, returning its `i32`
195/// id. Used by the candela host, whose closures have no `&World`.
196pub fn intern_node(entity: Entity, generation: u32) -> i32 {
197    node_handles()
198        .lock()
199        .map(|mut h| h.intern(entity, generation))
200        .unwrap_or(0)
201}
202
203/// Resolve an `i32` id against the process-global side-table.
204pub fn resolve_node(id: i32) -> Option<NodeHandle> {
205    node_handles().lock().ok().and_then(|h| h.resolve(id))
206}
207
208/// One selector-reachable element in a [`DomIndex`] snapshot. Positional
209/// fields (`child_index`, `sibling_count`, `doc_order`) are computed by
210/// [`DomIndex::build`] from the hierarchy; callers constructing records
211/// leave them at zero.
212#[derive(Debug, Clone)]
213pub struct DomRecord {
214    /// The element entity.
215    pub entity: Entity,
216    /// Its generation bits, mirrored into minted handles.
217    pub generation: u32,
218    /// Markup tag (`button`, `label`, ...). Empty for a tagless container.
219    pub tag: String,
220    /// Stable `id="..."`, if any.
221    pub id: Option<String>,
222    /// Class list from `class="..."`.
223    pub classes: Vec<String>,
224    /// Parent element, if this record has one inside the snapshot.
225    pub parent: Option<Entity>,
226    /// Child elements in document order.
227    pub children: Vec<Entity>,
228    /// 1-based position among siblings (computed).
229    pub child_index: i32,
230    /// Total sibling count including self (computed).
231    pub sibling_count: i32,
232    /// Depth-first pre-order rank across the whole snapshot (computed).
233    pub doc_order: u32,
234}
235
236/// Immutable per-tick snapshot of the selector-reachable element tree.
237/// Traversal and `get_by_id` read this directly; selector queries run in
238/// `lumen-script` over the same records.
239#[derive(Debug, Default)]
240pub struct DomIndex {
241    records: Vec<DomRecord>,
242    by_entity: HashMap<u64, usize>,
243    by_id: HashMap<String, Entity>,
244    roots: Vec<Entity>,
245}
246
247impl DomIndex {
248    /// Build a snapshot from unordered records. Each record must carry its
249    /// `parent` and ordered `children`; this computes sibling positions,
250    /// depth-first document order, the entity and id lookup maps, and the
251    /// root list. The last `id` wins on a duplicate (matching cascade
252    /// "first match by document order" is applied by `get_by_id` reading
253    /// the first-in-doc-order entry).
254    pub fn build(mut records: Vec<DomRecord>) -> Self {
255        let mut by_entity: HashMap<u64, usize> = HashMap::with_capacity(records.len());
256        for (i, r) in records.iter().enumerate() {
257            by_entity.insert(r.entity.to_bits(), i);
258        }
259
260        // Sibling positions. A record whose parent is absent from the
261        // snapshot is treated as a root; roots are ordered by entity bits
262        // for determinism.
263        let mut roots: Vec<Entity> = Vec::new();
264        for r in &records {
265            let in_index = r
266                .parent
267                .is_some_and(|p| by_entity.contains_key(&p.to_bits()));
268            if !in_index {
269                roots.push(r.entity);
270            }
271        }
272        roots.sort_by_key(|e| e.to_bits());
273
274        // child_index / sibling_count from each parent's children list;
275        // roots take their position among the sorted root list. Indexed so
276        // the write to `records[i]` can also read sibling records.
277        #[allow(clippy::needless_range_loop)]
278        for i in 0..records.len() {
279            let (idx, count) = match records[i].parent {
280                Some(p) if by_entity.contains_key(&p.to_bits()) => {
281                    let pi = by_entity[&p.to_bits()];
282                    let siblings = &records[pi].children;
283                    let pos = siblings
284                        .iter()
285                        .position(|c| *c == records[i].entity)
286                        .map(|z| z as i32 + 1)
287                        .unwrap_or(1);
288                    (pos, siblings.len().max(1) as i32)
289                }
290                _ => {
291                    let pos = roots
292                        .iter()
293                        .position(|c| *c == records[i].entity)
294                        .map(|z| z as i32 + 1)
295                        .unwrap_or(1);
296                    (pos, roots.len().max(1) as i32)
297                }
298            };
299            records[i].child_index = idx;
300            records[i].sibling_count = count;
301        }
302
303        // Depth-first document order from the roots.
304        let mut order: u32 = 0;
305        let mut stack: Vec<Entity> = roots.iter().rev().copied().collect();
306        let mut doc: HashMap<u64, u32> = HashMap::with_capacity(records.len());
307        while let Some(e) = stack.pop() {
308            let bits = e.to_bits();
309            if doc.contains_key(&bits) {
310                continue;
311            }
312            doc.insert(bits, order);
313            order += 1;
314            if let Some(&ri) = by_entity.get(&bits) {
315                for child in records[ri].children.iter().rev() {
316                    stack.push(*child);
317                }
318            }
319        }
320        for r in &mut records {
321            r.doc_order = doc.get(&r.entity.to_bits()).copied().unwrap_or(u32::MAX);
322        }
323
324        // id -> entity, first in document order wins.
325        let mut ordered: Vec<usize> = (0..records.len()).collect();
326        ordered.sort_by_key(|&i| records[i].doc_order);
327        let mut by_id: HashMap<String, Entity> = HashMap::new();
328        for &i in &ordered {
329            if let Some(id) = &records[i].id {
330                by_id.entry(id.clone()).or_insert(records[i].entity);
331            }
332        }
333
334        Self {
335            records,
336            by_entity,
337            by_id,
338            roots,
339        }
340    }
341
342    /// All records, unspecified order. Query callers sort by
343    /// [`DomRecord::doc_order`].
344    pub fn records(&self) -> &[DomRecord] {
345        &self.records
346    }
347
348    /// Look up a record by entity.
349    pub fn record(&self, entity: Entity) -> Option<&DomRecord> {
350        self.by_entity
351            .get(&entity.to_bits())
352            .map(|&i| &self.records[i])
353    }
354
355    /// Fast id lookup (`get_by_id`), first match in document order.
356    pub fn get_by_id(&self, id: &str) -> Option<Entity> {
357        self.by_id.get(id).copied()
358    }
359
360    /// Root elements, ordered.
361    pub fn roots(&self) -> &[Entity] {
362        &self.roots
363    }
364
365    /// The document root (`document()`), the first root in order.
366    pub fn document(&self) -> Option<Entity> {
367        self.roots.first().copied()
368    }
369
370    /// Parent of `entity`, if any (`node.parent()`).
371    pub fn parent(&self, entity: Entity) -> Option<Entity> {
372        self.record(entity).and_then(|r| r.parent)
373    }
374
375    /// Ordered children of `entity` (`node.children()`).
376    pub fn children(&self, entity: Entity) -> Vec<Entity> {
377        self.record(entity)
378            .map(|r| r.children.clone())
379            .unwrap_or_default()
380    }
381
382    /// First child (`node.first_child()`).
383    pub fn first_child(&self, entity: Entity) -> Option<Entity> {
384        self.record(entity)
385            .and_then(|r| r.children.first().copied())
386    }
387
388    /// Last child (`node.last_child()`).
389    pub fn last_child(&self, entity: Entity) -> Option<Entity> {
390        self.record(entity).and_then(|r| r.children.last().copied())
391    }
392
393    /// The sibling list `entity` belongs to (its parent's children, or the
394    /// root list when it has no parent in the snapshot).
395    fn siblings_of(&self, entity: Entity) -> &[Entity] {
396        match self.parent(entity) {
397            Some(p) => self.record(p).map(|r| r.children.as_slice()).unwrap_or(&[]),
398            None => &self.roots,
399        }
400    }
401
402    /// Next sibling in document order (`node.next()`).
403    pub fn next_sibling(&self, entity: Entity) -> Option<Entity> {
404        let sibs = self.siblings_of(entity);
405        let pos = sibs.iter().position(|e| *e == entity)?;
406        sibs.get(pos + 1).copied()
407    }
408
409    /// Previous sibling in document order (`node.prev()`).
410    pub fn prev_sibling(&self, entity: Entity) -> Option<Entity> {
411        let sibs = self.siblings_of(entity);
412        let pos = sibs.iter().position(|e| *e == entity)?;
413        if pos == 0 {
414            None
415        } else {
416            sibs.get(pos - 1).copied()
417        }
418    }
419
420    /// The ancestor chain of `entity`, closest-parent-first (not including
421    /// `entity`). Used by `lumen-script` to build the root-first
422    /// `AncestorInfo` slice the selector matcher consumes.
423    pub fn ancestors(&self, entity: Entity) -> Vec<Entity> {
424        let mut out = Vec::new();
425        let mut cur = entity;
426        for _ in 0..256 {
427            match self.parent(cur) {
428                Some(p) => {
429                    out.push(p);
430                    cur = p;
431                }
432                None => break,
433            }
434        }
435        out
436    }
437}
438
439// ---------------------------------------------------------------------------
440// Process-shared snapshot cache
441// ---------------------------------------------------------------------------
442
443static DOM_INDEX_CACHE: OnceLock<RwLock<Arc<DomIndex>>> = OnceLock::new();
444
445fn dom_index_cache() -> &'static RwLock<Arc<DomIndex>> {
446    DOM_INDEX_CACHE.get_or_init(|| RwLock::new(Arc::new(DomIndex::default())))
447}
448
449/// Publish a freshly-built snapshot for cross-thread readers. The runtime
450/// calls this each tick from `build_dom_index`, before event dispatch, so
451/// a query issued inside a handler sees the current tree.
452pub fn publish_dom_index(index: DomIndex) {
453    if let Ok(mut guard) = dom_index_cache().write() {
454        *guard = Arc::new(index);
455    }
456}
457
458/// Read the current snapshot. Returns a cheap `Arc` clone so the caller
459/// drops the lock immediately. Script hosts and the C-ABI read here; they
460/// hold no `&World` at call time.
461pub fn dom_index_snapshot() -> Arc<DomIndex> {
462    dom_index_cache()
463        .read()
464        .map(|g| g.clone())
465        .unwrap_or_else(|_| Arc::new(DomIndex::default()))
466}
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471    use bevy_ecs::world::World;
472
473    fn rec(
474        entity: Entity,
475        tag: &str,
476        id: Option<&str>,
477        classes: &[&str],
478        parent: Option<Entity>,
479        children: &[Entity],
480    ) -> DomRecord {
481        DomRecord {
482            entity,
483            generation: entity.generation().to_bits(),
484            tag: tag.to_string(),
485            id: id.map(str::to_string),
486            classes: classes.iter().map(|s| s.to_string()).collect(),
487            parent,
488            children: children.to_vec(),
489            child_index: 0,
490            sibling_count: 0,
491            doc_order: 0,
492        }
493    }
494
495    #[test]
496    fn handle_packs_and_validates() {
497        let mut w = World::new();
498        let e = w.spawn_empty().id();
499        let h = NodeHandle::new(e);
500        let packed = h.pack();
501        let back = NodeHandle::unpack(packed).unwrap();
502        assert_eq!(back.entity, e);
503        assert_eq!(h.validate(w.entities()), Some(e));
504        w.despawn(e);
505        // Stale handle: same bits, but the entity is gone.
506        assert_eq!(h.validate(w.entities()), None);
507    }
508
509    #[test]
510    fn unpack_rejects_garbage_without_panicking() {
511        assert!(NodeHandle::unpack(0).is_none());
512    }
513
514    #[test]
515    fn side_table_is_idempotent() {
516        let mut w = World::new();
517        let e = w.spawn_empty().id();
518        let mut t = NodeHandles::default();
519        let a = t.intern(e, e.generation().to_bits());
520        let b = t.intern(e, e.generation().to_bits());
521        assert_eq!(a, b);
522        assert!(a >= 1);
523        assert_eq!(t.resolve(a).unwrap().entity, e);
524        assert!(t.resolve(0).is_none());
525    }
526
527    #[test]
528    fn index_computes_tree_shape() {
529        let mut w = World::new();
530        let root = w.spawn_empty().id();
531        let a = w.spawn_empty().id();
532        let b = w.spawn_empty().id();
533        let recs = vec![
534            rec(root, "root", Some("app"), &[], None, &[a, b]),
535            rec(a, "button", Some("save"), &["row"], Some(root), &[]),
536            rec(b, "button", Some("cancel"), &["row"], Some(root), &[]),
537        ];
538        let idx = DomIndex::build(recs);
539        assert_eq!(idx.document(), Some(root));
540        assert_eq!(idx.get_by_id("save"), Some(a));
541        assert_eq!(idx.parent(a), Some(root));
542        assert_eq!(idx.children(root), vec![a, b]);
543        assert_eq!(idx.first_child(root), Some(a));
544        assert_eq!(idx.last_child(root), Some(b));
545        assert_eq!(idx.next_sibling(a), Some(b));
546        assert_eq!(idx.prev_sibling(b), Some(a));
547        assert_eq!(idx.next_sibling(b), None);
548        assert_eq!(idx.ancestors(a), vec![root]);
549        // Positions.
550        assert_eq!(idx.record(a).unwrap().child_index, 1);
551        assert_eq!(idx.record(b).unwrap().child_index, 2);
552        assert_eq!(idx.record(b).unwrap().sibling_count, 2);
553        // Document order: root, a, b.
554        assert!(idx.record(root).unwrap().doc_order < idx.record(a).unwrap().doc_order);
555        assert!(idx.record(a).unwrap().doc_order < idx.record(b).unwrap().doc_order);
556    }
557
558    #[test]
559    fn reserved_tokens_are_distinct_and_flagged() {
560        let a = reserve_node_token();
561        let b = reserve_node_token();
562        assert_ne!(a, b, "each reserved token is unique");
563        assert!(is_reserved_token(a));
564        assert!(is_reserved_token(b));
565        // A real entity handle is never mistaken for a reserved token.
566        let mut w = World::new();
567        let e = w.spawn_empty().id();
568        assert!(!is_reserved_token(NodeHandle::new(e).pack()));
569    }
570
571    #[test]
572    fn raw_intern_round_trips_real_and_reserved() {
573        let mut w = World::new();
574        let e = w.spawn_empty().id();
575        let real = NodeHandle::new(e).pack();
576        let token = reserve_node_token();
577        let mut t = NodeHandles::default();
578        let id_real = t.intern_raw(real);
579        let id_tok = t.intern_raw(token);
580        assert_ne!(id_real, id_tok);
581        assert_eq!(t.resolve_raw(id_real), Some(real));
582        assert_eq!(t.resolve_raw(id_tok), Some(token));
583        // `resolve` (element-only) yields the handle for the real node and
584        // nothing for the reserved token.
585        assert_eq!(t.resolve(id_real).unwrap().entity, e);
586        assert!(t.resolve(id_tok).is_none());
587    }
588
589    #[test]
590    fn global_cache_round_trips() {
591        let mut w = World::new();
592        let root = w.spawn_empty().id();
593        publish_dom_index(DomIndex::build(vec![rec(
594            root,
595            "root",
596            Some("r"),
597            &[],
598            None,
599            &[],
600        )]));
601        assert_eq!(dom_index_snapshot().get_by_id("r"), Some(root));
602    }
603}