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 36db392
Show file tree
Hide file tree
Showing 18 changed files with 463 additions and 125 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ jobs:
- name: Cranky
run: cargo cranky --all-targets --all-features -- -D warnings

- name: Cranky release
run: cargo cranky --all-targets --all-features --release -- -D warnings

# ---------------------------------------------------------------------------

check_wasm:
Expand Down
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.

7 changes: 7 additions & 0 deletions crates/egui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ 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.
## Does not work on web.
callstack = ["dep:backtrace"]

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

Expand Down Expand Up @@ -80,6 +85,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
186 changes: 186 additions & 0 deletions crates/egui/src/callstack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#[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::",
"<F as egui::widgets::Widget>",
"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(&format!("{:widest_depth$} …\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);
}
}
1 change: 1 addition & 0 deletions crates/egui/src/containers/resize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ impl Resize {

state.store(ui.ctx(), id);

#[cfg(debug_assertions)]
if ui.ctx().style().debug.show_resize {
ui.ctx().debug_painter().debug_rect(
Rect::from_min_size(content_ui.min_rect().left_top(), state.desired_size),
Expand Down
12 changes: 8 additions & 4 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,7 @@ impl Context {
// This solves the problem of overlapping widgets.
// Whichever widget is added LAST (=on top) gets the input:
if interact_rect.is_positive() && sense.interactive() {
#[cfg(debug_assertions)]
if self.style().debug.show_interactive_widgets {
Self::layer_painter(self, LayerId::debug()).rect(
interact_rect,
Expand All @@ -670,6 +671,8 @@ impl Context {
Stroke::new(1.0, Color32::YELLOW.additive().linear_multiply(0.05)),
);
}

#[cfg(debug_assertions)]
let mut show_blocking_widget = None;

self.write(|ctx| {
Expand All @@ -690,6 +693,7 @@ impl Context {
// Another interactive widget is covering us at the pointer position,
// so we aren't hovered.

#[cfg(debug_assertions)]
if ctx.memory.options.style.debug.show_blocking_widget {
// Store the rects to use them outside the write() call to
// avoid deadlock
Expand All @@ -705,6 +709,7 @@ impl Context {
}
});

#[cfg(debug_assertions)]
if let Some((interact_rect, prev_rect)) = show_blocking_widget {
Self::layer_painter(self, LayerId::debug()).debug_rect(
interact_rect,
Expand Down Expand Up @@ -1528,15 +1533,15 @@ impl Context {
// ---------------------------------------------------------------------

/// Whether or not to debug widget layout on hover.
#[cfg(debug_assertions)]
pub fn debug_on_hover(&self) -> bool {
self.options(|opt| opt.style.debug.debug_on_hover)
}

/// Turn on/off whether or not to debug widget layout on hover.
#[cfg(debug_assertions)]
pub fn set_debug_on_hover(&self, debug_on_hover: bool) {
let mut style = self.options(|opt| (*opt.style).clone());
style.debug.debug_on_hover = debug_on_hover;
self.set_style(style);
self.style_mut(|style| style.debug.debug_on_hover = debug_on_hover);
}
}

Expand Down Expand Up @@ -1619,7 +1624,6 @@ impl Context {
/// Show the state of egui, including its input and output.
pub fn inspection_ui(&self, ui: &mut Ui) {
use crate::containers::*;
crate::trace!(ui);

ui.label(format!("Is using pointer: {}", self.is_using_pointer()))
.on_hover_text(
Expand Down
5 changes: 5 additions & 0 deletions crates/egui/src/data/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,11 @@ impl Modifiers {
!self.is_none()
}

#[inline]
pub fn all(&self) -> bool {
self.alt && self.ctrl && self.shift && self.command
}

/// Is shift the only pressed button?
#[inline]
pub fn shift_only(&self) -> bool {
Expand Down
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
39 changes: 21 additions & 18 deletions crates/egui/src/grid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,24 +187,27 @@ impl GridLayout {
}

pub(crate) fn advance(&mut self, cursor: &mut Rect, _frame_rect: Rect, widget_rect: Rect) {
let debug_expand_width = self.style.debug.show_expand_width;
let debug_expand_height = self.style.debug.show_expand_height;
if debug_expand_width || debug_expand_height {
let rect = widget_rect;
let too_wide = rect.width() > self.prev_col_width(self.col);
let too_high = rect.height() > self.prev_row_height(self.row);

if (debug_expand_width && too_wide) || (debug_expand_height && too_high) {
let painter = self.ctx.debug_painter();
painter.rect_stroke(rect, 0.0, (1.0, Color32::LIGHT_BLUE));

let stroke = Stroke::new(2.5, Color32::from_rgb(200, 0, 0));
let paint_line_seg = |a, b| painter.line_segment([a, b], stroke);

if debug_expand_width && too_wide {
paint_line_seg(rect.left_top(), rect.left_bottom());
paint_line_seg(rect.left_center(), rect.right_center());
paint_line_seg(rect.right_top(), rect.right_bottom());
#[cfg(debug_assertions)]
{
let debug_expand_width = self.style.debug.show_expand_width;
let debug_expand_height = self.style.debug.show_expand_height;
if debug_expand_width || debug_expand_height {
let rect = widget_rect;
let too_wide = rect.width() > self.prev_col_width(self.col);
let too_high = rect.height() > self.prev_row_height(self.row);

if (debug_expand_width && too_wide) || (debug_expand_height && too_high) {
let painter = self.ctx.debug_painter();
painter.rect_stroke(rect, 0.0, (1.0, Color32::LIGHT_BLUE));

let stroke = Stroke::new(2.5, Color32::from_rgb(200, 0, 0));
let paint_line_seg = |a, b| painter.line_segment([a, b], stroke);

if debug_expand_width && too_wide {
paint_line_seg(rect.left_top(), rect.left_bottom());
paint_line_seg(rect.left_center(), rect.right_center());
paint_line_seg(rect.right_top(), rect.right_bottom());
}
}
}
}
Expand Down
Loading

0 comments on commit 36db392

Please sign in to comment.