1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 14:49:06 -04:00

Add raw key methods to TypeIdMap (#8007)

This PR fundamentally solves the same problem as
https://github.com/emilk/egui/pull/5827 just implemented with a lot less
ambition and api surface on my end. It contains the bare minimum amount
of changes that I need in order to be able to solve my problem.

My Problem:
I am still suffering from the problem that the TypeIdMap blows up over a
very long time when using my application. (The user generally never
turns off the application, it is intended to be just kept running
forever, some users also never restart their computers) My application
generates a lot of content dynamically so it may for some time display
widgets with a certain set of TypeId's + Id's later hiding them. Some of
the elements that were hidden may turn visible again once an external
event occurs, some may forever be discarded.
I do know myself when which sections of the UI have to be purged because
they will never become visible again, so this PR contains the minimum
amount of necessary functions that allow me to implement this
housekeeping logic on my end.

The existing facilities are insufficient to handle this as the type T
which the TypeId and the hash is derived from are sometimes pub(crate)
privates of widget subcrates or even pub(crate) of egui internals
itself, so its impossible to manually remove those from the TypeIdMap
the only build-in method to remove them is to call "clear" on the
TypeIdMap, however that gets rid of everything, even the elements that
are still shown and should still be in the TypeIdMap.

If the changes in this PR are agreeable to you and you want me to write
unit test for the 4 functions that I have added then tell me and I will
write the tests for you.

If you need anything else changed please tell me.

I ran cargo clippy and cargo fmt, but your check.sh does not work on my
computer.

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
Alexander Schütz
2026-03-26 08:03:39 +01:00
committed by GitHub
parent f1236f1c61
commit 1c9f74b8bd
2 changed files with 126 additions and 32 deletions

View File

@@ -4,7 +4,6 @@
// This will also allow users to pick their own serialization format per type.
use std::{any::Any, sync::Arc};
// -----------------------------------------------------------------------------------------------
/// Like [`std::any::TypeId`], but can be serialized and deserialized.
@@ -182,6 +181,18 @@ impl Element {
}
}
pub fn is_temp(&self) -> bool {
match self {
#[cfg(feature = "persistence")]
Self::Value { serialize_fn, .. } => serialize_fn.is_none(),
#[cfg(not(feature = "persistence"))]
Self::Value { .. } => true,
Self::Serialized(_) => false,
}
}
#[inline]
pub(crate) fn get_temp<T: 'static>(&self) -> Option<&T> {
match self {
@@ -316,6 +327,41 @@ fn from_ron_str<T: serde::de::DeserializeOwned>(ron: &str) -> Option<T> {
use crate::Id;
/// The key used in [`IdTypeMap`], which is a combination of an [`Id`] and a [`TypeId`].
///
/// This key can be used to remove or access values in the [`IdTypeMap`] without
/// knowledge of the `TypeId` `T` that is required for other accessors.
///
/// [`RawKey`]s make no guarantees about layout or their ability to be persisted.
/// They only produce deterministic results if they are used with the map
/// they were initially obtained from. Using them on other instances of [`IdTypeMap`]
/// may produce unexpected behavior.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[repr(transparent)]
pub struct RawKey(u64);
impl nohash_hasher::IsEnabled for RawKey {}
impl RawKey {
/// Create a new key for the given type.
///
/// Note that two keys with the same id but different types
/// will be different keys.
///
/// ```
/// use egui::{Id, util::id_type_map::RawKey};
/// assert_ne!(
/// RawKey::new::<i32>(Id::NULL),
/// RawKey::new::<String>(Id::NULL),
/// );
/// ```
#[inline(always)]
pub fn new<T: 'static>(id: Id) -> Self {
let type_id = TypeId::of::<T>();
Self(type_id.value() ^ id.value())
}
}
// TODO(emilk): make IdTypeMap generic over the key (`Id`), and make a library of IdTypeMap.
/// Stores values identified by an [`Id`] AND the [`std::any::TypeId`] of the value.
///
@@ -358,7 +404,7 @@ use crate::Id;
#[derive(Clone, Debug)]
// We use `id XOR typeid` as a key, so we don't need to hash again!
pub struct IdTypeMap {
map: nohash_hasher::IntMap<u64, Element>,
map: nohash_hasher::IntMap<RawKey, Element>,
max_bytes_per_type: usize,
}
@@ -375,16 +421,23 @@ impl Default for IdTypeMap {
impl IdTypeMap {
/// Insert a value that will not be persisted.
#[inline]
pub fn insert_temp<T: 'static + Any + Clone + Send + Sync>(&mut self, id: Id, value: T) {
let hash = hash(TypeId::of::<T>(), id);
self.map.insert(hash, Element::new_temp(value));
pub fn insert_temp<T: 'static + Any + Clone + Send + Sync>(
&mut self,
id: Id,
value: T,
) -> RawKey {
let key = RawKey::new::<T>(id);
self.map.insert(key, Element::new_temp(value));
key
}
/// Insert a value that will be persisted next time you start the app.
#[inline]
pub fn insert_persisted<T: SerializableAny>(&mut self, id: Id, value: T) {
let hash = hash(TypeId::of::<T>(), id);
self.map.insert(hash, Element::new_persisted(value));
let key = RawKey::new::<T>(id);
self.map.insert(key, Element::new_persisted(value));
// We don't yet return the key here, because currently all our `raw`
// methods are only for temporary values.
}
/// Read a value without trying to deserialize a persisted value.
@@ -392,8 +445,28 @@ impl IdTypeMap {
/// The call clones the value (if found), so make sure it is cheap to clone!
#[inline]
pub fn get_temp<T: 'static + Clone>(&self, id: Id) -> Option<T> {
let hash = hash(TypeId::of::<T>(), id);
self.map.get(&hash).and_then(|x| x.get_temp()).cloned()
let key = RawKey::new::<T>(id);
self.map.get(&key).and_then(|x| x.get_temp()).cloned()
}
/// Gets a reference to a value for a given raw key.
///
/// Serialized values are ignored.
pub fn get_temp_raw(&self, raw: RawKey) -> Option<&(dyn Any + Send + Sync)> {
match self.map.get(&raw)? {
Element::Value { value, .. } => Some(value.as_ref()),
Element::Serialized(_) => None,
}
}
/// Gets a mutable reference to a value for a given raw key.
///
/// Serialized values are ignored.
pub fn get_temp_raw_mut(&mut self, raw: RawKey) -> Option<&mut (dyn Any + Send + Sync)> {
match self.map.get_mut(&raw)? {
Element::Value { value, .. } => Some(value.as_mut()),
Element::Serialized(_) => None,
}
}
/// Read a value, optionally deserializing it if available.
@@ -404,9 +477,9 @@ impl IdTypeMap {
/// The call clones the value (if found), so make sure it is cheap to clone!
#[inline]
pub fn get_persisted<T: SerializableAny>(&mut self, id: Id) -> Option<T> {
let hash = hash(TypeId::of::<T>(), id);
let key = RawKey::new::<T>(id);
self.map
.get_mut(&hash)
.get_mut(&key)
.and_then(|x| x.get_mut_persisted())
.cloned()
}
@@ -443,9 +516,9 @@ impl IdTypeMap {
id: Id,
insert_with: impl FnOnce() -> T,
) -> &mut T {
let hash = hash(TypeId::of::<T>(), id);
let key = RawKey::new::<T>(id);
use std::collections::hash_map::Entry;
match self.map.entry(hash) {
match self.map.entry(key) {
Entry::Vacant(vacant) => {
// this unwrap will never panic, because we insert correct type right now
#[expect(clippy::unwrap_used)]
@@ -465,9 +538,9 @@ impl IdTypeMap {
id: Id,
insert_with: impl FnOnce() -> T,
) -> &mut T {
let hash = hash(TypeId::of::<T>(), id);
let key = RawKey::new::<T>(id);
use std::collections::hash_map::Entry;
match self.map.entry(hash) {
match self.map.entry(key) {
Entry::Vacant(vacant) => {
// this unwrap will never panic, because we insert correct type right now
#[expect(clippy::unwrap_used)]
@@ -486,7 +559,7 @@ impl IdTypeMap {
#[cfg(feature = "persistence")]
#[allow(clippy::allow_attributes, unused)]
fn get_generation<T: SerializableAny>(&self, id: Id) -> Option<usize> {
let element = self.map.get(&hash(TypeId::of::<T>(), id))?;
let element = self.map.get(&RawKey::new::<T>(id))?;
match element {
Element::Value { .. } => Some(0),
Element::Serialized(SerializedElement { generation, .. }) => Some(*generation),
@@ -496,18 +569,33 @@ impl IdTypeMap {
/// Remove the state of this type and id.
#[inline]
pub fn remove<T: 'static>(&mut self, id: Id) {
let hash = hash(TypeId::of::<T>(), id);
self.map.remove(&hash);
let key = RawKey::new::<T>(id);
self.map.remove(&key);
}
/// Remove and fetch the state of this type and id.
#[inline]
pub fn remove_temp<T: 'static + Default>(&mut self, id: Id) -> Option<T> {
let hash = hash(TypeId::of::<T>(), id);
let mut element = self.map.remove(&hash)?;
let key = RawKey::new::<T>(id);
let mut element = self.map.remove(&key)?;
Some(std::mem::take(element.get_mut_temp()?))
}
/// Remove a temporary value given a raw key.
///
/// Serialized values are ignored.
pub fn remove_temp_raw(&mut self, raw: RawKey) -> Option<Box<dyn Any + Send + Sync>> {
use std::collections::hash_map::Entry;
if let Entry::Occupied(e) = self.map.entry(raw)
&& e.get().is_temp()
&& let Element::Value { value, .. } = e.remove()
{
Some(value)
} else {
None
}
}
/// Note all state of the given type.
pub fn remove_by_type<T: 'static>(&mut self) {
let key = TypeId::of::<T>();
@@ -532,6 +620,18 @@ impl IdTypeMap {
self.map.len()
}
/// Returns all [`RawKey`]s to values in this map.
///
/// The returned keys can only be used with this map.
///
/// Serializable values will be ignored.
pub fn temp_keys(&self) -> impl Iterator<Item = RawKey> {
self.map
.iter()
.filter(|(_, v)| v.is_temp())
.map(|(k, _)| *k)
}
/// Count how many values are stored but not yet deserialized.
#[inline]
pub fn count_serialized(&self) -> usize {
@@ -576,11 +676,6 @@ impl IdTypeMap {
}
}
#[inline(always)]
fn hash(type_id: TypeId, id: Id) -> u64 {
type_id.value() ^ id.value()
}
// ----------------------------------------------------------------------------
/// How [`IdTypeMap`] is persisted.
@@ -613,13 +708,13 @@ impl PersistedMap {
{
profiling::scope!("gather");
for (hash, element) in &map.map {
for (key, element) in &map.map {
if let Some(element) = element.to_serialize() {
let stats = types_map.entry(element.type_id).or_default();
stats.num_bytes += element.ron.len();
let generation_stats = stats.generations.entry(element.generation).or_default();
generation_stats.num_bytes += element.ron.len();
generation_stats.elements.push((*hash, element));
generation_stats.elements.push((key.0, element));
} else {
// temporary value that shouldn't be serialized
}
@@ -659,7 +754,7 @@ impl PersistedMap {
.into_iter()
.map(
|(
hash,
raw,
SerializedElement {
type_id,
ron,
@@ -667,7 +762,7 @@ impl PersistedMap {
},
)| {
(
hash,
RawKey(raw),
Element::Serialized(SerializedElement {
type_id,
ron,

View File

@@ -656,14 +656,13 @@ impl TableState {
}
fn store(self, ui: &egui::Ui, state_id: egui::Id) {
#![expect(clippy::needless_return)]
#[cfg(feature = "serde")]
{
return ui.data_mut(|d| d.insert_persisted(state_id, self));
ui.data_mut(|d| d.insert_persisted(state_id, self));
}
#[cfg(not(feature = "serde"))]
{
return ui.data_mut(|d| d.insert_temp(state_id, self));
ui.data_mut(|d| d.insert_temp(state_id, self));
}
}