From 06861078407345b0953343c26c6dfac1df5a0ee4 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Mon, 23 Mar 2026 13:01:51 +0100 Subject: [PATCH] IdSalt (WIP) --- .../egui/src/containers/collapsing_header.rs | 12 +-- crates/egui/src/containers/combo_box.rs | 12 +-- crates/egui/src/containers/resize.rs | 6 +- crates/egui/src/containers/scroll_area.rs | 6 +- crates/egui/src/containers/window.rs | 2 +- crates/egui/src/data/input.rs | 2 +- crates/egui/src/grid.rs | 4 +- crates/egui/src/id.rs | 98 +++++++++++++++++++ crates/egui/src/lib.rs | 2 +- crates/egui/src/ui.rs | 10 +- crates/egui/src/ui_builder.rs | 8 +- crates/egui/src/widgets/text_edit/builder.rs | 6 +- .../egui_demo_lib/src/demo/extra_viewport.rs | 2 +- crates/egui_extras/src/table.rs | 6 +- 14 files changed, 136 insertions(+), 40 deletions(-) diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index aca8ab138..794a8c820 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -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) -> 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) -> 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) -> Self { + self.id_salt = id_salt.into().id(); self } diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index c4097f803..fea639740 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -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) -> Self { + pub fn new(id_salt: impl Into, label: impl Into) -> 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) -> 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) -> 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) -> Self { Self::from_id_salt(id_salt) } diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index 7ff943b3f..4311c05f0 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -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) -> 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) -> Self { + self.id_salt = Some(id_salt.into().id()); self } diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 2616fb414..e297b078a 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -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) -> 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) -> Self { + self.id_salt = Some(id_salt.into().id()); self } diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index c6b739589..f80d8475d 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -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(); diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index a52d40233..f3f7aceec 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -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); }); }); diff --git a/crates/egui/src/grid.rs b/crates/egui/src/grid.rs index 0cf5c95df..63727295e 100644 --- a/crates/egui/src/grid.rs +++ b/crates/egui/src/grid.rs @@ -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) -> 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, diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index 661bdf2bf..c71e0c507 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -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::(), + "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::>(), 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 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 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` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing. diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index d86851a1d..dca23c2bd 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -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::*, diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index daabd10b0..738cf680b 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -226,7 +226,7 @@ impl Ui { &mut self, max_rect: Rect, layout: Layout, - id_salt: impl Hash, + id_salt: impl Into, ui_stack_info: Option, ) -> Self { self.new_child( @@ -2365,7 +2365,7 @@ impl Ui { /// ``` pub fn push_id( &mut self, - id_salt: impl Hash, + id_salt: impl Into, add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse { self.scope_dyn(UiBuilder::new().id_salt(id_salt), Box::new(add_contents)) @@ -2465,15 +2465,15 @@ impl Ui { #[inline] pub fn indent( &mut self, - id_salt: impl Hash, + id_salt: impl Into, add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse { - 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 R + 'c>, ) -> InnerResponse { assert!( diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index 87786a726..9353feb1d 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -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) -> Self { + self.id_salt = Some(id_salt.into().id()); self } diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index b9fdb1cbe..79e24f699 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -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) -> 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) -> Self { + self.id_salt = Some(id_salt.into().id()); self } diff --git a/crates/egui_demo_lib/src/demo/extra_viewport.rs b/crates/egui_demo_lib/src/demo/extra_viewport.rs index 89ea735cf..f02d50634 100644 --- a/crates/egui_demo_lib/src/demo/extra_viewport.rs +++ b/crates/egui_demo_lib/src/demo/extra_viewport.rs @@ -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); }); }); diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index 78c6e7435..0884dd87b 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -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) -> 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) -> Self { + self.id_salt = id_salt.into().id(); self }