Skip to content

Commit

Permalink
Add option to show a callstack to the widget under the mouse
Browse files Browse the repository at this point in the history
  • Loading branch information
emilk committed Sep 26, 2023
1 parent e8986b1 commit bd674f2
Show file tree
Hide file tree
Showing 8 changed files with 313 additions and 5 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions crates/egui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ default = ["default_fonts"]
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`epaint::Vertex`], [`emath::Vec2`] etc to `&[u8]`.
bytemuck = ["epaint/bytemuck"]

## Show a debug-ui on hover including the stacktrace to the hovered item.
## This is very useful in finding the code that creates a part of the UI.
callstack = ["dep:backtrace"]

## [`cint`](https://docs.rs/cint) enables interoperability with other color libraries.
cint = ["epaint/cint"]

Expand Down Expand Up @@ -80,6 +84,8 @@ nohash-hasher = "0.2"
## accessibility APIs. Also requires support in the egui integration.
accesskit = { version = "0.11", optional = true }

backtrace = { version = "0.3", optional = true }

## Enable this when generating docs.
document-features = { version = "0.2", optional = true }

Expand Down
185 changes: 185 additions & 0 deletions crates/egui/src/callstack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
#[derive(Clone)]
struct Frame {
/// `_main` is usually as the deepest depth.
depth: usize,
name: String,
file_and_line: String,
}

/// Capture a callstack, skipping the frames that are not interesting.
///
/// In particular: slips everything before `egui::Context::run`,
/// and skipping all frames in the `egui::` namespace.
pub fn capture() -> String {
let mut frames = vec![];
let mut depth = 0;

backtrace::trace(|frame| {
// Resolve this instruction pointer to a symbol name
backtrace::resolve_frame(frame, |symbol| {
let mut file_and_line = symbol.filename().map(shorten_source_file_path);

if let Some(file_and_line) = &mut file_and_line {
if let Some(line_nr) = symbol.lineno() {
file_and_line.push_str(&format!(":{line_nr}"));
}
}
let file_and_line = file_and_line.unwrap_or_default();

let name = symbol
.name()
.map(|name| name.to_string())
.unwrap_or_default();

frames.push(Frame {
depth,
name,
file_and_line,
});
});

depth += 1; // note: we can resolve multiple symbols on the same frame.

true // keep going to the next frame
});

if frames.is_empty() {
return Default::default();
}

// Inclusive:
let mut min_depth = 0;
let mut max_depth = frames.len() - 1;

for frame in &frames {
if frame.name.starts_with("egui::callstack::capture") {
min_depth = frame.depth + 1;
}
if frame.name.starts_with("egui::context::Context::run") {
max_depth = frame.depth;
}
}

// Remove frames that are uninteresting:
frames.retain(|frame| {
// Keep some special frames to give the user a sense of chronology:
if frame.name == "main"
|| frame.name == "_main"
|| frame.name.starts_with("egui::context::Context::run")
|| frame.name.starts_with("eframe::run_native")
{
return true;
}

if frame.depth < min_depth || max_depth < frame.depth {
return false;
}

// Remove stuff that isn't user calls:
let skip_prefixes = [
// "backtrace::", // not needed, since we cut at at egui::callstack::capture
"egui::",
"<egui::",
"egui_plot::",
"egui_extras::",
"core::ptr::drop_in_place<egui::ui::Ui>::",
"eframe::",
"core::ops::function::FnOnce::call_once",
"<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once",
];
for prefix in skip_prefixes {
if frame.name.starts_with(prefix) {
return false;
}
}
true
});

frames.reverse(); // main on top, i.e. chronological order. Same as Python.

let mut deepest_depth = 0;
let mut widest_file_line = 0;
for frame in &frames {
deepest_depth = frame.depth.max(deepest_depth);
widest_file_line = frame.file_and_line.len().max(widest_file_line);
}

let widest_depth = deepest_depth.to_string().len();

let mut formatted = String::new();

if !frames.is_empty() {
let mut last_depth = frames[0].depth;

for frame in &frames {
let Frame {
depth,
name,
file_and_line,
} = frame;

if frame.depth + 1 < last_depth || last_depth + 1 < frame.depth {
// Show that some frames were elided
formatted.push_str(" …\n");
}

formatted.push_str(&format!(
"{depth:widest_depth$}: {file_and_line:widest_file_line$} {name}\n"
));

last_depth = frame.depth;
}
}

formatted
}

