textual/node_id.rs
1//! Arena-based widget identity.
2//!
3//! `NodeId` is a generational key from `slotmap` used by the arena-based
4//! `WidgetTree`. It detects use-after-remove and round-trips through `u64`
5//! for hit-test metadata compatibility (`MetaValue::Int`).
6
7/// Canonical identity for every node in the widget tree.
8///
9/// Generational — detects use-after-remove. Used by the arena-based
10/// `WidgetTree` for all widget identity needs.
11///
12/// Round-trips to `u64` via [`node_id_to_ffi`] / [`node_id_from_ffi`] for
13/// hit-test metadata compatibility.
14pub type NodeId = slotmap::DefaultKey;
15
16/// Encode a `NodeId` as a `u64` for FFI / hit-test metadata.
17///
18/// The returned value is an opaque encoding of the key's version and index.
19/// Use [`node_id_from_ffi`] to recover the original `NodeId`.
20#[inline]
21pub fn node_id_to_ffi(id: NodeId) -> u64 {
22 use slotmap::Key;
23 id.data().as_ffi()
24}
25
26/// Decode a `u64` back into a `NodeId`.
27///
28/// # Safety contract (logical, not `unsafe`)
29///
30/// The caller must pass a value previously obtained from [`node_id_to_ffi`].
31/// Passing arbitrary integers produces a syntactically valid but semantically
32/// bogus key — any subsequent `SlotMap` lookup will simply return `None`.
33#[inline]
34pub fn node_id_from_ffi(ffi: u64) -> NodeId {
35 slotmap::KeyData::from_ffi(ffi).into()
36}
37
38#[cfg(test)]
39mod tests {
40 use super::*;
41 use slotmap::SlotMap;
42
43 #[test]
44 fn round_trip_through_ffi() {
45 let mut sm = SlotMap::new();
46 let id: NodeId = sm.insert("hello");
47
48 let encoded = node_id_to_ffi(id);
49 let decoded = node_id_from_ffi(encoded);
50 assert_eq!(id, decoded);
51 assert_eq!(sm[decoded], "hello");
52 }
53
54 #[test]
55 fn bogus_ffi_value_does_not_crash() {
56 let sm: SlotMap<NodeId, ()> = SlotMap::new();
57 let bogus = node_id_from_ffi(0xDEAD_BEEF);
58 // Lookup returns None, no panic.
59 assert!(sm.get(bogus).is_none());
60 }
61}