Skip to main content

egui/
id.rs

1// TODO(emilk): have separate types `PositionId` and `UniqueId`. ?
2
3use core::num::NonZeroU64;
4
5use crate::{AsIdSalt, IdSalt};
6
7/// Types that can be converted to an [`Id`].
8///
9/// This is all types implementing `Hash` and `Debug`,
10/// which includes things like string, integers, tuples of those, etc.
11pub trait AsId: core::hash::Hash + core::fmt::Debug {}
12
13impl<T: core::hash::Hash + core::fmt::Debug> AsId for T {}
14
15/// egui tracks widgets frame-to-frame using [`Id`]s.
16///
17/// For instance, if you start dragging a slider one frame, egui stores
18/// the sliders [`Id`] as the current active id so that next frame when
19/// you move the mouse the same slider changes, even if the mouse has
20/// moved outside the slider.
21///
22/// For some widgets [`Id`]s are also used to persist some state about the
23/// widgets, such as Window position or whether not a collapsing header region is open.
24///
25/// This implies that the [`Id`]s must be unique.
26///
27/// For simple things like sliders and buttons that don't have any memory and
28/// doesn't move we can use the location of the widget as a source of identity.
29/// For instance, a slider only needs a unique and persistent ID while you are
30/// dragging the slider. As long as it is still while moving, that is fine.
31///
32/// For things that need to persist state even after moving (windows, collapsing headers)
33/// the location of the widgets is obviously not good enough. For instance,
34/// a collapsing region needs to remember whether or not it is open even
35/// if the layout next frame is different and the collapsing is not lower down
36/// on the screen.
37///
38/// Then there are widgets that need no identifiers at all, like labels,
39/// because they have no state nor are interacted with.
40///
41/// This is niche-optimized to that `Option<Id>` is the same size as `Id`.
42#[derive(Clone, Copy, Hash, Eq, PartialEq)]
43#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
44pub struct Id(NonZeroU64);
45
46impl nohash_hasher::IsEnabled for Id {}
47
48impl Id {
49    /// A special [`Id`], in particular as a key to [`crate::Memory::data`]
50    /// for when there is no particular widget to attach the data.
51    ///
52    /// The null [`Id`] is still a valid id to use in all circumstances,
53    /// though obviously it will lead to a lot of collisions if you do use it!
54    pub const NULL: Self = Self(NonZeroU64::MAX);
55
56    /// Create a new root [`Id`] from a high-entropy hash.
57    #[inline]
58    const fn from_hash(hash: u64) -> Self {
59        if let Some(nonzero) = NonZeroU64::new(hash) {
60            Self(nonzero)
61        } else {
62            Self(NonZeroU64::MIN) // The hash was exactly zero (very bad luck)
63        }
64    }
65
66    /// Generate a new root [`Id`] by hashing some source (e.g. a string or integer).
67    pub fn new(source: impl AsId) -> Self {
68        let id = Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(&source));
69
70        #[cfg(debug_assertions)]
71        id_source::insert_root(id, &source);
72
73        id
74    }
75
76    /// Generate a child [`Id`] by salting the parent [`Id`] with the given argument.
77    pub fn with(self, salt: impl AsIdSalt) -> Self {
78        use core::hash::{BuildHasher as _, Hasher as _};
79        let mut hasher = ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher();
80        hasher.write_u64(self.value());
81        hasher.write_u64(IdSalt::new(&salt).value());
82        let id = Self::from_hash(hasher.finish());
83
84        #[cfg(debug_assertions)]
85        id_source::insert_child(id, self, &salt);
86
87        id
88    }
89
90    /// Short and readable summary
91    pub fn short_debug_format(&self) -> String {
92        format!("{:04X}", self.value() as u16)
93    }
94
95    /// The inner value of the [`Id`].
96    ///
97    /// This is a high-entropy hash, or [`Self::NULL`].
98    #[inline(always)]
99    pub fn value(&self) -> u64 {
100        self.0.get()
101    }
102
103    pub fn accesskit_id(&self) -> accesskit::NodeId {
104        self.value().into()
105    }
106
107    /// Create a new [`Id`] from a high-entropy value. No hashing is done.
108    ///
109    /// This can be useful if you have an [`Id`] that was converted to some other type
110    /// (e.g. accesskit::NodeId) and you want to convert it back to an [`Id`].
111    ///
112    /// # Safety
113    /// You need to ensure that the value is high-entropy since it might be used in
114    /// a [`IdSet`] or [`IdMap`], which rely on the assumption that [`Id`]s have good entropy.
115    ///
116    /// The method is not unsafe in terms of memory safety.
117    ///
118    /// # Panics
119    /// If the value is zero, this will panic.
120    #[doc(hidden)]
121    #[expect(unsafe_code)]
122    pub unsafe fn from_high_entropy_bits(value: u64) -> Self {
123        Self(NonZeroU64::new(value).expect("Id must be non-zero."))
124    }
125}
126
127impl core::fmt::Debug for Id {
128    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
129        if *self == Self::NULL {
130            return write!(f, "Id::NULL");
131        }
132        #[cfg(debug_assertions)]
133        if let Some(source) = id_source::get(*self) {
134            return f.write_str(&source);
135        }
136        write!(f, "id_{:04X}", self.value() as u16)
137    }
138}
139
140// ----------------------------------------------------------------------------
141
142/// Convenience
143impl From<&'static str> for Id {
144    #[inline]
145    fn from(string: &'static str) -> Self {
146        Self::new(string)
147    }
148}
149
150impl From<String> for Id {
151    #[inline]
152    fn from(string: String) -> Self {
153        Self::new(string)
154    }
155}
156
157// ----------------------------------------------------------------------------
158
159/// `IdSet` is a `HashSet<Id>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
160pub type IdSet = nohash_hasher::IntSet<Id>;
161
162/// `IdMap<V>` is a `HashMap<Id, V>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
163pub type IdMap<V> = nohash_hasher::IntMap<Id, V>;
164
165// ----------------------------------------------------------------------------
166
167/// In debug builds, remember the `Debug`-formatted call chain that produced each [`Id`].
168///
169/// Used by [`Id`]'s `Debug` impl so that `Id::new("foo")` prints as `Id::new("foo")`,
170/// and `Id::new("foo").with("bar")` prints as `Id::new("foo").with("bar")`, etc.
171#[cfg(debug_assertions)]
172mod id_source {
173    use super::{AsId, AsIdSalt, Id, IdMap};
174    use epaint::mutex::RwLock;
175    use std::sync::LazyLock;
176
177    static SOURCE_MAP: LazyLock<RwLock<IdMap<String>>> = LazyLock::new(RwLock::default);
178
179    pub(super) fn insert_root(id: Id, source: &impl AsId) {
180        if SOURCE_MAP.read().contains_key(&id) {
181            return;
182        }
183        // Format outside the lock since `{source:?}` may itself recurse into [`Id`]'s `Debug` impl.
184        let formatted = format!("Id::new({source:?})");
185        SOURCE_MAP.write().insert(id, formatted);
186    }
187
188    pub(super) fn insert_child(id: Id, parent: Id, salt: &impl AsIdSalt) {
189        if SOURCE_MAP.read().contains_key(&id) {
190            return;
191        }
192        // Look up parent's repr and drop the read guard before formatting,
193        // since `{parent:?}` and `{salt:?}` may themselves recurse into [`Id`]'s `Debug` impl.
194        let cached_parent_repr = SOURCE_MAP.read().get(&parent).cloned();
195        let parent_repr = cached_parent_repr.unwrap_or_else(|| format!("{parent:?}"));
196        let formatted = format!("{parent_repr}.with({salt:?})");
197        SOURCE_MAP.write().insert(id, formatted);
198    }
199
200    pub(super) fn get(id: Id) -> Option<String> {
201        SOURCE_MAP.read().get(&id).cloned()
202    }
203}
204
205#[test]
206fn id_size() {
207    assert_eq!(core::mem::size_of::<Id>(), 8);
208    assert_eq!(core::mem::size_of::<Option<Id>>(), 8);
209}
210
211#[cfg(test)]
212#[cfg(debug_assertions)]
213mod debug_format_tests {
214    use crate::IdSalt;
215
216    use super::Id;
217
218    #[test]
219    fn root_string() {
220        let id = Id::new("foo");
221        assert_eq!(format!("{id:?}"), r#"Id::new("foo")"#);
222    }
223
224    #[test]
225    fn root_integer() {
226        let id = Id::new(42_i32);
227        assert_eq!(format!("{id:?}"), "Id::new(42)");
228    }
229
230    #[test]
231    fn root_id_salt() {
232        let id = Id::new(IdSalt::new("foo"));
233        assert_eq!(format!("{id:?}"), r#"Id::new(IdSalt::new("foo"))"#);
234    }
235
236    #[test]
237    fn with_one_child() {
238        let id = Id::new("parent").with("child");
239        assert_eq!(format!("{id:?}"), r#"Id::new("parent").with("child")"#);
240    }
241
242    #[test]
243    fn with_chain() {
244        let id = Id::new("a").with("b").with("c").with(7_i32);
245        assert_eq!(
246            format!("{id:?}"),
247            r#"Id::new("a").with("b").with("c").with(7)"#
248        );
249    }
250
251    #[test]
252    fn nested_id_as_source() {
253        let inner = Id::new("foo");
254        let outer = Id::new(inner);
255        assert_eq!(format!("{outer:?}"), r#"Id::new(Id::new("foo"))"#);
256    }
257
258    #[test]
259    fn null_prints_as_null() {
260        assert_eq!(format!("{:?}", Id::NULL), "Id::NULL");
261    }
262
263    #[test]
264    fn null_as_parent() {
265        let id = Id::NULL.with("foo");
266        assert_eq!(format!("{id:?}"), r#"Id::NULL.with("foo")"#);
267    }
268}