/// Shorten a path to a Rust source file from a callstack.
///
/// Example input:
/// * `/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs`
/// * `crates/rerun/src/main.rs`
/// * `/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs`
fn shorten_source_file_path(path: &std::path::Path) -> String {
// Look for `src` and strip everything up to it.

let components: Vec<_> = path.iter().map(|path| path.to_string_lossy()).collect();

let mut src_idx = None;
for (i, c) in components.iter().enumerate() {
if c == "src" {
src_idx = Some(i);
}
}

// Look for the last `src`:
if let Some(src_idx) = src_idx {
// Before `src` comes the name of the crate - let's include that:
let first_index = src_idx.saturating_sub(1);

let mut output = components[first_index].to_string();
for component in &components[first_index + 1..] {
output.push('/');
output.push_str(component);
}
output
} else {
// No `src` directory found - weird!
path.display().to_string()
}
}

#[test]
fn test_shorten_path() {
for (before, after) in [
("/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs", "tokio-1.24.1/src/runtime/runtime.rs"),
("crates/rerun/src/main.rs", "rerun/src/main.rs"),
("/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs", "core/src/ops/function.rs"),
("/weird/path/file.rs", "/weird/path/file.rs"),
]
{
use std::str::FromStr as _;
let before = std::path::PathBuf::from_str(before).unwrap();
assert_eq!(shorten_source_file_path(&before), after);
}
}
14 changes: 14 additions & 0 deletions crates/egui/src/frame_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ pub(crate) struct FrameState {

/// Highlight these widgets the next frame. Write to this.
pub(crate) highlight_next_frame: IdSet,

#[cfg(feature = "callstack")]
pub(crate) has_printed_callstack_this_frame: bool,
}

impl Default for FrameState {
Expand All @@ -70,6 +73,9 @@ impl Default for FrameState {
accesskit_state: None,
highlight_this_frame: Default::default(),
highlight_next_frame: Default::default(),

#[cfg(feature = "callstack")]
has_printed_callstack_this_frame: false,
}
}
}
Expand All @@ -89,6 +95,9 @@ impl FrameState {
accesskit_state,
highlight_this_frame,
highlight_next_frame,

#[cfg(feature = "callstack")]
has_printed_callstack_this_frame,
} = self;

used_ids.clear();
Expand All @@ -99,6 +108,11 @@ impl FrameState {
*scroll_delta = input.scroll_delta;
*scroll_target = [None, None];

#[cfg(feature = "callstack")]
{
*has_printed_callstack_this_frame = false;
}

#[cfg(feature = "accesskit")]
{
*accesskit_state = None;
Expand Down
3 changes: 3 additions & 0 deletions crates/egui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,9 @@ pub mod util;
pub mod widget_text;
pub mod widgets;

#[cfg(feature = "callstack")]
mod callstack;

#[cfg(feature = "accesskit")]
pub use accesskit;

Expand Down
19 changes: 18 additions & 1 deletion crates/egui/src/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -693,9 +693,15 @@ impl WidgetVisuals {
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct DebugOptions {
/// However over widgets to see their rectangles
/// Hover over widgets to see their rectangles
pub debug_on_hover: bool,

/// Print callstack to ui on hover.
///
/// Useful for figuring out where in the code some UI is beeing created.
#[cfg(feature = "callstack")]
pub callstack_on_hover: bool,

/// Show which widgets make their parent wider
pub show_expand_width: bool,

Expand Down Expand Up @@ -1481,6 +1487,8 @@ impl DebugOptions {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
debug_on_hover,
#[cfg(feature = "callstack")]
callstack_on_hover,
show_expand_width,
show_expand_height,
show_resize,
Expand All @@ -1489,6 +1497,15 @@ impl DebugOptions {
} = self;

ui.checkbox(debug_on_hover, "Show debug info on hover");

#[cfg(feature = "callstack")]
{
ui.checkbox(
callstack_on_hover,
"Show callstack on hover. Requires the `callstack` feature",
);
}

ui.checkbox(
show_expand_width,
"Show which widgets make their parent wider",
Expand Down
Loading

0 comments on commit bd674f2

Please sign in to comment.