egui/id.rs
1// TODO(emilk): have separate types `PositionId` and `UniqueId`. ?
2
3use std::num::NonZeroU64;
4
5/// egui tracks widgets frame-to-frame using [`Id`]s.
6///
7/// For instance, if you start dragging a slider one frame, egui stores
8/// the sliders [`Id`] as the current active id so that next frame when
9/// you move the mouse the same slider changes, even if the mouse has
10/// moved outside the slider.
11///
12/// For some widgets [`Id`]s are also used to persist some state about the
13/// widgets, such as Window position or whether not a collapsing header region is open.
14///
15/// This implies that the [`Id`]s must be unique.
16///
17/// For simple things like sliders and buttons that don't have any memory and
18/// doesn't move we can use the location of the widget as a source of identity.
19/// For instance, a slider only needs a unique and persistent ID while you are
20/// dragging the slider. As long as it is still while moving, that is fine.
21///
22/// For things that need to persist state even after moving (windows, collapsing headers)
23/// the location of the widgets is obviously not good enough. For instance,
24/// a collapsing region needs to remember whether or not it is open even
25/// if the layout next frame is different and the collapsing is not lower down
26/// on the screen.
27///
28/// Then there are widgets that need no identifiers at all, like labels,
29/// because they have no state nor are interacted with.
30///
31/// This is niche-optimized to that `Option<Id>` is the same size as `Id`.
32#[derive(Clone, Copy, Hash, Eq, PartialEq)]
33#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
34pub struct Id(NonZeroU64);
35
36impl nohash_hasher::IsEnabled for Id {}
37
38impl Id {
39 /// A special [`Id`], in particular as a key to [`crate::Memory::data`]
40 /// for when there is no particular widget to attach the data.
41 ///
42 /// The null [`Id`] is still a valid id to use in all circumstances,
43 /// though obviously it will lead to a lot of collisions if you do use it!
44 pub const NULL: Self = Self(NonZeroU64::MAX);
45
46 #[inline]
47 const fn from_hash(hash: u64) -> Self {
48 if let Some(nonzero) = NonZeroU64::new(hash) {
49 Self(nonzero)
50 } else {
51 Self(NonZeroU64::MIN) // The hash was exactly zero (very bad luck)
52 }
53 }
54
55 /// Generate a new [`Id`] by hashing some source (e.g. a string or integer).
56 pub fn new(source: impl std::hash::Hash) -> Self {
57 Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(source))
58 }
59
60 /// Generate a new [`Id`] by hashing the parent [`Id`] and the given argument.
61 pub fn with(self, child: impl std::hash::Hash) -> Self {
62 use std::hash::{BuildHasher as _, Hasher as _};
63 let mut hasher = ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher();
64 hasher.write_u64(self.0.get());
65 child.hash(&mut hasher);
66 Self::from_hash(hasher.finish())
67 }
68
69 /// Short and readable summary
70 pub fn short_debug_format(&self) -> String {
71 format!("{:04X}", self.value() as u16)
72 }
73
74 /// The inner value of the [`Id`].
75 ///
76 /// This is a high-entropy hash, or [`Self::NULL`].
77 #[inline(always)]
78 pub fn value(&self) -> u64 {
79 self.0.get()
80 }
81
82 #[cfg(feature = "accesskit")]
83 pub(crate) fn accesskit_id(&self) -> accesskit::NodeId {
84 self.value().into()
85 }
86
87 /// Create a new [`Id`] from a high-entropy value. No hashing is done.
88 ///
89 /// This can be useful if you have an [`Id`] that was converted to some other type
90 /// (e.g. accesskit::NodeId) and you want to convert it back to an [`Id`].
91 ///
92 /// # Safety
93 /// You need to ensure that the value is high-entropy since it might be used in
94 /// a [`IdSet`] or [`IdMap`], which rely on the assumption that [`Id`]s have good entropy.
95 ///
96 /// The method is not unsafe in terms of memory safety.
97 ///
98 /// # Panics
99 /// If the value is zero, this will panic.
100 #[doc(hidden)]
101 #[expect(unsafe_code)]
102 pub unsafe fn from_high_entropy_bits(value: u64) -> Self {
103 Self(NonZeroU64::new(value).expect("Id must be non-zero."))
104 }
105}
106
107impl std::fmt::Debug for Id {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 write!(f, "{:04X}", self.value() as u16)
110 }
111}
112
113/// Convenience
114impl From<&'static str> for Id {
115 #[inline]
116 fn from(string: &'static str) -> Self {
117 Self::new(string)
118 }
119}
120
121impl From<String> for Id {
122 #[inline]
123 fn from(string: String) -> Self {
124 Self::new(string)
125 }
126}
127
128#[test]
129fn id_size() {
130 assert_eq!(std::mem::size_of::<Id>(), 8);
131 assert_eq!(std::mem::size_of::<Option<Id>>(), 8);
132}
133
134// ----------------------------------------------------------------------------
135
136/// `IdSet` is a `HashSet<Id>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
137pub type IdSet = nohash_hasher::IntSet<Id>;
138
139/// `IdMap<V>` is a `HashMap<Id, V>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
140pub type IdMap<V> = nohash_hasher::IntMap<Id, V>;