1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-31 22:00:03 -04:00

Add IdInfo and only enable this with debug_assertions

This commit is contained in:
lucasmerlin
2025-03-27 16:05:37 +01:00
parent f4dcb996da
commit 4ca5e49722
6 changed files with 152 additions and 94 deletions

View File

@@ -306,13 +306,13 @@ impl ScrollArea {
/// A source for the unique [`Id`], e.g. `.id_source("second_scroll_area")` or `.id_source(loop_index)`. /// A source for the unique [`Id`], e.g. `.id_source("second_scroll_area")` or `.id_source(loop_index)`.
#[inline] #[inline]
#[deprecated = "Renamed id_salt"] #[deprecated = "Renamed id_salt"]
pub fn id_source(self, id_salt: impl crate::IdTrait) -> Self { pub fn id_source(self, id_salt: impl crate::AsId) -> Self {
self.id_salt(id_salt) self.id_salt(id_salt)
} }
/// A source for the unique [`Id`], e.g. `.id_salt("second_scroll_area")` or `.id_salt(loop_index)`. /// A source for the unique [`Id`], e.g. `.id_salt("second_scroll_area")` or `.id_salt(loop_index)`.
#[inline] #[inline]
pub fn id_salt(mut self, id_salt: impl crate::IdTrait) -> Self { pub fn id_salt(mut self, id_salt: impl crate::AsId) -> Self {
self.id_salt = Some(Id::new(id_salt)); self.id_salt = Some(Id::new(id_salt));
self self
} }

View File

@@ -1,11 +1,6 @@
// TODO(emilk): have separate types `PositionId` and `UniqueId`. ? // TODO(emilk): have separate types `PositionId` and `UniqueId`. ?
use ahash::HashMap;
use epaint::mutex::{Mutex, RwLock};
use std::any::TypeId;
use std::hash::Hasher;
use std::num::NonZeroU64; use std::num::NonZeroU64;
use std::sync::LazyLock;
/// egui tracks widgets frame-to-frame using [`Id`]s. /// egui tracks widgets frame-to-frame using [`Id`]s.
/// ///
@@ -38,21 +33,10 @@ use std::sync::LazyLock;
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Id(NonZeroU64); pub struct Id(NonZeroU64);
enum IdSource {
Id(Id),
Other(String),
}
static ID_MAP: LazyLock<RwLock<HashMap<Id, (IdSource, Option<Id>)>>> = LazyLock::new(|| {
let mut map = HashMap::default();
map.insert(Id::NULL, (IdSource::Other("Id::NULL".to_owned()), None));
RwLock::new(map)
});
impl nohash_hasher::IsEnabled for Id {} impl nohash_hasher::IsEnabled for Id {}
pub trait IdTrait: std::hash::Hash + std::fmt::Debug {} pub trait AsId: std::hash::Hash + std::fmt::Debug {}
impl<T: std::hash::Hash + std::fmt::Debug> IdTrait for T {} impl<T: std::hash::Hash + std::fmt::Debug> AsId for T {}
impl Id { impl Id {
/// A special [`Id`], in particular as a key to [`crate::Memory::data`] /// A special [`Id`], in particular as a key to [`crate::Memory::data`]
@@ -71,82 +55,26 @@ impl Id {
} }
} }
/// Checks if [`T`] is a [`Id`].
///
/// If it is, it returns `IdSource::Id`, otherwise it returns `IdSource::Other`.
fn get_source<T: IdTrait>(t: T) -> IdSource {
/// Ugly hack to try to determine if T is an Id or not.
struct FakeHasher {
val: Option<u64>,
first: bool,
}
impl Hasher for FakeHasher {
fn finish(&self) -> u64 {
unreachable!()
}
fn write(&mut self, bytes: &[u8]) {
self.first = false;
}
fn write_u64(&mut self, i: u64) {
if self.first {
self.val = Some(i);
self.first = false;
} else {
self.val = None;
}
}
}
let mut hasher = FakeHasher {
val: None,
first: true,
};
t.hash(&mut hasher);
let maybe_source_id = hasher.val.map(Id::from_hash);
// Ideally we would just implement IdTriat for Id with specialization, but that's not
// a thing yet :( So we check if the hash is already in the map, if so, the source must be
// an Id.
if let Some(maybe_source_id) = maybe_source_id {
if ID_MAP.read().contains_key(&maybe_source_id) {
IdSource::Id(maybe_source_id)
} else {
IdSource::Other(format!("{:?}", t))
}
} else {
IdSource::Other(format!("{:?}", t))
}
}
/// Generate a new [`Id`] by hashing some source (e.g. a string or integer). /// Generate a new [`Id`] by hashing some source (e.g. a string or integer).
pub fn new<T: IdTrait>(source: T) -> Self { pub fn new<T: AsId>(source: T) -> Self {
let id = 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));
if !ID_MAP.read().contains_key(&id) { #[cfg(debug_assertions)]
let source = Self::get_source(source); id_source::maybe_insert(id, source, None);
ID_MAP.write().insert(id, (source, None));
}
id id
} }
/// Generate a new [`Id`] by hashing the parent [`Id`] and the given argument. /// Generate a new [`Id`] by hashing the parent [`Id`] and the given argument.
pub fn with(self, child: impl std::hash::Hash + std::fmt::Debug) -> Self { pub fn with(self, child: impl AsId) -> Self {
use std::hash::{BuildHasher, Hasher}; use std::hash::{BuildHasher, Hasher};
let mut hasher = ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher(); let mut hasher = ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher();
hasher.write_u64(self.0.get()); hasher.write_u64(self.0.get());
(&child).hash(&mut hasher); (&child).hash(&mut hasher);
let id = Self::from_hash(hasher.finish()); let id = Self::from_hash(hasher.finish());
if !ID_MAP.read().contains_key(&id) { #[cfg(debug_assertions)]
let source = Self::get_source(child); id_source::maybe_insert(id, &child, Some(self));
ID_MAP.write().insert(id, (source, Some(self)));
}
id id
} }
@@ -168,22 +96,152 @@ impl Id {
pub(crate) fn accesskit_id(&self) -> accesskit::NodeId { pub(crate) fn accesskit_id(&self) -> accesskit::NodeId {
self.value().into() self.value().into()
} }
// TODO: Nice debug ui
// pub fn ui(self, ui: &mut crate::Ui) -> crate::Response {
// ui.code(self.short_debug_format()).on_hover_ui(|ui| {
// let data = self.info();
// })
// }
}
#[cfg(debug_assertions)]
mod id_source {
use crate::{AsId, Id};
use ahash::HashMap;
use epaint::mutex::RwLock;
use std::hash::Hasher;
use std::sync::LazyLock;
#[derive(Clone)]
pub struct IdInfo {
/// What was this Id generated from?
pub source: IdSource,
/// If the Id was crated via [`Id::with`], what was the parent Id?
pub parent: Option<Id>,
}
#[derive(Clone)]
pub enum IdSource {
Id(Id),
Other(String),
}
static ID_MAP: LazyLock<RwLock<HashMap<Id, IdInfo>>> = LazyLock::new(|| {
let mut map = HashMap::default();
map.insert(
Id::NULL,
IdInfo {
source: IdSource::Other("Id::NULL".to_owned()),
parent: None,
},
);
RwLock::new(map)
});
/// Ugly hack to try to determine if T is an Id or not.
#[derive(Default)]
struct ExtractIdHasher {
val: Option<u64>,
not_id: bool,
}
impl ExtractIdHasher {
fn id(&self) -> Option<Id> {
self.val.map(Id::from_hash)
}
}
impl Hasher for ExtractIdHasher {
fn finish(&self) -> u64 {
unreachable!()
}
fn write(&mut self, _bytes: &[u8]) {
self.not_id = true;
self.val = None;
}
fn write_u64(&mut self, i: u64) {
if !self.not_id && !self.val.is_some() {
self.val = Some(i);
} else {
self.not_id = true;
self.val = None;
}
}
}
/// Checks if [`T`] is a [`Id`].
///
/// If it is, it returns `IdSource::Id`, otherwise it returns `IdSource::Other`.
fn get_source<T: AsId>(t: T) -> IdSource {
let mut hasher = ExtractIdHasher::default();
t.hash(&mut hasher);
let maybe_source_id = hasher.id();
// Ideally we would just implement IdTriat for Id with specialization, but that's not
// a thing yet :( So we check if the hash is already in the map, if so, the source must be
// an Id.
if let Some(maybe_source_id) = maybe_source_id {
if ID_MAP.read().contains_key(&maybe_source_id) {
IdSource::Id(maybe_source_id)
} else {
IdSource::Other(format!("{:?}", t))
}
} else {
IdSource::Other(format!("{:?}", t))
}
}
pub(super) fn maybe_insert(id: Id, source: impl AsId, parent: Option<Id>) {
if !ID_MAP.read().contains_key(&id) {
let source1 = get_source(source);
ID_MAP.write().insert(
id,
IdInfo {
source: source1,
parent,
},
);
}
}
impl Id {
pub fn info(&self) -> Option<IdInfo> {
ID_MAP.read().get(self).cloned()
}
}
#[test]
fn test_fake_hasher() {
use std::hash::Hash;
let mut hasher = ExtractIdHasher::default();
let id = Id::new("test");
id.hash(&mut hasher);
assert_eq!(hasher.id(), Some(id));
}
} }
impl std::fmt::Debug for Id { impl std::fmt::Debug for Id {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:04X}", self.value() as u16)?; write!(f, "{:04X}", self.value() as u16)?;
let lock = ID_MAP.read();
if let Some((source, parent)) = lock.get(self) { #[cfg(debug_assertions)]
match source { if let Some(info) = self.info() {
IdSource::Id(source_id) => { match info.source {
id_source::IdSource::Id(source_id) => {
write!(f, "({:?})", source_id)?; write!(f, "({:?})", source_id)?;
} }
IdSource::Other(label) => { id_source::IdSource::Other(label) => {
write!(f, " ({})", label)?; write!(f, " ({})", label)?;
} }
} }
if let Some(parent) = parent { if let Some(parent) = info.parent {
// Let's hope there are no cycles! // Let's hope there are no cycles!
write!(f, " <- {:?}", parent)?; write!(f, " <- {:?}", parent)?;
} }

View File

@@ -492,7 +492,7 @@ pub use self::{
drag_and_drop::DragAndDrop, drag_and_drop::DragAndDrop,
epaint::text::TextWrapMode, epaint::text::TextWrapMode,
grid::Grid, grid::Grid,
id::{Id, IdMap, IdTrait}, id::{AsId, Id, IdMap},
input_state::{InputState, MultiTouchInfo, PointerState}, input_state::{InputState, MultiTouchInfo, PointerState},
layers::{LayerId, Order}, layers::{LayerId, Order},
layout::*, layout::*,

View File

@@ -131,7 +131,7 @@ impl ViewportId {
pub const ROOT: Self = Self(Id::NULL); pub const ROOT: Self = Self(Id::NULL);
#[inline] #[inline]
pub fn from_hash_of(source: impl crate::IdTrait) -> Self { pub fn from_hash_of(source: impl crate::AsId) -> Self {
Self(Id::new(source)) Self(Id::new(source))
} }
} }

View File

@@ -166,13 +166,13 @@ impl<'t> TextEdit<'t> {
/// A source for the unique [`Id`], e.g. `.id_source("second_text_edit_field")` or `.id_source(loop_index)`. /// A source for the unique [`Id`], e.g. `.id_source("second_text_edit_field")` or `.id_source(loop_index)`.
#[inline] #[inline]
pub fn id_source(self, id_salt: impl crate::IdTrait) -> Self { pub fn id_source(self, id_salt: impl crate::AsId) -> Self {
self.id_salt(id_salt) self.id_salt(id_salt)
} }
/// A source for the unique [`Id`], e.g. `.id_salt("second_text_edit_field")` or `.id_salt(loop_index)`. /// A source for the unique [`Id`], e.g. `.id_salt("second_text_edit_field")` or `.id_salt(loop_index)`.
#[inline] #[inline]
pub fn id_salt(mut self, id_salt: impl crate::IdTrait) -> Self { pub fn id_salt(mut self, id_salt: impl crate::AsId) -> Self {
self.id_salt = Some(Id::new(id_salt)); self.id_salt = Some(Id::new(id_salt));
self self
} }

View File

@@ -275,7 +275,7 @@ impl<'a> TableBuilder<'a> {
/// This is required if you have multiple tables in the same [`Ui`]. /// This is required if you have multiple tables in the same [`Ui`].
#[inline] #[inline]
#[deprecated = "Renamed id_salt"] #[deprecated = "Renamed id_salt"]
pub fn id_source(self, id_salt: impl egui::IdTrait) -> Self { pub fn id_source(self, id_salt: impl egui::AsId) -> Self {
self.id_salt(id_salt) self.id_salt(id_salt)
} }
@@ -283,7 +283,7 @@ impl<'a> TableBuilder<'a> {
/// ///
/// This is required if you have multiple tables in the same [`Ui`]. /// This is required if you have multiple tables in the same [`Ui`].
#[inline] #[inline]
pub fn id_salt(mut self, id_salt: impl egui::IdTrait) -> Self { pub fn id_salt(mut self, id_salt: impl egui::AsId) -> Self {
self.id_salt = Id::new(id_salt); self.id_salt = Id::new(id_salt);
self self
} }