use std::num::NonZeroU64;
use crate::{AsIdSalt, IdSalt};
pub trait AsId: std::hash::Hash + std::fmt::Debug {}
impl<T: std::hash::Hash + std::fmt::Debug> AsId for T {}
#[derive(Clone, Copy, Hash, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
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 new(source: impl AsId) -> 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
}
pub fn with(self, salt: impl AsIdSalt) -> Self {
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());
let id = Self::from_hash(hasher.finish());
#[cfg(debug_assertions)]
id_source::insert_child(id, self, &salt);
id
}
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 std::fmt::Debug for Id {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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)
}
}
impl From<&'static str> for Id {
#[inline]
fn from(string: &'static str) -> Self {
Self::new(string)
}
}
impl From<String> for Id {
#[inline]
fn from(string: String) -> Self {
Self::new(string)
}
}
pub type IdSet = nohash_hasher::IntSet<Id>;
pub type IdMap<V> = nohash_hasher::IntMap<Id, V>;
#[cfg(debug_assertions)]
mod id_source {
use super::{AsId, AsIdSalt, Id, IdMap};
use epaint::mutex::RwLock;
use std::sync::LazyLock;
static SOURCE_MAP: LazyLock<RwLock<IdMap<String>>> = LazyLock::new(RwLock::default);
pub(super) fn insert_root(id: Id, source: &impl AsId) {
if SOURCE_MAP.read().contains_key(&id) {
return;
}
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;
}
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<String> {
SOURCE_MAP.read().get(&id).cloned()
}
}
#[test]
fn id_size() {
assert_eq!(std::mem::size_of::<Id>(), 8);
assert_eq!(std::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::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")"#);
}
}