Skip to main content

egui/
id_salt.rs

1use core::num::NonZeroU64;
2
3/// Types that can be converted to an [`IdSalt`].
4///
5/// This is all types implementing `Hash` and `Debug`,
6/// which includes things like string, integers, tuples of those, etc.
7pub trait AsIdSalt: core::hash::Hash + core::fmt::Debug {}
8
9impl<T: core::hash::Hash + core::fmt::Debug> AsIdSalt for T {}
10
11/// Uniquely identifies a child widget within a parent widget.
12///
13/// An [`IdSalt`] is only unique within a parent [`crate::Id`].
14/// An [`IdSalt`] is NOT globally unique.
15///
16/// You combine a parent [`crate::Id`] with an [`IdSalt`] to get a child [`crate::Id`],
17/// using [`crate::Id::with`].
18///
19/// An [`IdSalt`] is usually a string, an integer, or similar.
20///
21/// An [`IdSalt`] should NOT be produced from an [`crate::Id`].
22///
23/// This is niche-optimized to that `Option<IdSalt>` is the same size as `IdSalt`.
24#[derive(Clone, Copy, Hash, Eq, PartialEq)]
25#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
26pub struct IdSalt(NonZeroU64);
27
28impl nohash_hasher::IsEnabled for IdSalt {}
29
30impl IdSalt {
31    /// Create a new [`IdSalt`] by hashing some source (e.g. a string or integer).
32    pub fn new(source: impl AsIdSalt) -> Self {
33        let id_salt = Self::from_hash(ahash::RandomState::with_seeds(5, 6, 7, 8).hash_one(&source));
34
35        #[cfg(debug_assertions)]
36        id_salt_source::maybe_insert(id_salt, &source);
37
38        id_salt
39    }
40
41    /// Create a new root [`IdSalt`] from a high-entropy hash.
42    #[inline]
43    const fn from_hash(hash: u64) -> Self {
44        if let Some(nonzero) = NonZeroU64::new(hash) {
45            Self(nonzero)
46        } else {
47            Self(NonZeroU64::MIN) // The hash was exactly zero
48        }
49    }
50
51    /// The inner value of the [`IdSalt`].
52    ///
53    /// This is a high-entropy hash.
54    #[inline(always)]
55    pub fn value(&self) -> u64 {
56        self.0.get()
57    }
58}
59
60impl core::fmt::Debug for IdSalt {
61    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
62        #[cfg(debug_assertions)]
63        if let Some(source) = id_salt_source::get(*self) {
64            return write!(f, "IdSalt::new({source})");
65        }
66        write!(f, "salt_{:04X}", self.value() as u16)
67    }
68}
69
70/// In debug builds, remember the `Debug`-formatted source that produced each [`IdSalt`].
71///
72/// Used by [`IdSalt`]'s `Debug` impl so that `IdSalt::new("foo")` prints as
73/// `IdSalt::new("foo")`, and `IdSalt::new(IdSalt::new("foo"))` prints as
74/// `IdSalt::new(IdSalt::new("foo"))`, etc.
75#[cfg(debug_assertions)]
76mod id_salt_source {
77    use super::{AsIdSalt, IdSalt};
78    use epaint::mutex::RwLock;
79    use nohash_hasher::IntMap;
80    use std::sync::LazyLock;
81
82    static SOURCE_MAP: LazyLock<RwLock<IntMap<IdSalt, String>>> = LazyLock::new(RwLock::default);
83
84    pub(super) fn maybe_insert(id_salt: IdSalt, source: &impl AsIdSalt) {
85        if !SOURCE_MAP.read().contains_key(&id_salt) {
86            let formatted = format!("{source:?}");
87            SOURCE_MAP.write().insert(id_salt, formatted);
88        }
89    }
90
91    pub(super) fn get(id_salt: IdSalt) -> Option<String> {
92        SOURCE_MAP.read().get(&id_salt).cloned()
93    }
94}
95
96#[cfg(test)]
97#[cfg(debug_assertions)]
98mod tests {
99    use super::IdSalt;
100
101    #[test]
102    fn debug_format_string_source() {
103        let salt = IdSalt::new("foo");
104        assert_eq!(format!("{salt:?}"), r#"IdSalt::new("foo")"#);
105    }
106
107    #[test]
108    fn debug_format_integer_source() {
109        let salt = IdSalt::new(42_i32);
110        assert_eq!(format!("{salt:?}"), "IdSalt::new(42)");
111    }
112
113    #[test]
114    fn debug_format_nested_salt() {
115        let inner = IdSalt::new("foo");
116        let outer = IdSalt::new(inner);
117        assert_eq!(format!("{outer:?}"), r#"IdSalt::new(IdSalt::new("foo"))"#);
118    }
119
120    #[test]
121    fn debug_format_triple_nested_salt() {
122        let a = IdSalt::new("foo");
123        let b = IdSalt::new(a);
124        let c = IdSalt::new(b);
125        assert_eq!(
126            format!("{c:?}"),
127            r#"IdSalt::new(IdSalt::new(IdSalt::new("foo")))"#
128        );
129    }
130
131    #[test]
132    fn debug_format_tuple_source() {
133        let salt = IdSalt::new(("foo", 7_i32));
134        assert_eq!(format!("{salt:?}"), r#"IdSalt::new(("foo", 7))"#);
135    }
136}