Skip to main content

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}