Skip to main content

fission_ir/
widget_id.rs

1//! Stable identity for widgets and lowered IR nodes.
2//!
3//! A [`WidgetId`] is the single identity type used across authoring widgets,
4//! lowered IR nodes, layout, rendering, hit testing, and runtime state. The old
5//! split between widget identity and node identity is intentionally gone: a
6//! widget may lower to one or more IR nodes, and those nodes use derived
7//! `WidgetId` values when they need child identities.
8
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12/// A stable 128-bit identity for widgets and lowered IR nodes.
13///
14/// Fission assigns every widget a deterministic identity from the application
15/// root and its structural child path. Most application code therefore does
16/// not need to set IDs. Unidentified collection items intentionally retain
17/// identity by position; give each logical item an explicit ID when state must
18/// follow the item through insertion, removal, filtering, or reordering.
19///
20/// `WidgetId` values are derived from BLAKE3 hashes. Two public construction
21/// strategies are available:
22///
23/// * [`WidgetId::explicit`] hashes a user-provided stable key.
24/// * [`WidgetId::derived`] hashes a parent identity plus a child-index path.
25///
26/// # Example
27///
28/// ```rust
29/// use fission_ir::WidgetId;
30///
31/// let sidebar = WidgetId::explicit("sidebar");
32/// let first_item = WidgetId::derived(sidebar.as_u128(), &[0]);
33/// assert_ne!(sidebar, first_item);
34/// ```
35#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
36pub struct WidgetId(u128);
37
38impl WidgetId {
39    /// Returns the deterministic identity seed used for an application's root
40    /// widget when the embedding shell does not provide a mount-specific id.
41    pub fn app_root() -> Self {
42        Self::explicit("fission.app.root")
43    }
44
45    /// Creates a `WidgetId` from a raw 128-bit value.
46    ///
47    /// This is intended for internal use or deserialization. In normal code use
48    /// [`WidgetId::explicit`] or [`WidgetId::derived`] instead.
49    pub const fn from_u128(val: u128) -> Self {
50        Self(val)
51    }
52
53    /// Returns the underlying 128-bit value.
54    pub fn as_u128(&self) -> u128 {
55        self.0
56    }
57
58    /// Creates a `WidgetId` from a user-provided stable key.
59    ///
60    /// The key is hashed with BLAKE3 using the same explicit-identity domain as
61    /// the original IR identity system. Keep the key stable across rebuilds when
62    /// you want runtime state, focus, scroll, animation, or host-surface state to
63    /// follow a widget through tree changes.
64    pub fn explicit(key: &str) -> Self {
65        let mut hasher = blake3::Hasher::new();
66        hasher.update(b"explicit:");
67        hasher.update(key.as_bytes());
68        let hash = hasher.finalize();
69        Self(u128::from_le_bytes(
70            hash.as_bytes()[0..16].try_into().unwrap(),
71        ))
72    }
73
74    /// Creates a `WidgetId` derived from a parent identity and child-index path.
75    ///
76    /// This provides structural identity for children that do not have explicit
77    /// keys. Dynamic/reorderable lists should provide explicit IDs for list items;
78    /// purely structural children can use derived IDs.
79    pub fn derived(parent: u128, path: &[u32]) -> Self {
80        let mut hasher = blake3::Hasher::new();
81        hasher.update(b"derived:");
82        hasher.update(&parent.to_le_bytes());
83        for index in path {
84            hasher.update(&index.to_le_bytes());
85        }
86        let hash = hasher.finalize();
87        Self(u128::from_le_bytes(
88            hash.as_bytes()[0..16].try_into().unwrap(),
89        ))
90    }
91
92    /// Creates an identity scoped to a parent and a stable string key.
93    ///
94    /// This is used for authoring locations and keyed collection items where a
95    /// numeric child position is not the logical identity authority.
96    pub fn scoped(parent: u128, key: &str) -> Self {
97        let mut hasher = blake3::Hasher::new();
98        hasher.update(b"scoped:");
99        hasher.update(&parent.to_le_bytes());
100        hasher.update(key.as_bytes());
101        let hash = hasher.finalize();
102        Self(u128::from_le_bytes(
103            hash.as_bytes()[0..16].try_into().unwrap(),
104        ))
105    }
106
107    /// Creates an identity scoped to a source location without allocating a
108    /// temporary location string.
109    #[doc(hidden)]
110    pub fn scoped_location(parent: u128, file: &str, line: u32, column: u32) -> Self {
111        let mut hasher = blake3::Hasher::new();
112        hasher.update(b"source-location:");
113        hasher.update(&parent.to_le_bytes());
114        hasher.update(file.as_bytes());
115        hasher.update(&line.to_le_bytes());
116        hasher.update(&column.to_le_bytes());
117        let hash = hasher.finalize();
118        Self(u128::from_le_bytes(
119            hash.as_bytes()[0..16].try_into().unwrap(),
120        ))
121    }
122}
123
124impl fmt::Debug for WidgetId {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        write!(f, "WidgetId({:032x})", self.0)
127    }
128}
129
130impl fmt::Display for WidgetId {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        write!(f, "{:032x}", self.0)
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::WidgetId;
139
140    #[test]
141    fn all_identity_domains_are_deterministic_and_distinct() {
142        let parent = WidgetId::explicit("parent");
143        let explicit = WidgetId::explicit("child");
144        let derived = WidgetId::derived(parent.as_u128(), &[4, 2]);
145        let scoped = WidgetId::scoped(parent.as_u128(), "child");
146
147        assert_eq!(explicit, WidgetId::explicit("child"));
148        assert_eq!(derived, WidgetId::derived(parent.as_u128(), &[4, 2]));
149        assert_eq!(scoped, WidgetId::scoped(parent.as_u128(), "child"));
150        assert_ne!(explicit, derived);
151        assert_ne!(explicit, scoped);
152        assert_ne!(derived, scoped);
153    }
154
155    #[test]
156    fn structural_path_segments_and_order_are_significant() {
157        let parent = WidgetId::explicit("parent");
158        assert_ne!(
159            WidgetId::derived(parent.as_u128(), &[1, 2]),
160            WidgetId::derived(parent.as_u128(), &[2, 1])
161        );
162        assert_ne!(
163            WidgetId::derived(parent.as_u128(), &[1, 2]),
164            WidgetId::derived(parent.as_u128(), &[1, 2, 0])
165        );
166    }
167
168    #[test]
169    fn parent_identity_namespaces_structural_and_scoped_children() {
170        let first = WidgetId::explicit("first-parent");
171        let second = WidgetId::explicit("second-parent");
172        assert_ne!(
173            WidgetId::derived(first.as_u128(), &[0]),
174            WidgetId::derived(second.as_u128(), &[0])
175        );
176        assert_ne!(
177            WidgetId::scoped(first.as_u128(), "item"),
178            WidgetId::scoped(second.as_u128(), "item")
179        );
180    }
181
182    #[test]
183    fn source_location_identity_includes_every_input() {
184        let parent = WidgetId::explicit("parent");
185        let base = WidgetId::scoped_location(parent.as_u128(), "src/app.rs", 10, 4);
186        assert_eq!(
187            base,
188            WidgetId::scoped_location(parent.as_u128(), "src/app.rs", 10, 4)
189        );
190        assert_ne!(
191            base,
192            WidgetId::scoped_location(parent.as_u128(), "src/other.rs", 10, 4)
193        );
194        assert_ne!(
195            base,
196            WidgetId::scoped_location(parent.as_u128(), "src/app.rs", 11, 4)
197        );
198        assert_ne!(
199            base,
200            WidgetId::scoped_location(parent.as_u128(), "src/app.rs", 10, 5)
201        );
202    }
203}