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

WIP: Add code formatting of id debug strings

This commit is contained in:
lucasmerlin
2026-03-20 11:48:42 +01:00
parent 08bf4c2d53
commit c4fb03ed7c
2 changed files with 202 additions and 1 deletions

View File

@@ -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::<bool>(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::<Vec<_>>()
.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:<max_code_len$} {comment}")
} else {
line.to_owned()
}
})
.collect::<Vec<_>>()
.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<String> = 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");

View File

@@ -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));