Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add debug-option to show a callstack to the widget under the mouse #3391

Merged
merged 1 commit into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(debug_assertions)]
pub(crate) has_debug_viewed_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(debug_assertions)]
has_debug_viewed_this_frame: false,
}
}
}
Expand All @@ -89,6 +95,9 @@ impl FrameState {
accesskit_state,
highlight_this_frame,
highlight_next_frame,

#[cfg(debug_assertions)]
has_debug_viewed_this_frame,
} = self;

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

#[cfg(debug_assertions)]
{
*has_debug_viewed_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
Loading