diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index c0e21fbe2..e7bb4b2e2 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -65,7 +65,12 @@ impl Id { /// Generate a new root [`Id`] by hashing some source (e.g. a string or integer). pub fn new(source: impl AsId) -> Self { - Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(source)) + let id = Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(&source)); + + #[cfg(debug_assertions)] + id_source::insert_root(id, &source); + + id } /// Generate a child [`Id`] by salting the parent [`Id`] with the given argument. @@ -73,8 +78,13 @@ impl Id { use std::hash::{BuildHasher as _, Hasher as _}; let mut hasher = ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher(); hasher.write_u64(self.value()); - hasher.write_u64(IdSalt::new(salt).value()); - Self::from_hash(hasher.finish()) + hasher.write_u64(IdSalt::new(&salt).value()); + let id = Self::from_hash(hasher.finish()); + + #[cfg(debug_assertions)] + id_source::insert_child(id, self, &salt); + + id } /// Short and readable summary @@ -116,10 +126,19 @@ impl Id { impl std::fmt::Debug for Id { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:04X}", self.value() as u16) + if *self == Self::NULL { + return write!(f, "Id::NULL"); + } + #[cfg(debug_assertions)] + if let Some(source) = id_source::get(*self) { + return f.write_str(&source); + } + write!(f, "id_{:04X}", self.value() as u16) } } +// ---------------------------------------------------------------------------- + /// Convenience impl From<&'static str> for Id { #[inline] @@ -135,12 +154,6 @@ impl From for Id { } } -#[test] -fn id_size() { - assert_eq!(std::mem::size_of::(), 8); - assert_eq!(std::mem::size_of::>(), 8); -} - // ---------------------------------------------------------------------------- /// `IdSet` is a `HashSet` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing. @@ -148,3 +161,108 @@ pub type IdSet = nohash_hasher::IntSet; /// `IdMap` is a `HashMap` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing. pub type IdMap = nohash_hasher::IntMap; + +// ---------------------------------------------------------------------------- + +/// In debug builds, remember the `Debug`-formatted call chain that produced each [`Id`]. +/// +/// Used by [`Id`]'s `Debug` impl so that `Id::new("foo")` prints as `Id::new("foo")`, +/// and `Id::new("foo").with("bar")` prints as `Id::new("foo").with("bar")`, etc. +#[cfg(debug_assertions)] +mod id_source { + use super::{AsId, AsIdSalt, Id, IdMap}; + use epaint::mutex::RwLock; + use std::sync::LazyLock; + + static SOURCE_MAP: LazyLock>> = LazyLock::new(RwLock::default); + + pub(super) fn insert_root(id: Id, source: &impl AsId) { + if SOURCE_MAP.read().contains_key(&id) { + return; + } + // Format outside the lock since `{source:?}` may itself recurse into [`Id`]'s `Debug` impl. + let formatted = format!("Id::new({source:?})"); + SOURCE_MAP.write().insert(id, formatted); + } + + pub(super) fn insert_child(id: Id, parent: Id, salt: &impl AsIdSalt) { + if SOURCE_MAP.read().contains_key(&id) { + return; + } + // Look up parent's repr and drop the read guard before formatting, + // since `{parent:?}` and `{salt:?}` may themselves recurse into [`Id`]'s `Debug` impl. + let cached_parent_repr = SOURCE_MAP.read().get(&parent).cloned(); + let parent_repr = cached_parent_repr.unwrap_or_else(|| format!("{parent:?}")); + let formatted = format!("{parent_repr}.with({salt:?})"); + SOURCE_MAP.write().insert(id, formatted); + } + + pub(super) fn get(id: Id) -> Option { + SOURCE_MAP.read().get(&id).cloned() + } +} + +#[test] +fn id_size() { + assert_eq!(std::mem::size_of::(), 8); + assert_eq!(std::mem::size_of::>(), 8); +} + +#[cfg(test)] +#[cfg(debug_assertions)] +mod debug_format_tests { + use crate::IdSalt; + + use super::Id; + + #[test] + fn root_string() { + let id = Id::new("foo"); + assert_eq!(format!("{id:?}"), r#"Id::new("foo")"#); + } + + #[test] + fn root_integer() { + let id = Id::new(42_i32); + assert_eq!(format!("{id:?}"), "Id::new(42)"); + } + + #[test] + fn root_id_salt() { + let id = Id::new(IdSalt::new("foo")); + assert_eq!(format!("{id:?}"), r#"Id::new(IdSalt::new("foo"))"#); + } + + #[test] + fn with_one_child() { + let id = Id::new("parent").with("child"); + assert_eq!(format!("{id:?}"), r#"Id::new("parent").with("child")"#); + } + + #[test] + fn with_chain() { + let id = Id::new("a").with("b").with("c").with(7_i32); + assert_eq!( + format!("{id:?}"), + r#"Id::new("a").with("b").with("c").with(7)"# + ); + } + + #[test] + fn nested_id_as_source() { + let inner = Id::new("foo"); + let outer = Id::new(inner); + assert_eq!(format!("{outer:?}"), r#"Id::new(Id::new("foo"))"#); + } + + #[test] + fn null_prints_as_null() { + assert_eq!(format!("{:?}", Id::NULL), "Id::NULL"); + } + + #[test] + fn null_as_parent() { + let id = Id::NULL.with("foo"); + assert_eq!(format!("{id:?}"), r#"Id::NULL.with("foo")"#); + } +} diff --git a/crates/egui/src/id_salt.rs b/crates/egui/src/id_salt.rs index 0912dcbd3..486dda239 100644 --- a/crates/egui/src/id_salt.rs +++ b/crates/egui/src/id_salt.rs @@ -30,7 +30,12 @@ impl nohash_hasher::IsEnabled for IdSalt {} impl IdSalt { /// Create a new [`IdSalt`] by hashing some source (e.g. a string or integer). pub fn new(source: impl AsIdSalt) -> Self { - Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(source)) + let id_salt = Self::from_hash(ahash::RandomState::with_seeds(5, 6, 7, 8).hash_one(&source)); + + #[cfg(debug_assertions)] + id_salt_source::maybe_insert(id_salt, &source); + + id_salt } /// Create a new root [`IdSalt`] from a high-entropy hash. @@ -54,6 +59,78 @@ impl IdSalt { impl std::fmt::Debug for IdSalt { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + #[cfg(debug_assertions)] + if let Some(source) = id_salt_source::get(*self) { + return write!(f, "IdSalt::new({source})"); + } write!(f, "salt_{:04X}", self.value() as u16) } } + +/// In debug builds, remember the `Debug`-formatted source that produced each [`IdSalt`]. +/// +/// Used by [`IdSalt`]'s `Debug` impl so that `IdSalt::new("foo")` prints as +/// `IdSalt::new("foo")`, and `IdSalt::new(IdSalt::new("foo"))` prints as +/// `IdSalt::new(IdSalt::new("foo"))`, etc. +#[cfg(debug_assertions)] +mod id_salt_source { + use super::{AsIdSalt, IdSalt}; + use epaint::mutex::RwLock; + use nohash_hasher::IntMap; + use std::sync::LazyLock; + + static SOURCE_MAP: LazyLock>> = LazyLock::new(RwLock::default); + + pub(super) fn maybe_insert(id_salt: IdSalt, source: &impl AsIdSalt) { + if !SOURCE_MAP.read().contains_key(&id_salt) { + let formatted = format!("{source:?}"); + SOURCE_MAP.write().insert(id_salt, formatted); + } + } + + pub(super) fn get(id_salt: IdSalt) -> Option { + SOURCE_MAP.read().get(&id_salt).cloned() + } +} + +#[cfg(test)] +#[cfg(debug_assertions)] +mod tests { + use super::IdSalt; + + #[test] + fn debug_format_string_source() { + let salt = IdSalt::new("foo"); + assert_eq!(format!("{salt:?}"), r#"IdSalt::new("foo")"#); + } + + #[test] + fn debug_format_integer_source() { + let salt = IdSalt::new(42_i32); + assert_eq!(format!("{salt:?}"), "IdSalt::new(42)"); + } + + #[test] + fn debug_format_nested_salt() { + let inner = IdSalt::new("foo"); + let outer = IdSalt::new(inner); + assert_eq!(format!("{outer:?}"), r#"IdSalt::new(IdSalt::new("foo"))"#); + } + + #[test] + fn debug_format_triple_nested_salt() { + let a = IdSalt::new("foo"); + let b = IdSalt::new(a); + let c = IdSalt::new(b); + assert_eq!( + format!("{c:?}"), + r#"IdSalt::new(IdSalt::new(IdSalt::new("foo")))"# + ); + } + + #[test] + fn debug_format_tuple_source() { + let salt = IdSalt::new(("foo", 7_i32)); + assert_eq!(format!("{salt:?}"), r#"IdSalt::new(("foo", 7))"#); + } +} diff --git a/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png b/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png index f26458ea5..53575912a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9b497aa9b2b92843937c84a6ff8901f248501d5d4a89c2fb1237008a44fcad1 -size 114038 +oid sha256:bbd7fa4db7dd580968949b9d76c1521d5627cc1ee8cd19bb3dea752d3d47607b +size 114174