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

IdSalt (WIP)

This commit is contained in:
lucasmerlin
2026-03-23 13:01:51 +01:00
parent ad510257de
commit 0686107840
14 changed files with 136 additions and 40 deletions

View File

@@ -1,5 +1,3 @@
use std::hash::Hash;
use crate::{
Context, Id, InnerResponse, NumExt as _, Rect, Response, Sense, Stroke, TextStyle,
TextWrapMode, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetInfo, WidgetText, WidgetType,
@@ -410,7 +408,7 @@ impl CollapsingHeader {
/// you need to provide a unique id source with [`Self::id_salt`].
pub fn new(text: impl Into<WidgetText>) -> Self {
let text = text.into();
let id_salt = Id::new(text.text());
let id_salt = Id::new_salt(text.text());
Self {
text,
default_open: false,
@@ -446,8 +444,8 @@ impl CollapsingHeader {
/// Explicitly set the source of the [`Id`] of this widget, instead of using title label.
/// This is useful if the title label is dynamic or not unique.
#[inline]
pub fn id_salt(mut self, id_salt: impl Hash) -> Self {
self.id_salt = Id::new(id_salt);
pub fn id_salt(mut self, id_salt: impl Into<crate::IdSalt>) -> Self {
self.id_salt = id_salt.into().id();
self
}
@@ -455,8 +453,8 @@ impl CollapsingHeader {
/// This is useful if the title label is dynamic or not unique.
#[deprecated = "Renamed id_salt"]
#[inline]
pub fn id_source(mut self, id_salt: impl Hash) -> Self {
self.id_salt = Id::new(id_salt);
pub fn id_source(mut self, id_salt: impl Into<crate::IdSalt>) -> Self {
self.id_salt = id_salt.into().id();
self
}

View File

@@ -49,9 +49,9 @@ pub struct ComboBox {
impl ComboBox {
/// Create new [`ComboBox`] with id and label
pub fn new(id_salt: impl std::hash::Hash, label: impl Into<WidgetText>) -> Self {
pub fn new(id_salt: impl Into<crate::IdSalt>, label: impl Into<WidgetText>) -> Self {
Self {
id_salt: Id::new(id_salt),
id_salt: id_salt.into().id(),
label: Some(label.into()),
selected_text: Default::default(),
width: None,
@@ -67,7 +67,7 @@ impl ComboBox {
pub fn from_label(label: impl Into<WidgetText>) -> Self {
let label = label.into();
Self {
id_salt: Id::new(label.text()),
id_salt: Id::new_salt(label.text()),
label: Some(label),
selected_text: Default::default(),
width: None,
@@ -80,9 +80,9 @@ impl ComboBox {
}
/// Without label.
pub fn from_id_salt(id_salt: impl std::hash::Hash) -> Self {
pub fn from_id_salt(id_salt: impl Into<crate::IdSalt>) -> Self {
Self {
id_salt: Id::new(id_salt),
id_salt: id_salt.into().id(),
label: Default::default(),
selected_text: Default::default(),
width: None,
@@ -96,7 +96,7 @@ impl ComboBox {
/// Without label.
#[deprecated = "Renamed from_id_salt"]
pub fn from_id_source(id_salt: impl std::hash::Hash) -> Self {
pub fn from_id_source(id_salt: impl Into<crate::IdSalt>) -> Self {
Self::from_id_salt(id_salt)
}

View File

@@ -72,14 +72,14 @@ impl Resize {
/// A source for the unique [`Id`], e.g. `.id_source("second_resize_area")` or `.id_source(loop_index)`.
#[inline]
#[deprecated = "Renamed id_salt"]
pub fn id_source(self, id_salt: impl std::hash::Hash) -> Self {
pub fn id_source(self, id_salt: impl Into<crate::IdSalt>) -> Self {
self.id_salt(id_salt)
}
/// A source for the unique [`Id`], e.g. `.id_salt("second_resize_area")` or `.id_salt(loop_index)`.
#[inline]
pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self {
self.id_salt = Some(Id::new(id_salt));
pub fn id_salt(mut self, id_salt: impl Into<crate::IdSalt>) -> Self {
self.id_salt = Some(id_salt.into().id());
self
}

View File

@@ -426,14 +426,14 @@ impl ScrollArea {
/// A source for the unique [`Id`], e.g. `.id_source("second_scroll_area")` or `.id_source(loop_index)`.
#[inline]
#[deprecated = "Renamed id_salt"]
pub fn id_source(self, id_salt: impl std::hash::Hash) -> Self {
pub fn id_source(self, id_salt: impl Into<crate::IdSalt>) -> Self {
self.id_salt(id_salt)
}
/// A source for the unique [`Id`], e.g. `.id_salt("second_scroll_area")` or `.id_salt(loop_index)`.
#[inline]
pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self {
self.id_salt = Some(Id::new(id_salt));
pub fn id_salt(mut self, id_salt: impl Into<crate::IdSalt>) -> Self {
self.id_salt = Some(id_salt.into().id());
self
}

View File

@@ -984,7 +984,7 @@ fn do_resize_interaction(
}
};
let id = Id::new(layer_id).with("edge_drag");
let id = layer_id.id.with("edge_drag");
let style = ctx.global_style();

View File

@@ -1194,7 +1194,7 @@ impl RawInput {
for (id, viewport) in ordered_viewports {
ui.group(|ui| {
ui.label(format!("Viewport {id:?}"));
ui.push_id(id, |ui| {
ui.push_id(id.0, |ui| {
viewport.ui(ui);
});
});

View File

@@ -324,9 +324,9 @@ pub struct Grid {
impl Grid {
/// Create a new [`Grid`] with a locally unique identifier.
pub fn new(id_salt: impl std::hash::Hash) -> Self {
pub fn new(id_salt: impl Into<crate::IdSalt>) -> Self {
Self {
id_salt: Id::new(id_salt),
id_salt: id_salt.into().id(),
num_columns: None,
min_col_width: None,
min_row_height: None,

View File

@@ -54,6 +54,19 @@ impl Id {
/// Generate a new [`Id`] by hashing some source (e.g. a string or integer).
pub fn new(source: impl std::hash::Hash) -> Self {
debug_assert!(
std::any::type_name_of_val(&source) != std::any::type_name::<Self>(),
"Don't pass an `Id` to `Id::new()`: use `.with()` to create child `Id`s"
);
Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(source))
}
/// Like [`Self::new`], but for use as an id salt.
///
/// Unlike [`Self::new`], this does not reject [`Id`] input,
/// because using an [`Id`] as a salt (to be mixed into a parent via [`.with()`](Self::with))
/// is a valid use case.
pub(crate) fn new_salt(source: impl std::hash::Hash) -> Self {
Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(source))
}
@@ -130,6 +143,91 @@ fn id_size() {
assert_eq!(std::mem::size_of::<Option<Id>>(), 8);
}
#[test]
#[should_panic(expected = "Don't pass an `Id` to `Id::new()`")]
fn test_id_new_rejects_id() {
let _ = Id::new(Id::NULL);
}
// ----------------------------------------------------------------------------
/// A value to be used as an [`Id`] salt.
///
/// This is used by builder methods like [`crate::UiBuilder::id_salt`], [`crate::Grid::new`], etc.
/// It can be created from common hashable types (`&str`, `String`, integers)
/// as well as from an existing [`Id`].
///
/// When created from an [`Id`], the value is stored directly without re-hashing.
/// When created from other types, the value is hashed into an [`Id`].
///
/// ## Example
/// ```
/// use egui::{Id, IdSalt};
///
/// // From a string:
/// let salt: IdSalt = "my_widget".into();
///
/// // From an existing Id (no re-hash):
/// let id = Id::new("parent");
/// let salt: IdSalt = id.into();
/// ```
#[derive(Clone, Copy)]
pub struct IdSalt(Id);
impl IdSalt {
/// Create an [`IdSalt`] by hashing some source.
///
/// Use this for types that don't have a `From` impl
/// (e.g. tuples, custom types).
#[inline]
pub fn new(source: impl std::hash::Hash) -> Self {
Self(Id::new_salt(source))
}
/// Get the inner [`Id`].
#[inline]
pub fn id(self) -> Id {
self.0
}
}
impl From<Id> for IdSalt {
/// Store an [`Id`] directly as a salt, without re-hashing.
#[inline]
fn from(id: Id) -> Self {
Self(id)
}
}
impl From<&str> for IdSalt {
#[inline]
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl From<String> for IdSalt {
#[inline]
fn from(s: String) -> Self {
Self::new(s)
}
}
macro_rules! impl_id_salt_from_int {
($($t:ty),*) => {
$(
impl From<$t> for IdSalt {
#[inline]
fn from(v: $t) -> Self {
Self::new(v)
}
}
)*
};
}
impl_id_salt_from_int!(u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, bool);
// ----------------------------------------------------------------------------
/// `IdSet` is a `HashSet<Id>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.

View File

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

View File

@@ -226,7 +226,7 @@ impl Ui {
&mut self,
max_rect: Rect,
layout: Layout,
id_salt: impl Hash,
id_salt: impl Into<IdSalt>,
ui_stack_info: Option<UiStackInfo>,
) -> Self {
self.new_child(
@@ -2365,7 +2365,7 @@ impl Ui {
/// ```
pub fn push_id<R>(
&mut self,
id_salt: impl Hash,
id_salt: impl Into<IdSalt>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.scope_dyn(UiBuilder::new().id_salt(id_salt), Box::new(add_contents))
@@ -2465,15 +2465,15 @@ impl Ui {
#[inline]
pub fn indent<R>(
&mut self,
id_salt: impl Hash,
id_salt: impl Into<IdSalt>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.indent_dyn(id_salt, Box::new(add_contents))
self.indent_dyn(id_salt.into(), Box::new(add_contents))
}
fn indent_dyn<'c, R>(
&mut self,
id_salt: impl Hash,
id_salt: IdSalt,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
assert!(

View File

@@ -1,9 +1,9 @@
use std::{hash::Hash, sync::Arc};
use std::sync::Arc;
use crate::ClosableTag;
#[expect(unused_imports)] // Used for doclinks
use crate::Ui;
use crate::{Id, LayerId, Layout, Rect, Sense, Style, UiStackInfo};
use crate::{Id, IdSalt, LayerId, Layout, Rect, Sense, Style, UiStackInfo};
/// Build a [`Ui`] as the child of another [`Ui`].
///
@@ -39,8 +39,8 @@ impl UiBuilder {
/// You should give each [`Ui`] an `id_salt` that is unique
/// within the parent, or give it none at all.
#[inline]
pub fn id_salt(mut self, id_salt: impl Hash) -> Self {
self.id_salt = Some(Id::new(id_salt));
pub fn id_salt(mut self, id_salt: impl Into<IdSalt>) -> Self {
self.id_salt = Some(id_salt.into().id());
self
}

View File

@@ -168,14 +168,14 @@ impl<'t> TextEdit<'t> {
/// A source for the unique [`Id`], e.g. `.id_source("second_text_edit_field")` or `.id_source(loop_index)`.
#[inline]
pub fn id_source(self, id_salt: impl std::hash::Hash) -> Self {
pub fn id_source(self, id_salt: impl Into<crate::IdSalt>) -> Self {
self.id_salt(id_salt)
}
/// A source for the unique [`Id`], e.g. `.id_salt("second_text_edit_field")` or `.id_salt(loop_index)`.
#[inline]
pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self {
self.id_salt = Some(Id::new(id_salt));
pub fn id_salt(mut self, id_salt: impl Into<crate::IdSalt>) -> Self {
self.id_salt = Some(id_salt.into().id());
self
}

View File

@@ -56,7 +56,7 @@ fn viewport_content(ui: &mut egui::Ui, open: &mut bool) {
for (id, viewport) in ordered_viewports {
ui.group(|ui| {
ui.label(format!("viewport {id:?}"));
ui.push_id(id, |ui| {
ui.push_id(id.0, |ui| {
viewport.ui(ui);
});
});

View File

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