From 5304d4c6333aaaa941c117b2eabfe9ecbd9a0ab9 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 26 Sep 2023 13:06:54 +0200 Subject: [PATCH] Add option to show a callstack to the widget under the mouse --- .github/workflows/rust.yml | 3 + Cargo.lock | 1 + crates/egui/Cargo.toml | 7 + crates/egui/src/callstack.rs | 186 ++++++++++++++++ crates/egui/src/containers/resize.rs | 1 + crates/egui/src/context.rs | 12 +- crates/egui/src/data/input.rs | 5 + crates/egui/src/frame_state.rs | 14 ++ crates/egui/src/grid.rs | 39 ++-- crates/egui/src/layout.rs | 1 + crates/egui/src/lib.rs | 30 +-- crates/egui/src/placer.rs | 1 + crates/egui/src/style.rs | 66 +++++- crates/egui/src/ui.rs | 201 ++++++++++++------ crates/egui_demo_app/Cargo.toml | 1 + crates/egui_demo_app/src/backend_panel.rs | 10 +- crates/egui_demo_app/src/wrap_app.rs | 9 +- .../src/demo/demo_app_windows.rs | 1 - 18 files changed, 463 insertions(+), 125 deletions(-) create mode 100644 crates/egui/src/callstack.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 679023315d1..2b952ef6b9c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -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: diff --git a/Cargo.lock b/Cargo.lock index 765f03ef041..767c89324f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1164,6 +1164,7 @@ version = "0.22.0" dependencies = [ "accesskit", "ahash 0.8.3", + "backtrace", "document-features", "epaint", "log", diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index 7ce890f4bab..c8ae84727f8 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -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"] @@ -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 } diff --git a/crates/egui/src/callstack.rs b/crates/egui/src/callstack.rs new file mode 100644 index 00000000000..fac5ac5936e --- /dev/null +++ b/crates/egui/src/callstack.rs @@ -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_plot::", + "egui_extras::", + "core::ptr::drop_in_place::", + "eframe::", + "core::ops::function::FnOnce::call_once", + " as core::ops::function::FnOnce>::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); + } +} diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index 2f71a57c08b..e991e4e1043 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -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), diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index f78101ff2ff..0900a111134 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -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, @@ -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| { @@ -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 @@ -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, @@ -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); } } @@ -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( diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 5b74e81b60d..4ed234c05df 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -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 { diff --git a/crates/egui/src/frame_state.rs b/crates/egui/src/frame_state.rs index f82b2e7e600..49c3a5f1077 100644 --- a/crates/egui/src/frame_state.rs +++ b/crates/egui/src/frame_state.rs @@ -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 { @@ -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, } } } @@ -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(); @@ -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; diff --git a/crates/egui/src/grid.rs b/crates/egui/src/grid.rs index 3f1dc592c8c..3e3e1df99ff 100644 --- a/crates/egui/src/grid.rs +++ b/crates/egui/src/grid.rs @@ -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()); + } } } } diff --git a/crates/egui/src/layout.rs b/crates/egui/src/layout.rs index 8da23700a23..1dd77c21580 100644 --- a/crates/egui/src/layout.rs +++ b/crates/egui/src/layout.rs @@ -801,6 +801,7 @@ impl Layout { /// ## Debug stuff impl Layout { /// Shows where the next widget is going to be placed + #[cfg(debug_assertions)] pub(crate) fn paint_text_at_cursor( &self, painter: &crate::Painter, diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 15020c33327..dc500516d4b 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -353,6 +353,10 @@ pub mod util; pub mod widget_text; pub mod widgets; +#[cfg(feature = "callstack")] +#[cfg(debug_assertions)] +mod callstack; + #[cfg(feature = "accesskit")] pub use accesskit; @@ -486,32 +490,6 @@ macro_rules! github_link_file { // ---------------------------------------------------------------------------- -/// Show debug info on hover when [`Context::set_debug_on_hover`] has been turned on. -/// -/// ``` -/// # egui::__run_test_ui(|ui| { -/// // Turn on tracing of widgets -/// ui.ctx().set_debug_on_hover(true); -/// -/// /// Show [`std::file`], [`std::line`] and argument on hover -/// egui::trace!(ui, "MyWindow"); -/// -/// /// Show [`std::file`] and [`std::line`] on hover -/// egui::trace!(ui); -/// # }); -/// ``` -#[macro_export] -macro_rules! trace { - ($ui: expr) => {{ - $ui.trace_location(format!("{}:{}", file!(), line!())) - }}; - ($ui: expr, $label: expr) => {{ - $ui.trace_location(format!("{} - {}:{}", $label, file!(), line!())) - }}; -} - -// ---------------------------------------------------------------------------- - /// An assert that is only active when `egui` is compiled with the `extra_asserts` feature /// or with the `extra_debug_asserts` feature in debug builds. #[macro_export] diff --git a/crates/egui/src/placer.rs b/crates/egui/src/placer.rs index c9f9ddefbc5..81c137f4e83 100644 --- a/crates/egui/src/placer.rs +++ b/crates/egui/src/placer.rs @@ -263,6 +263,7 @@ impl Placer { } impl Placer { + #[cfg(debug_assertions)] pub(crate) fn debug_paint_cursor(&self, painter: &crate::Painter, text: impl ToString) { let stroke = Stroke::new(1.0, Color32::DEBUG_COLOR); diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index cc31a1c617b..3c44b292a6c 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -201,6 +201,9 @@ pub struct Style { pub animation_time: f32, /// Options to help debug why egui behaves strangely. + /// + /// Only available in debug builds. + #[cfg(debug_assertions)] pub debug: DebugOptions, /// Show tooltips explaining [`DragValue`]:s etc when hovered. @@ -690,12 +693,36 @@ impl WidgetVisuals { } /// Options for help debug egui by adding extra visualization -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg(debug_assertions)] pub struct DebugOptions { - /// However over widgets to see their rectangles + /// Always show callstack to ui on hover. + /// + /// Useful for figuring out where in the code some UI is being created. + /// + /// Only works in debug builds. + /// Requires the `callstack` feature. + /// Does not work on web. + #[cfg(debug_assertions)] pub debug_on_hover: bool, + /// Show callstack for the current widget on hover if all modifier keys are pressed down. + /// + /// Useful for figuring out where in the code some UI is being created. + /// + /// Only works in debug builds. + /// Requires the `callstack` feature. + /// Does not work on web. + /// + /// Default is `true` in debug builds, on native, if the `callstack` feature is enabled. + #[cfg(debug_assertions)] + pub debug_on_hover_with_all_modifiers: bool, + + /// If we show the hover ui, include where the next widget is placed. + #[cfg(debug_assertions)] + pub hover_shows_next: bool, + /// Show which widgets make their parent wider pub show_expand_width: bool, @@ -711,6 +738,23 @@ pub struct DebugOptions { pub show_blocking_widget: bool, } +#[cfg(debug_assertions)] +impl Default for DebugOptions { + fn default() -> Self { + Self { + debug_on_hover: false, + debug_on_hover_with_all_modifiers: cfg!(feature = "callstack") + && !cfg!(target_arch = "wasm32"), + hover_shows_next: false, + show_expand_width: false, + show_expand_height: false, + show_resize: false, + show_interactive_widgets: false, + show_blocking_widget: false, + } + } +} + // ---------------------------------------------------------------------------- /// The default text styles of the default egui theme. @@ -739,6 +783,7 @@ impl Default for Style { interaction: Interaction::default(), visuals: Visuals::default(), animation_time: 1.0 / 12.0, + #[cfg(debug_assertions)] debug: Default::default(), explanation_tooltips: false, } @@ -993,6 +1038,7 @@ impl Style { interaction, visuals, animation_time, + #[cfg(debug_assertions)] debug, explanation_tooltips, } = self; @@ -1055,6 +1101,8 @@ impl Style { ui.collapsing("📏 Spacing", |ui| spacing.ui(ui)); ui.collapsing("☝ Interaction", |ui| interaction.ui(ui)); ui.collapsing("🎨 Visuals", |ui| visuals.ui(ui)); + + #[cfg(debug_assertions)] ui.collapsing("🐛 Debug", |ui| debug.ui(ui)); ui.checkbox(explanation_tooltips, "Explanation tooltips") @@ -1477,10 +1525,13 @@ impl Visuals { } } +#[cfg(debug_assertions)] impl DebugOptions { pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { debug_on_hover, + debug_on_hover_with_all_modifiers, + hover_shows_next, show_expand_width, show_expand_height, show_resize, @@ -1488,7 +1539,16 @@ impl DebugOptions { show_blocking_widget, } = self; - ui.checkbox(debug_on_hover, "Show debug info on hover"); + { + ui.checkbox(debug_on_hover, "Show widget info on hover."); + ui.checkbox( + debug_on_hover_with_all_modifiers, + "Show widget info on hover if holding all modifier keys", + ); + + ui.checkbox(hover_shows_next, "Show next widget placement on hover"); + } + ui.checkbox( show_expand_width, "Show which widgets make their parent wider", diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 01241ccf2fd..b2a828c9abc 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -738,43 +738,41 @@ impl Ui { /// # }); /// ``` pub fn allocate_space(&mut self, desired_size: Vec2) -> (Id, Rect) { - // For debug rendering + #[cfg(debug_assertions)] let original_available = self.available_size_before_wrap(); - let too_wide = desired_size.x > original_available.x; - let too_high = desired_size.y > original_available.y; let rect = self.allocate_space_impl(desired_size); - if self.style().debug.debug_on_hover && self.rect_contains_pointer(rect) { - let painter = self.ctx().debug_painter(); - painter.rect_stroke(rect, 4.0, (1.0, Color32::LIGHT_BLUE)); - self.placer.debug_paint_cursor(&painter, "next"); - } - - let debug_expand_width = self.style().debug.show_expand_width; - let debug_expand_height = self.style().debug.show_expand_height; - - if (debug_expand_width && too_wide) || (debug_expand_height && too_high) { - self.painter - .rect_stroke(rect, 0.0, (1.0, Color32::LIGHT_BLUE)); + #[cfg(debug_assertions)] + { + let too_wide = desired_size.x > original_available.x; + let too_high = desired_size.y > original_available.y; - let stroke = Stroke::new(2.5, Color32::from_rgb(200, 0, 0)); - let paint_line_seg = |a, b| self.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( - pos2(rect.left() + original_available.x, rect.top()), - pos2(rect.left() + original_available.x, rect.bottom()), - ); - paint_line_seg(rect.right_top(), rect.right_bottom()); - } + let debug_expand_width = self.style().debug.show_expand_width; + let debug_expand_height = self.style().debug.show_expand_height; - if debug_expand_height && too_high { - paint_line_seg(rect.left_top(), rect.right_top()); - paint_line_seg(rect.center_top(), rect.center_bottom()); - paint_line_seg(rect.left_bottom(), rect.right_bottom()); + if (debug_expand_width && too_wide) || (debug_expand_height && too_high) { + self.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| self.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( + pos2(rect.left() + original_available.x, rect.top()), + pos2(rect.left() + original_available.x, rect.bottom()), + ); + paint_line_seg(rect.right_top(), rect.right_bottom()); + } + + if debug_expand_height && too_high { + paint_line_seg(rect.left_top(), rect.right_top()); + paint_line_seg(rect.center_top(), rect.center_bottom()); + paint_line_seg(rect.left_bottom(), rect.right_bottom()); + } } } @@ -795,6 +793,8 @@ impl Ui { self.placer .advance_after_rects(frame_rect, widget_rect, item_spacing); + register_rect(self, widget_rect); + widget_rect } @@ -803,6 +803,7 @@ impl Ui { /// Ignore the layout of the [`Ui`]: just put my widget here! /// The layout cursor will advance to past this `rect`. pub fn allocate_rect(&mut self, rect: Rect, sense: Sense) -> Response { + register_rect(self, rect); let id = self.advance_cursor_after_rect(rect); self.interact(rect, id, sense) } @@ -813,12 +814,6 @@ impl Ui { let item_spacing = self.spacing().item_spacing; self.placer.advance_after_rects(rect, rect, item_spacing); - if self.style().debug.debug_on_hover && self.rect_contains_pointer(rect) { - let painter = self.ctx().debug_painter(); - painter.rect_stroke(rect, 4.0, (1.0, Color32::LIGHT_BLUE)); - self.placer.debug_paint_cursor(&painter, "next"); - } - let id = Id::new(self.next_auto_id_source); self.next_auto_id_source = self.next_auto_id_source.wrapping_add(1); id @@ -896,13 +891,6 @@ impl Ui { self.placer .advance_after_rects(final_child_rect, final_child_rect, item_spacing); - if self.style().debug.debug_on_hover && self.rect_contains_pointer(final_child_rect) { - let painter = self.ctx().debug_painter(); - painter.rect_stroke(frame_rect, 4.0, (1.0, Color32::LIGHT_BLUE)); - painter.rect_stroke(final_child_rect, 4.0, (1.0, Color32::LIGHT_BLUE)); - self.placer.debug_paint_cursor(&painter, "next"); - } - let response = self.interact(final_child_rect, child_ui.id, Sense::hover()); InnerResponse::new(ret, response) } @@ -1793,10 +1781,7 @@ impl Ui { let mut child_rect = self.placer.available_rect_before_wrap(); child_rect.min.x += indent; - let mut child_ui = Self { - id: self.id.with(id_source), - ..self.child_ui(child_rect, *self.layout()) - }; + let mut child_ui = self.child_ui_with_id_source(child_rect, *self.layout(), id_source); let ret = add_contents(&mut child_ui); let left_vline = self.visuals().indent_has_left_vline; @@ -2024,12 +2009,6 @@ impl Ui { let item_spacing = self.spacing().item_spacing; self.placer.advance_after_rects(rect, rect, item_spacing); - if self.style().debug.debug_on_hover && self.rect_contains_pointer(rect) { - let painter = self.ctx().debug_painter(); - painter.rect_stroke(rect, 4.0, (1.0, Color32::LIGHT_BLUE)); - self.placer.debug_paint_cursor(&painter, "next"); - } - InnerResponse::new(inner, self.interact(rect, child_ui.id, Sense::hover())) } @@ -2215,21 +2194,121 @@ impl Ui { /// # Debug stuff impl Ui { /// Shows where the next widget is going to be placed + #[cfg(debug_assertions)] pub fn debug_paint_cursor(&self) { self.placer.debug_paint_cursor(&self.painter, "next"); } +} + +#[cfg(debug_assertions)] +impl Drop for Ui { + fn drop(&mut self) { + register_rect(self, self.min_rect()); + } +} + +/// Show this rectangle to the user if certain debug options are set. +#[cfg(debug_assertions)] +fn register_rect(ui: &Ui, rect: Rect) { + let debug = ui.style().debug; + + let show_callstacks = debug.debug_on_hover + || debug.debug_on_hover_with_all_modifiers && ui.input(|i| i.modifiers.all()); + + if !show_callstacks { + return; + } + + if ui.ctx().frame_state(|o| o.has_debug_viewed_this_frame) { + return; + } - /// Shows the given text where the next widget is to be placed - /// if when [`Context::set_debug_on_hover`] has been turned on and the mouse is hovering the Ui. - pub fn trace_location(&self, text: impl ToString) { - let rect = self.max_rect(); - if self.style().debug.debug_on_hover && self.rect_contains_pointer(rect) { - self.placer - .debug_paint_cursor(&self.ctx().debug_painter(), text); + if !ui.rect_contains_pointer(rect) { + return; + } + + // We only show one debug rectangle, or things get confusing: + ui.ctx() + .frame_state_mut(|o| o.has_debug_viewed_this_frame = true); + + // ---------------------------------------------- + + let is_clicking = ui.input(|i| i.pointer.could_any_button_be_click()); + + // Use the debug-painter to avoid clip rect, + // otherwise the content of the widget may cover what we paint here! + let painter = ui.ctx().debug_painter(); + + // Paint rectangle around widget: + { + let rect_fg_color = if is_clicking { + Color32::WHITE + } else { + Color32::LIGHT_BLUE + }; + let rect_bg_color = Color32::BLUE.gamma_multiply(0.5); + + painter.rect(rect, 0.0, rect_bg_color, (1.0, rect_fg_color)); + } + + // ---------------------------------------------- + + if debug.hover_shows_next { + ui.placer.debug_paint_cursor(&painter, "next"); + } + + // ---------------------------------------------- + + #[cfg(feature = "callstack")] + let callstack = crate::callstack::capture(); + + #[cfg(not(feature = "callstack"))] + let callstack = String::default(); + + if !callstack.is_empty() { + let font_id = FontId::monospace(12.0); + let text = format!("{callstack}\n\n(click to copy)"); + let galley = painter.layout_no_wrap(text, font_id, Color32::WHITE); + + // Position the text either under or above: + let screen_rect = ui.ctx().screen_rect(); + let y = if galley.size().y <= rect.top() { + // Above + rect.top() - galley.size().y + } else { + // Below + rect.bottom() + }; + + let y = y + .at_most(screen_rect.bottom() - galley.size().y) + .at_least(0.0); + + let x = rect + .left() + .at_most(screen_rect.right() - galley.size().x) + .at_least(0.0); + let text_pos = pos2(x, y); + + let text_bg_color = Color32::from_black_alpha(180); + let text_rect_stroke_color = if is_clicking { + Color32::WHITE + } else { + text_bg_color + }; + let text_rect = Rect::from_min_size(text_pos, galley.size()); + painter.rect(text_rect, 0.0, text_bg_color, (1.0, text_rect_stroke_color)); + painter.galley(text_pos, galley); + + if ui.input(|i| i.pointer.any_click()) { + ui.ctx().copy_text(callstack); } } } +#[cfg(not(debug_assertions))] +fn register_rect(_ui: &Ui, _rect: Rect) {} + #[test] fn ui_impl_send_sync() { fn assert_send_sync() {} diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 677e746392e..0686f017fab 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -35,6 +35,7 @@ chrono = { version = "0.4", default-features = false, features = [ ] } eframe = { version = "0.22.0", path = "../eframe", default-features = false } egui = { version = "0.22.0", path = "../egui", features = [ + "callstack", "extra_debug_asserts", "log", ] } diff --git a/crates/egui_demo_app/src/backend_panel.rs b/crates/egui_demo_app/src/backend_panel.rs index ebef75f4321..ae7336156f7 100644 --- a/crates/egui_demo_app/src/backend_panel.rs +++ b/crates/egui_demo_app/src/backend_panel.rs @@ -82,8 +82,6 @@ impl BackendPanel { } pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { - egui::trace!(ui); - self.integration_ui(ui, frame); ui.separator(); @@ -101,11 +99,9 @@ impl BackendPanel { ui.separator(); - { - let mut debug_on_hover = ui.ctx().debug_on_hover(); - ui.checkbox(&mut debug_on_hover, "🐛 Debug on hover") - .on_hover_text("Show structure of the ui when you hover with the mouse"); - ui.ctx().set_debug_on_hover(debug_on_hover); + #[cfg(debug_assertions)] + if ui.ctx().style().debug.debug_on_hover_with_all_modifiers { + ui.label("Press down all modifiers and hover a widget to see a callstack for it"); } #[cfg(target_arch = "wasm32")] diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index 619e70ad866..2085b2fd323 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -164,21 +164,21 @@ pub struct WrapApp { } impl WrapApp { - pub fn new(_cc: &eframe::CreationContext<'_>) -> Self { - egui_extras::install_image_loaders(&_cc.egui_ctx); + pub fn new(cc: &eframe::CreationContext<'_>) -> Self { + egui_extras::install_image_loaders(&cc.egui_ctx); #[allow(unused_mut)] let mut slf = Self { state: State::default(), #[cfg(any(feature = "glow", feature = "wgpu"))] - custom3d: crate::apps::Custom3d::new(_cc), + custom3d: crate::apps::Custom3d::new(cc), dropped_files: Default::default(), }; #[cfg(feature = "persistence")] - if let Some(storage) = _cc.storage { + if let Some(storage) = cc.storage { if let Some(state) = eframe::get_value(storage, eframe::APP_KEY) { slf.state = state; } @@ -263,7 +263,6 @@ impl eframe::App for WrapApp { let mut cmd = Command::Nothing; egui::TopBottomPanel::top("wrap_app_top_bar").show(ctx, |ui| { - egui::trace!(ui); ui.horizontal_wrapped(|ui| { ui.visuals_mut().button_frame = false; self.bar_contents(ui, frame, &mut cmd); diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 02c88357c93..07d866242ff 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -243,7 +243,6 @@ impl DemoWindows { .resizable(false) .default_width(150.0) .show(ctx, |ui| { - egui::trace!(ui); ui.vertical_centered(|ui| { ui.heading("✒ egui demos"); });