diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index 14b41116c..1d848a539 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -1,5 +1,6 @@ // TODO(emilk): have separate types `PositionId` and `UniqueId`. ? +use epaint::text::TextWrapMode; use epaint::Color32; use std::num::NonZeroU64; @@ -145,7 +146,21 @@ impl Id { #[cfg(debug_assertions)] let response = response.on_hover_ui(|ui| { - Self::tree_ui(ui, self, "", 0); + let checkbox_id = Id::new("egui::id::show_as_code_checkbox"); + let mut show_as_code = ui + .ctx() + .data_mut(|d| *d.get_persisted_mut_or_default::(checkbox_id)); + ui.checkbox(&mut show_as_code, "Show as code"); + ui.ctx() + .data_mut(|d| d.insert_temp(checkbox_id, show_as_code)); + + if show_as_code { + ui.style_mut().wrap_mode = Some(TextWrapMode::Extend); + ui.style_mut().interaction.selectable_labels = true; + ui.code(self.to_code_string()); + } else { + Self::tree_ui(ui, self, "", 0); + } }); if response.hovered() { @@ -274,6 +289,53 @@ mod id_source { } } + /// Format a call like `Id::new(arg)` or `.with(arg)`. + /// + /// `outer_indent` is the indentation of the call itself (e.g. `" "` if inside a chain). + /// + /// If the arg is single-line, keeps it inline: `Id::new("foo")` + /// If the arg is multi-line, uses rustfmt style: + /// ```text + /// Id::new( + /// arg, + /// ) + /// ``` + fn format_call(func: &str, arg: &str, outer_indent: &str, inner_indent: &str) -> String { + if arg.contains('\n') { + let indented_arg = arg + .lines() + .map(|l| format!("{outer_indent}{inner_indent}{l}")) + .collect::>() + .join("\n"); + format!("{outer_indent}{func}(\n{indented_arg}\n{outer_indent})") + } else { + format!("{outer_indent}{func}({arg})") + } + } + + /// Align all `// XXXX` comments in a string to the same column. + fn align_comments(s: &str) -> String { + let comment_marker = " // "; + let max_code_len = s + .lines() + .filter_map(|line| line.find(comment_marker).map(|pos| pos)) + .max() + .unwrap_or(0); + + s.lines() + .map(|line| { + if let Some(pos) = line.find(comment_marker) { + let code = &line[..pos]; + let comment = &line[pos + 1..]; // include the space before // + format!("{code:>() + .join("\n") + } + impl Id { /// Get info about this id (what source was it generated from, what parent does it have)? /// @@ -282,6 +344,99 @@ mod id_source { ID_MAP.read().get(self).cloned() } + /// Returns a Rust code representation of how this Id was constructed. + /// + /// Formats like rustfmt would: + /// ```text + /// Id::new("parent") + /// .with("child") + /// ``` + pub fn to_code_string(&self) -> String { + let calls = Self::collect_calls(*self); + let result = Self::format_chain(&calls, true); + align_comments(&result) + } + + /// Code string without id comments, used for nested args. + fn to_code_string_inner(&self) -> String { + let calls = Self::collect_calls(*self); + Self::format_chain(&calls, false) + } + + fn collect_calls(id: Id) -> Vec<(&'static str, String, Id)> { + let mut calls: Vec<(&str, String, Id)> = Vec::new(); + let mut current = id; + + loop { + let Some(info) = current.info() else { + calls.push(("", format!("Id({})", current.short_debug_format()), current)); + break; + }; + + let source_str = match &info.source { + IdSource::Id(id) => { + // Use commented version for multi-line args (each line + // except the first gets a comment), plain for single-line + // to avoid embedding comments inline. + let plain = id.to_code_string_inner(); + if plain.contains('\n') { + id.to_code_string() + } else { + plain + } + } + IdSource::Other(s) => s.clone(), + }; + + match info.parent { + None => { + calls.push(("Id::new", source_str, current)); + break; + } + Some(parent) => { + calls.push((".with", source_str, current)); + current = parent; + } + } + } + + calls.reverse(); + calls + } + + fn format_chain(calls: &[(&str, String, Id)], with_comments: bool) -> String { + const INDENT: &str = " "; + + let mut parts: Vec = Vec::new(); + for (i, (func, arg, id)) in calls.iter().enumerate() { + let base = if func.is_empty() { + arg.clone() + } else { + let outer = if i == 0 { "" } else { INDENT }; + format_call(func, arg, outer, INDENT) + }; + + if with_comments { + let comment = format!(" // {}", id.short_debug_format()); + let mut lines: Vec<&str> = base.lines().collect(); + let last = lines.len() - 1; + let last_with_comment = format!("{}{comment}", lines[last]); + lines[last] = &last_with_comment; + parts.push(lines.join("\n")); + } else { + parts.push(base); + } + } + + if parts.len() <= 1 { + parts.into_iter().next().unwrap_or_default() + } else { + let base = &parts[0]; + let withs: Vec<&str> = parts[1..].iter().map(|s| s.as_str()).collect(); + format!("{base}\n{}", withs.join("\n")) + } + } + pub(super) fn tree_ui(ui: &mut crate::Ui, id: Self, prefix: &str, depth: usize) { let info = id.info(); if let Some(info) = info { @@ -325,6 +480,43 @@ mod id_source { assert_eq!(hasher.id(), Some(id)); } + #[test] + fn test_to_code_string() { + let parent = Id::new("parent"); + let child = parent.with("child"); + let grandchild = child.with("grandchild"); + let nested = Id::new(grandchild).with(grandchild); + + assert_eq!( + parent.to_code_string(), + r#"Id::new("parent") // 9DE0"# + ); + assert_eq!( + child.to_code_string(), + r#"Id::new("parent") // 9DE0 + .with("child") // F27D"# + ); + assert_eq!( + grandchild.to_code_string(), + r#"Id::new("parent") // 9DE0 + .with("child") // F27D + .with("grandchild") // 61DA"# + ); + assert_eq!( + nested.to_code_string(), + r#"Id::new( + Id::new("parent") // 9DE0 + .with("child") // F27D + .with("grandchild") // 61DA +) // 02A4 + .with( + Id::new("parent") // 9DE0 + .with("child") // F27D + .with("grandchild") // 61DA + ) // B2D6"# + ); + } + #[test] fn test_debug_format() { let parent = Id::new("parent"); diff --git a/crates/egui_demo_lib/src/demo/tests/id_test.rs b/crates/egui_demo_lib/src/demo/tests/id_test.rs index 73a29d744..065115893 100644 --- a/crates/egui_demo_lib/src/demo/tests/id_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/id_test.rs @@ -1,3 +1,5 @@ +use egui::Id; + #[derive(Default)] pub struct IdTest {} @@ -50,8 +52,15 @@ impl crate::View for IdTest { let button_response = ui.button("Button"); let _ = ui.button("Button"); + + let parent = Id::new("parent"); + let child = parent.with("child"); + let grandchild = child.with("grandchild"); + let nested = Id::new(grandchild).with(grandchild); + ui.label("Use id.ui() to show a interactive debug ui that explains how a id was derived:"); button_response.id.ui(ui); + nested.ui(ui); ui.label("Debug formatting the id will also show the hierarchy (useful when logging ids):"); ui.code(format!("{:?}", button_response.id));