use core::num::NonZeroU64;
use crate::{AsIdSalt, IdSalt};
#[deprecated = "Use AsIdSalt or Id instead"]
pub trait AsId: core::hash::Hash + core::fmt::Debug {}
#[expect(deprecated)]
impl<T: core::hash::Hash + core::fmt::Debug> AsId for T {}
#[derive(Clone, Copy, Hash, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(
feature = "serde",
expect(
clippy::unsafe_derive_deserialize,
reason = "`from_high_entropy_bits` is only `unsafe` about entropy, not memory safety"
)
)]
pub struct Id(NonZeroU64);
impl nohash_hasher::IsEnabled for Id {}
impl Id {
pub const NULL: Self = Self(NonZeroU64::MAX);
#[inline]
const fn from_hash(hash: u64) -> Self {
if let Some(nonzero) = NonZeroU64::new(hash) {
Self(nonzero)
} else {
Self(NonZeroU64::MIN) }
}
pub fn unique(source: impl core::hash::Hash + core::fmt::Debug) -> Self {
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
}
#[deprecated = "Use `Id::unique` (for a globally unique id) or `IdSalt::new` (for a locally unique salt) instead"]
pub fn new(source: impl core::hash::Hash + core::fmt::Debug) -> Self {
Self::unique(source)
}
pub fn with(self, salt: impl AsIdSalt) -> Self {
let id = self.hash_with_salt(IdSalt::new(&salt));
#[cfg(debug_assertions)]
id_source::insert_child(id, self, &salt);
id
}
pub fn with_salt(self, salt: IdSalt) -> Self {
let id = self.hash_with_salt(salt);
#[cfg(debug_assertions)]
id_source::insert_child(id, self, &salt);
id
}
fn hash_with_salt(self, salt: IdSalt) -> Self {
use core::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(salt.value());
Self::from_hash(hasher.finish())
}
pub fn short_debug_format(&self) -> String {
format!("{:04X}", self.value() as u16)
}
#[inline(always)]
pub fn value(&self) -> u64 {
self.0.get()
}
pub fn accesskit_id(&self) -> accesskit::NodeId {
self.value().into()
}
#[doc(hidden)]
#[expect(unsafe_code)]
pub unsafe fn from_high_entropy_bits(value: u64) -> Self {
Self(NonZeroU64::new(value).expect("Id must be non-zero."))
}
}
impl core::fmt::Debug for Id {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
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)
}
}
pub type IdSet = nohash_hasher::IntSet<Id>;
pub type IdMap<V> = nohash_hasher::IntMap<Id, V>;
#[cfg(debug_assertions)]
pub(crate) mod id_source {
use super::{AsIdSalt, Id, IdMap};
use epaint::mutex::RwLock;
use std::sync::LazyLock;
static SOURCE_MAP: LazyLock<RwLock<IdMap<String>>> = LazyLock::new(RwLock::default);
const MAX_SOURCE_LEN: usize = 1024;
pub(crate) fn truncate(mut source: String) -> String {
if MAX_SOURCE_LEN < source.len() {
let start = source.ceil_char_boundary(source.len() - MAX_SOURCE_LEN);
source.replace_range(..start, "…");
}
source
}
pub(super) fn insert_root(id: Id, source: &impl core::fmt::Debug) {
if SOURCE_MAP.read().contains_key(&id) {
return;
}
let formatted = truncate(format!("Id::unique({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;
}
let cached_parent_repr = SOURCE_MAP.read().get(&parent).cloned();
let parent_repr = cached_parent_repr.unwrap_or_else(|| format!("{parent:?}"));
let formatted = truncate(format!("{parent_repr}.with({salt:?})"));
SOURCE_MAP.write().insert(id, formatted);
}
pub(super) fn get(id: Id) -> Option<String> {
SOURCE_MAP.read().get(&id).cloned()
}
}
#[test]
fn id_size() {
assert_eq!(core::mem::size_of::<Id>(), 8);
assert_eq!(core::mem::size_of::<Option<Id>>(), 8);
}
#[cfg(test)]
#[cfg(debug_assertions)]
mod debug_format_tests {
use crate::IdSalt;
use super::Id;
#[test]
fn root_string() {
let id = Id::unique("foo");
assert_eq!(format!("{id:?}"), r#"Id::unique("foo")"#);
}
#[test]
fn root_integer() {
let id = Id::unique(42_i32);
assert_eq!(format!("{id:?}"), "Id::unique(42)");
}
#[test]
fn root_id_salt() {
let id = Id::unique(IdSalt::new("foo"));
assert_eq!(format!("{id:?}"), r#"Id::unique(IdSalt::new("foo"))"#);
}
#[test]
fn with_salt_matches_with() {
let parent = Id::unique("salted_parent");
assert_eq!(
parent.with_salt(IdSalt::new("salted_child")),
parent.with("salted_child")
);
}
#[test]
fn with_one_child() {
let id = Id::unique("parent").with("child");
assert_eq!(format!("{id:?}"), r#"Id::unique("parent").with("child")"#);
}
#[test]
fn with_chain() {
let id = Id::unique("a").with("b").with("c").with(7_i32);
assert_eq!(
format!("{id:?}"),
r#"Id::unique("a").with("b").with("c").with(7)"#
);
}
#[test]
fn nested_id_as_source() {
let inner = Id::unique("foo");
let outer = Id::unique(inner);
assert_eq!(format!("{outer:?}"), r#"Id::unique(Id::unique("foo"))"#);
}
#[test]
fn salting_with_self_is_truncated() {
let mut id = Id::unique("self_salted");
for _ in 0..64 {
id = id.with(id);
}
let formatted = format!("{id:?}");
assert!(formatted.starts_with('…'), "{formatted}");
assert!(formatted.len() < 2000, "{} bytes", formatted.len());
}
#[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")"#);
}
}