Add `Plugin::on_widget_under_pointer` to support widget inspector (#7652)

This PR adds `Plugin::on_widget_under_pointer` which gets called
whenever a widget is created whose rect contains the pointer.

The point of the hook is to capture a stack trace which can be used to
map widgets to their corresponding source code so it must be called
while the widget is being created. The obvious concern is performance
impact. However, since it's only called for rects under the cursor, the
effect seems negligible afaict. It's under `debug_assertions` just in
case.

This change is needed so we can publish the widget inspector we've been
working on. Basically a plugin that allows us to jump from any widget
back to their corresponding source code.

This video shows the plugin configured to open the corresponding code in
github, but normally it would open your local editor.

Update: [Live demo](https://membrane-io.github.io/egui/) (Firefox/Safari
not yet supported. `Cmd-I` to inspect. `Tab` to cycle filters. `Click`
to open). It will try to open a file under
`/home/runner/work/egui/egui/` so it won't work, but you get the idea.


https://github.com/user-attachments/assets/afe4d6af-7f67-44b5-be25-44f7564d9a3a

## What's next

After this gets merged I plan to publish the above plugin as its own
crate, that way we can iterate and release quickly while things are
still changing. I agree it would make sense to eventually merge it into
the main egui repo (like @emilk suggested in #4650).

* [x] I have followed the instructions in the PR template

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
Juan Campa 2025-10-27 09:52:42 -04:00 committed by Emil Ernerfeldt
parent 4833dd8720
commit e0561e1820
2 changed files with 23 additions and 2 deletions

View File

@ -1198,6 +1198,12 @@ impl Context {
#[allow(clippy::let_and_return, clippy::allow_attributes)]
let res = self.get_response(w);
#[cfg(debug_assertions)]
if res.contains_pointer() {
let plugins = self.read(|ctx| ctx.plugins.ordered_plugins());
plugins.on_widget_under_pointer(self, &w);
}
#[cfg(feature = "accesskit")]
if allow_focus && w.sense.is_focusable() {
// Make sure anything that can receive focus has an AccessKit node.

View File

@ -34,14 +34,21 @@ pub trait Plugin: Send + Sync + std::any::Any + 'static {
/// Called just before the input is processed.
///
/// Useful to inspect or modify the input.
/// Since this is called outside a pass, don't show ui here.
/// Since this is called outside a pass, don't show ui here. Using `Context::debug_painter` is fine though.
fn input_hook(&mut self, input: &mut RawInput) {}
/// Called just before the output is passed to the backend.
///
/// Useful to inspect or modify the output.
/// Since this is called outside a pass, don't show ui here.
/// Since this is called outside a pass, don't show ui here. Using `Context::debug_painter` is fine though.
fn output_hook(&mut self, output: &mut FullOutput) {}
/// Called when a widget is created and is under the pointer.
///
/// Useful for capturing a stack trace so that widgets can be mapped back to their source code.
/// Since this is called outside a pass, don't show ui here. Using `Context::debug_painter` is fine though.
#[cfg(debug_assertions)]
fn on_widget_under_pointer(&mut self, ctx: &Context, widget: &crate::WidgetRect) {}
}
pub(crate) struct PluginHandle {
@ -167,6 +174,14 @@ impl PluginsOrdered {
plugin.output_hook(output);
});
}
#[cfg(debug_assertions)]
pub fn on_widget_under_pointer(&self, ctx: &Context, widget: &crate::WidgetRect) {
profiling::scope!("plugins", "on_widget_under_pointer");
self.for_each_dyn(|plugin| {
plugin.on_widget_under_pointer(ctx, widget);
});
}
}
impl Plugins {