From 68e45301cbf3b99c76c3018bedb58f45a37e8236 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 16 Jul 2025 16:41:39 +0200 Subject: [PATCH 01/22] New Plugin trait --- crates/egui/src/context.rs | 137 +++++++++++++++++++++++++++---------- crates/egui/src/id.rs | 4 ++ crates/egui/src/lib.rs | 2 +- 3 files changed, 106 insertions(+), 37 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 9594f03e93f..45e68f4267a 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2,18 +2,6 @@ use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration}; -use emath::{GuiRounding as _, OrderedFloat}; -use epaint::{ - ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect, StrokeKind, - TessellationOptions, TextureAtlas, TextureId, Vec2, - emath::{self, TSTransform}, - mutex::RwLock, - stats::PaintStats, - tessellator, - text::{FontInsert, FontPriority, Fonts}, - vec2, -}; - use crate::{ Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport, ImmediateViewportRendererCallback, Key, KeyboardShortcut, Label, LayerId, Memory, @@ -37,6 +25,17 @@ use crate::{ util::IdTypeMap, viewport::ViewportClass, }; +use emath::{GuiRounding as _, OrderedFloat}; +use epaint::{ + ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect, StrokeKind, + TessellationOptions, TextureAtlas, TextureId, Vec2, + emath::{self, TSTransform}, + mutex::RwLock, + stats::PaintStats, + tessellator, + text::{FontInsert, FontPriority, Fonts}, + vec2, +}; #[cfg(feature = "accesskit")] use crate::IdMap; @@ -102,32 +101,54 @@ struct NamedContextCallback { callback: ContextCallback, } +#[allow(unused_variables)] +pub trait Plugin: Send + Sync { + fn name(&self) -> &'static str; + fn on_begin_pass(&mut self, ctx: &Context) {} + fn on_end_pass(&mut self, ctx: &Context) {} + fn output_hook(&mut self, output: &mut FullOutput) {} + fn input_hook(&mut self, input: &mut RawInput) {} +} + /// Callbacks that users can register #[derive(Clone, Default)] struct Plugins { - pub on_begin_pass: Vec, - pub on_end_pass: Vec, + // pub on_begin_pass: Vec, + // pub on_end_pass: Vec, + pub plugins: Arc>>>, } impl Plugins { - fn call(ctx: &Context, _cb_name: &str, callbacks: &[NamedContextCallback]) { - profiling::scope!("plugins", _cb_name); - for NamedContextCallback { - debug_name: _name, - callback, - } in callbacks - { - profiling::scope!("plugin", _name); - (callback)(ctx); - } - } - fn on_begin_pass(&self, ctx: &Context) { - Self::call(ctx, "on_begin_pass", &self.on_begin_pass); + profiling::scope!("plugins", "on_begin_pass"); + self.plugins.lock().iter_mut().for_each(|plugin| { + profiling::scope!("plugin", plugin.name()); + plugin.on_begin_pass(ctx); + }); } fn on_end_pass(&self, ctx: &Context) { - Self::call(ctx, "on_end_pass", &self.on_end_pass); + profiling::scope!("plugins", "on_end_pass"); + self.plugins.lock().iter_mut().for_each(|plugin| { + profiling::scope!("plugin", plugin.name()); + plugin.on_end_pass(ctx); + }); + } + + fn on_input(&self, input: &mut RawInput) { + profiling::scope!("plugins", "on_input"); + self.plugins.lock().iter_mut().for_each(|plugin| { + profiling::scope!("plugin", plugin.name()); + plugin.input_hook(input); + }); + } + + fn on_output(&self, output: &mut FullOutput) { + profiling::scope!("plugins", "on_output"); + self.plugins.lock().iter_mut().for_each(|plugin| { + profiling::scope!("plugin", plugin.name()); + plugin.output_hook(output); + }); } } @@ -907,13 +928,16 @@ impl Context { /// let full_output = ctx.end_pass(); /// // handle full_output /// ``` - pub fn begin_pass(&self, new_input: RawInput) { + pub fn begin_pass(&self, mut new_input: RawInput) { profiling::function_scope!(); + let plugins = self.read(|ctx| ctx.plugins.clone()); + plugins.on_input(&mut new_input); + self.write(|ctx| ctx.begin_pass(new_input)); // Plugins run just after the pass starts: - self.read(|ctx| ctx.plugins.clone()).on_begin_pass(self); + plugins.on_begin_pass(self); } /// See [`Self::begin_pass`]. @@ -1875,11 +1899,26 @@ impl Context { /// This can be used for egui _plugins_. /// See [`crate::debug_text`] for an example. pub fn on_begin_pass(&self, debug_name: &'static str, cb: ContextCallback) { - let named_cb = NamedContextCallback { + struct OnBeginPass { + debug_name: &'static str, + callback: ContextCallback, + } + + let on_begin_pass = OnBeginPass { debug_name, callback: cb, }; - self.write(|ctx| ctx.plugins.on_begin_pass.push(named_cb)); + + impl Plugin for OnBeginPass { + fn name(&self) -> &'static str { + self.debug_name + } + fn on_begin_pass(&mut self, ctx: &Context) { + (self.callback)(ctx); + } + } + + self.add_plugin(on_begin_pass); } /// Call the given callback at the end of each pass of each viewport. @@ -1887,11 +1926,34 @@ impl Context { /// This can be used for egui _plugins_. /// See [`crate::debug_text`] for an example. pub fn on_end_pass(&self, debug_name: &'static str, cb: ContextCallback) { - let named_cb = NamedContextCallback { + struct OnEndPass { + debug_name: &'static str, + callback: ContextCallback, + } + + let on_end_pass = OnEndPass { debug_name, callback: cb, }; - self.write(|ctx| ctx.plugins.on_end_pass.push(named_cb)); + + impl Plugin for OnEndPass { + fn name(&self) -> &'static str { + self.debug_name + } + fn on_end_pass(&mut self, ctx: &Context) { + (self.callback)(ctx); + } + } + + self.add_plugin(on_end_pass); + } + + pub fn add_plugin(&self, plugin: impl Plugin + 'static) { + profiling::function_scope!(); + + self.write(|ctx| { + ctx.plugins.plugins.lock().push(Box::new(plugin)); + }); } } @@ -2257,12 +2319,15 @@ impl Context { } // Plugins run just before the pass ends. - self.read(|ctx| ctx.plugins.clone()).on_end_pass(self); + let plugins = self.read(|ctx| ctx.plugins.clone()); + plugins.on_end_pass(self); #[cfg(debug_assertions)] self.debug_painting(); - self.write(|ctx| ctx.end_pass()) + let mut output = self.write(|ctx| ctx.end_pass()); + plugins.on_output(&mut output); + output } /// Call at the end of each frame if you called [`Context::begin_pass`]. diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index 7bcef8dc220..c55f67c5d3e 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -79,6 +79,10 @@ impl Id { self.0.get() } + pub fn from_value(value: u64) -> Self { + Self::from_hash(value) + } + #[cfg(feature = "accesskit")] pub(crate) fn accesskit_id(&self) -> accesskit::NodeId { self.value().into() diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index c2908df1984..d2c2eeac32c 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -482,7 +482,7 @@ pub mod text { pub use self::{ atomics::*, containers::{menu::MenuBar, *}, - context::{Context, RepaintCause, RequestRepaintInfo}, + context::{Context, Plugin, RepaintCause, RequestRepaintInfo}, data::{ Key, UserData, input::*, From 07bd0773abb48196424e382c9e68b19012d896a5 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 16 Jul 2025 16:41:39 +0200 Subject: [PATCH 02/22] New Plugin trait --- crates/egui/src/context.rs | 137 +++++++++++++++++++++++++++---------- crates/egui/src/id.rs | 4 ++ crates/egui/src/lib.rs | 2 +- 3 files changed, 106 insertions(+), 37 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 9594f03e93f..45e68f4267a 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2,18 +2,6 @@ use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration}; -use emath::{GuiRounding as _, OrderedFloat}; -use epaint::{ - ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect, StrokeKind, - TessellationOptions, TextureAtlas, TextureId, Vec2, - emath::{self, TSTransform}, - mutex::RwLock, - stats::PaintStats, - tessellator, - text::{FontInsert, FontPriority, Fonts}, - vec2, -}; - use crate::{ Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport, ImmediateViewportRendererCallback, Key, KeyboardShortcut, Label, LayerId, Memory, @@ -37,6 +25,17 @@ use crate::{ util::IdTypeMap, viewport::ViewportClass, }; +use emath::{GuiRounding as _, OrderedFloat}; +use epaint::{ + ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect, StrokeKind, + TessellationOptions, TextureAtlas, TextureId, Vec2, + emath::{self, TSTransform}, + mutex::RwLock, + stats::PaintStats, + tessellator, + text::{FontInsert, FontPriority, Fonts}, + vec2, +}; #[cfg(feature = "accesskit")] use crate::IdMap; @@ -102,32 +101,54 @@ struct NamedContextCallback { callback: ContextCallback, } +#[allow(unused_variables)] +pub trait Plugin: Send + Sync { + fn name(&self) -> &'static str; + fn on_begin_pass(&mut self, ctx: &Context) {} + fn on_end_pass(&mut self, ctx: &Context) {} + fn output_hook(&mut self, output: &mut FullOutput) {} + fn input_hook(&mut self, input: &mut RawInput) {} +} + /// Callbacks that users can register #[derive(Clone, Default)] struct Plugins { - pub on_begin_pass: Vec, - pub on_end_pass: Vec, + // pub on_begin_pass: Vec, + // pub on_end_pass: Vec, + pub plugins: Arc>>>, } impl Plugins { - fn call(ctx: &Context, _cb_name: &str, callbacks: &[NamedContextCallback]) { - profiling::scope!("plugins", _cb_name); - for NamedContextCallback { - debug_name: _name, - callback, - } in callbacks - { - profiling::scope!("plugin", _name); - (callback)(ctx); - } - } - fn on_begin_pass(&self, ctx: &Context) { - Self::call(ctx, "on_begin_pass", &self.on_begin_pass); + profiling::scope!("plugins", "on_begin_pass"); + self.plugins.lock().iter_mut().for_each(|plugin| { + profiling::scope!("plugin", plugin.name()); + plugin.on_begin_pass(ctx); + }); } fn on_end_pass(&self, ctx: &Context) { - Self::call(ctx, "on_end_pass", &self.on_end_pass); + profiling::scope!("plugins", "on_end_pass"); + self.plugins.lock().iter_mut().for_each(|plugin| { + profiling::scope!("plugin", plugin.name()); + plugin.on_end_pass(ctx); + }); + } + + fn on_input(&self, input: &mut RawInput) { + profiling::scope!("plugins", "on_input"); + self.plugins.lock().iter_mut().for_each(|plugin| { + profiling::scope!("plugin", plugin.name()); + plugin.input_hook(input); + }); + } + + fn on_output(&self, output: &mut FullOutput) { + profiling::scope!("plugins", "on_output"); + self.plugins.lock().iter_mut().for_each(|plugin| { + profiling::scope!("plugin", plugin.name()); + plugin.output_hook(output); + }); } } @@ -907,13 +928,16 @@ impl Context { /// let full_output = ctx.end_pass(); /// // handle full_output /// ``` - pub fn begin_pass(&self, new_input: RawInput) { + pub fn begin_pass(&self, mut new_input: RawInput) { profiling::function_scope!(); + let plugins = self.read(|ctx| ctx.plugins.clone()); + plugins.on_input(&mut new_input); + self.write(|ctx| ctx.begin_pass(new_input)); // Plugins run just after the pass starts: - self.read(|ctx| ctx.plugins.clone()).on_begin_pass(self); + plugins.on_begin_pass(self); } /// See [`Self::begin_pass`]. @@ -1875,11 +1899,26 @@ impl Context { /// This can be used for egui _plugins_. /// See [`crate::debug_text`] for an example. pub fn on_begin_pass(&self, debug_name: &'static str, cb: ContextCallback) { - let named_cb = NamedContextCallback { + struct OnBeginPass { + debug_name: &'static str, + callback: ContextCallback, + } + + let on_begin_pass = OnBeginPass { debug_name, callback: cb, }; - self.write(|ctx| ctx.plugins.on_begin_pass.push(named_cb)); + + impl Plugin for OnBeginPass { + fn name(&self) -> &'static str { + self.debug_name + } + fn on_begin_pass(&mut self, ctx: &Context) { + (self.callback)(ctx); + } + } + + self.add_plugin(on_begin_pass); } /// Call the given callback at the end of each pass of each viewport. @@ -1887,11 +1926,34 @@ impl Context { /// This can be used for egui _plugins_. /// See [`crate::debug_text`] for an example. pub fn on_end_pass(&self, debug_name: &'static str, cb: ContextCallback) { - let named_cb = NamedContextCallback { + struct OnEndPass { + debug_name: &'static str, + callback: ContextCallback, + } + + let on_end_pass = OnEndPass { debug_name, callback: cb, }; - self.write(|ctx| ctx.plugins.on_end_pass.push(named_cb)); + + impl Plugin for OnEndPass { + fn name(&self) -> &'static str { + self.debug_name + } + fn on_end_pass(&mut self, ctx: &Context) { + (self.callback)(ctx); + } + } + + self.add_plugin(on_end_pass); + } + + pub fn add_plugin(&self, plugin: impl Plugin + 'static) { + profiling::function_scope!(); + + self.write(|ctx| { + ctx.plugins.plugins.lock().push(Box::new(plugin)); + }); } } @@ -2257,12 +2319,15 @@ impl Context { } // Plugins run just before the pass ends. - self.read(|ctx| ctx.plugins.clone()).on_end_pass(self); + let plugins = self.read(|ctx| ctx.plugins.clone()); + plugins.on_end_pass(self); #[cfg(debug_assertions)] self.debug_painting(); - self.write(|ctx| ctx.end_pass()) + let mut output = self.write(|ctx| ctx.end_pass()); + plugins.on_output(&mut output); + output } /// Call at the end of each frame if you called [`Context::begin_pass`]. diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index 7bcef8dc220..c55f67c5d3e 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -79,6 +79,10 @@ impl Id { self.0.get() } + pub fn from_value(value: u64) -> Self { + Self::from_hash(value) + } + #[cfg(feature = "accesskit")] pub(crate) fn accesskit_id(&self) -> accesskit::NodeId { self.value().into() diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index c2908df1984..d2c2eeac32c 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -482,7 +482,7 @@ pub mod text { pub use self::{ atomics::*, containers::{menu::MenuBar, *}, - context::{Context, RepaintCause, RequestRepaintInfo}, + context::{Context, Plugin, RepaintCause, RequestRepaintInfo}, data::{ Key, UserData, input::*, From 9441580262752d8cf5cd3969a501eec7a7b8dde2 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Jul 2025 11:08:19 +0200 Subject: [PATCH 03/22] Add setup fn, prevent adding popup multiple times and add documentation --- crates/egui/src/context.rs | 69 +++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 45e68f4267a..bae28dddddc 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1,5 +1,6 @@ #![warn(missing_docs)] // Let's keep `Context` well-documented. +use std::any::Any as _; use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration}; use crate::{ @@ -95,27 +96,48 @@ impl Default for WrappedTextureManager { /// Generic event callback. pub type ContextCallback = Arc; -#[derive(Clone)] -struct NamedContextCallback { - debug_name: &'static str, - callback: ContextCallback, -} - -#[allow(unused_variables)] +/// A plugin to extend egui. +/// +/// Add plugins via [`Context::add_plugin`]. +#[expect(unused_variables)] pub trait Plugin: Send + Sync { + /// Plugin name. + /// + /// Used when profiling. fn name(&self) -> &'static str; + + /// Called once, when the plugin is registered. + /// + /// Useful to e.g. register image loaders. + fn setup(&mut self, ctx: &Context) {} + + /// Called at the start of each pass. + /// + /// Can be used to show ui, e.g. a [`crate::Window`] or [`crate::SidePanel`]. fn on_begin_pass(&mut self, ctx: &Context) {} + + /// Called at the end of each pass. + /// + /// Can be used to show ui, e.g. a [`crate::Window`]. fn on_end_pass(&mut self, ctx: &Context) {} + + /// 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. fn output_hook(&mut self, output: &mut FullOutput) {} + + /// 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. fn input_hook(&mut self, input: &mut RawInput) {} } -/// Callbacks that users can register +/// User-registered plugins. #[derive(Clone, Default)] struct Plugins { - // pub on_begin_pass: Vec, - // pub on_end_pass: Vec, - pub plugins: Arc>>>, + plugins: Arc>>>, } impl Plugins { @@ -150,6 +172,20 @@ impl Plugins { plugin.output_hook(output); }); } + + /// Remember to call [`Plugin::setup`] when adding a plugin. + /// + /// Will not add the plugin if a plugin of the same type already exists. + fn add(&self, plugin: Box) { + profiling::scope!("plugins", "add"); + let mut plugins = self.plugins.lock(); + if !plugins + .iter() + .any(|p| (**p).type_id() == (*plugin).type_id()) + { + plugins.push(plugin); + }; + } } // ---------------------------------------------------------------------------- @@ -1948,11 +1984,18 @@ impl Context { self.add_plugin(on_end_pass); } - pub fn add_plugin(&self, plugin: impl Plugin + 'static) { + /// Register a [`Plugin`] + /// + /// Plugins are called in the order they are added. + /// + /// A plugin of the same type can only be added once (further calls with the same type will be ignored). + /// This way it's convenient to add plugins in `eframe::run_simple_native`. + pub fn add_plugin(&self, mut plugin: impl Plugin + 'static) { profiling::function_scope!(); + plugin.setup(self); self.write(|ctx| { - ctx.plugins.plugins.lock().push(Box::new(plugin)); + ctx.plugins.add(Box::new(plugin)); }); } } From 54f56c46a7242ec95ec5be9b904b68ddad763d13 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Jul 2025 11:11:21 +0200 Subject: [PATCH 04/22] Lint --- crates/egui/src/context.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index bae28dddddc..1af8cc5f9a4 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1949,6 +1949,7 @@ impl Context { fn name(&self) -> &'static str { self.debug_name } + fn on_begin_pass(&mut self, ctx: &Context) { (self.callback)(ctx); } @@ -1976,6 +1977,7 @@ impl Context { fn name(&self) -> &'static str { self.debug_name } + fn on_end_pass(&mut self, ctx: &Context) { (self.callback)(ctx); } From e5887bbbb97335c1e147f5043bd423b99063a2c0 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Jul 2025 11:08:19 +0200 Subject: [PATCH 05/22] Add setup fn, prevent adding popup multiple times and add documentation --- crates/egui/src/context.rs | 69 +++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 45e68f4267a..bae28dddddc 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1,5 +1,6 @@ #![warn(missing_docs)] // Let's keep `Context` well-documented. +use std::any::Any as _; use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration}; use crate::{ @@ -95,27 +96,48 @@ impl Default for WrappedTextureManager { /// Generic event callback. pub type ContextCallback = Arc; -#[derive(Clone)] -struct NamedContextCallback { - debug_name: &'static str, - callback: ContextCallback, -} - -#[allow(unused_variables)] +/// A plugin to extend egui. +/// +/// Add plugins via [`Context::add_plugin`]. +#[expect(unused_variables)] pub trait Plugin: Send + Sync { + /// Plugin name. + /// + /// Used when profiling. fn name(&self) -> &'static str; + + /// Called once, when the plugin is registered. + /// + /// Useful to e.g. register image loaders. + fn setup(&mut self, ctx: &Context) {} + + /// Called at the start of each pass. + /// + /// Can be used to show ui, e.g. a [`crate::Window`] or [`crate::SidePanel`]. fn on_begin_pass(&mut self, ctx: &Context) {} + + /// Called at the end of each pass. + /// + /// Can be used to show ui, e.g. a [`crate::Window`]. fn on_end_pass(&mut self, ctx: &Context) {} + + /// 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. fn output_hook(&mut self, output: &mut FullOutput) {} + + /// 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. fn input_hook(&mut self, input: &mut RawInput) {} } -/// Callbacks that users can register +/// User-registered plugins. #[derive(Clone, Default)] struct Plugins { - // pub on_begin_pass: Vec, - // pub on_end_pass: Vec, - pub plugins: Arc>>>, + plugins: Arc>>>, } impl Plugins { @@ -150,6 +172,20 @@ impl Plugins { plugin.output_hook(output); }); } + + /// Remember to call [`Plugin::setup`] when adding a plugin. + /// + /// Will not add the plugin if a plugin of the same type already exists. + fn add(&self, plugin: Box) { + profiling::scope!("plugins", "add"); + let mut plugins = self.plugins.lock(); + if !plugins + .iter() + .any(|p| (**p).type_id() == (*plugin).type_id()) + { + plugins.push(plugin); + }; + } } // ---------------------------------------------------------------------------- @@ -1948,11 +1984,18 @@ impl Context { self.add_plugin(on_end_pass); } - pub fn add_plugin(&self, plugin: impl Plugin + 'static) { + /// Register a [`Plugin`] + /// + /// Plugins are called in the order they are added. + /// + /// A plugin of the same type can only be added once (further calls with the same type will be ignored). + /// This way it's convenient to add plugins in `eframe::run_simple_native`. + pub fn add_plugin(&self, mut plugin: impl Plugin + 'static) { profiling::function_scope!(); + plugin.setup(self); self.write(|ctx| { - ctx.plugins.plugins.lock().push(Box::new(plugin)); + ctx.plugins.add(Box::new(plugin)); }); } } From ec402615275ec4e9555d9e6b2ab87612b8337b0c Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Jul 2025 11:11:21 +0200 Subject: [PATCH 06/22] Lint --- crates/egui/src/context.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index bae28dddddc..1af8cc5f9a4 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1949,6 +1949,7 @@ impl Context { fn name(&self) -> &'static str { self.debug_name } + fn on_begin_pass(&mut self, ctx: &Context) { (self.callback)(ctx); } @@ -1976,6 +1977,7 @@ impl Context { fn name(&self) -> &'static str { self.debug_name } + fn on_end_pass(&mut self, ctx: &Context) { (self.callback)(ctx); } From 4488bec2cea0553e3e1c8d8aeacc5ba0ed26cd82 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 16 Jul 2025 16:54:08 +0200 Subject: [PATCH 07/22] Add basic accessibility inspector plugin --- Cargo.lock | 2 + crates/egui_demo_app/Cargo.toml | 2 + .../src/accessibility_inspector.rs | 212 ++++++++++++++++++ crates/egui_demo_app/src/lib.rs | 1 + crates/egui_demo_app/src/wrap_app.rs | 3 + 5 files changed, 220 insertions(+) create mode 100644 crates/egui_demo_app/src/accessibility_inspector.rs diff --git a/Cargo.lock b/Cargo.lock index b1ed1855914..d55bd13f6a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1310,6 +1310,8 @@ dependencies = [ name = "egui_demo_app" version = "0.32.0" dependencies = [ + "accesskit", + "accesskit_consumer", "bytemuck", "chrono", "eframe", diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 5868ed481ec..4d218708a12 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -64,6 +64,8 @@ log.workspace = true profiling.workspace = true # Optional dependencies: +accesskit = "0.19.0" +accesskit_consumer = "0.28.0" bytemuck = { workspace = true, optional = true } puffin = { workspace = true, optional = true } diff --git a/crates/egui_demo_app/src/accessibility_inspector.rs b/crates/egui_demo_app/src/accessibility_inspector.rs new file mode 100644 index 00000000000..ffcc9fa9b3b --- /dev/null +++ b/crates/egui_demo_app/src/accessibility_inspector.rs @@ -0,0 +1,212 @@ +use accesskit::{Action, ActionRequest, NodeId}; +use accesskit_consumer::{Node, Tree, TreeChangeHandler}; +use eframe::emath::Align; +use eframe::epaint::text::TextWrapMode; +use egui::collapsing_header::CollapsingState; +use egui::{ + Button, CollapsingHeader, Color32, Context, Event, FullOutput, Id, Key, KeyboardShortcut, + Layout, Modifiers, Pos2, RawInput, Rect, ScrollArea, SidePanel, Ui, +}; +use std::mem; + +#[derive(Default, Debug)] +pub struct AccessibilityInspectorPlugin { + pub open: bool, + tree: Option, + selected_node: Option, + queued_action: Option, +} + +struct ChangeHandler; + +impl TreeChangeHandler for ChangeHandler { + fn node_added(&mut self, node: &Node) {} + fn node_updated(&mut self, old_node: &Node, new_node: &Node) {} + fn focus_moved(&mut self, old_node: Option<&Node>, new_node: Option<&Node>) {} + fn node_removed(&mut self, node: &Node) {} +} + +impl egui::Plugin for AccessibilityInspectorPlugin { + fn name(&self) -> &'static str { + "Accessibility Inspector" + } + + fn input_hook(&mut self, input: &mut RawInput) { + if let Some(queued_action) = self.queued_action.take() { + input + .events + .push(Event::AccessKitActionRequest(queued_action)); + } + } + + fn output_hook(&mut self, output: &mut FullOutput) { + if let Some(update) = output.platform_output.accesskit_update.clone() { + self.tree = match mem::take(&mut self.tree) { + None => { + // Create a new tree if it doesn't exist + Some(Tree::new(update, true)) + } + Some(mut tree) => { + // Update the tree with the latest accesskit data + tree.update_and_process_changes(update, &mut ChangeHandler); + + Some(tree) + } + } + } + } + + fn on_begin_pass(&mut self, ctx: &Context) { + if ctx.input_mut(|i| { + i.consume_shortcut(&KeyboardShortcut::new( + Modifiers::COMMAND | Modifiers::ALT, + Key::I, + )) + }) { + self.open = !self.open; + } + + if self.open { + ctx.enable_accesskit(); + if let Some(selected_node) = &self.selected_node { + SidePanel::right(Self::id().with("details")).show(ctx, |ui| { + if let Some(tree) = &self.tree { + if let Some(node) = + tree.state().node_by_id(NodeId::from(selected_node.value())) + { + let node_response = ui.ctx().read_response(*selected_node); + + if let Some(widget_response) = node_response { + ui.ctx().debug_painter().debug_rect( + widget_response.rect, + ui.style_mut().visuals.selection.bg_fill, + "", + ); + } + + ui.label(format!("Node ID: {:?}", selected_node)); + ui.label(format!("Role: {:?}", node.role())); + ui.label(format!("Label: {}", node.label().unwrap_or_default())); + ui.label(format!("Value: {}", node.value().unwrap_or_default())); + ui.label(format!("Children Count: {}", node.children().len())); + + ui.label("Supported Actions:"); + for action_n in 0..50 { + let action = Action::n(action_n); + let Some(action) = action else { + break; + }; + if node.supports_action(action) { + if ui.button(format!("{:?}", action)).clicked() { + let action_request = ActionRequest { + target: node.id(), + action, + data: None, + }; + self.queued_action = Some(action_request); + } + } + } + } else { + ui.label("Node not found"); + } + } else { + ui.label("No tree data available"); + } + }); + } + + SidePanel::right(Self::id()).show(ctx, |ui| { + ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate); + let response = ui.heading("🔎 AccessKit Inspector"); + ui.separator(); + ctx.with_accessibility_parent(response.id, || { + ScrollArea::vertical().show(ui, |ui| { + if let Some(tree) = &self.tree { + Self::node_ui(ui, &tree.state().root(), &mut self.selected_node); + } + }); + }); + }); + } + } +} + +impl AccessibilityInspectorPlugin { + fn id() -> Id { + Id::new("Accessibility Inspector") + } + + fn node_ui(ui: &mut Ui, node: &Node, selected_node: &mut Option) { + if node.id() == Self::id().value().into() + || node + .value() + .as_deref() + .is_some_and(|l| l.contains("AccessKit Inspector")) + { + return; + } + let label = node + .label() + .or(node.value()) + .unwrap_or(node.id().0.to_string()); + let label = format!("({:?}) {}", node.role(), label); + + let node_id = Id::from_value(node.id().0.try_into().unwrap()); + + ui.push_id(node.id(), |ui| { + // let response = CollapsingHeader::new(label.clone()).show(ui, |ui| { + // node.children().for_each(|c| { + // Self::node_ui(ui, &c); + // }) + // }); + + let child_count = node.children().len(); + let has_children = child_count > 0; + let default_open = child_count == 1 && node.role() != accesskit::Role::Label; + + let mut collapsing = CollapsingState::load_with_default_open( + ui.ctx(), + node_id.with("ak_collapse"), + default_open, + ); + + let header_response = ui.horizontal(|ui| { + let text = if collapsing.is_open() { "⏷" } else { "⏵" }; + + if ui + .add_visible(has_children, Button::new(text).frame_when_inactive(false)) + .clicked() + { + collapsing.set_open(!collapsing.is_open()); + }; + let label_response = ui.selectable_value( + selected_node, + Some(Id::from_value(node.id().0)), + label.clone(), + ); + if label_response.hovered() { + let widget_response = ui.ctx().read_response(node_id); + + if let Some(widget_response) = widget_response { + ui.ctx().debug_painter().debug_rect( + widget_response.rect, + Color32::RED, + label, + ); + } + } + }); + + if has_children { + collapsing.show_body_indented(&header_response.response, ui, |ui| { + node.children().for_each(|c| { + Self::node_ui(ui, &c, selected_node); + }) + }); + } + + collapsing.store(ui.ctx()); + }); + } +} diff --git a/crates/egui_demo_app/src/lib.rs b/crates/egui_demo_app/src/lib.rs index 9cfa26baadc..d97349bf1b0 100644 --- a/crates/egui_demo_app/src/lib.rs +++ b/crates/egui_demo_app/src/lib.rs @@ -17,6 +17,7 @@ pub(crate) fn seconds_since_midnight() -> f64 { // ---------------------------------------------------------------------------- +mod accessibility_inspector; #[cfg(target_arch = "wasm32")] mod web; diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index 3775d93d211..d636fcf62a1 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -3,6 +3,7 @@ use egui_demo_lib::is_mobile; #[cfg(feature = "glow")] use eframe::glow; +use crate::accessibility_inspector::AccessibilityInspectorPlugin; #[cfg(target_arch = "wasm32")] use core::any::Any; @@ -187,6 +188,8 @@ impl WrapApp { pub fn new(cc: &eframe::CreationContext<'_>) -> Self { // This gives us image support: egui_extras::install_image_loaders(&cc.egui_ctx); + cc.egui_ctx + .add_plugin(AccessibilityInspectorPlugin::default()); #[allow(unused_mut, clippy::allow_attributes)] let mut slf = Self { From fb99bba020c0c29613725257054e262006d7c5c9 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Jul 2025 11:54:52 +0200 Subject: [PATCH 08/22] Don't call setup again when adding plugin multiple times --- crates/egui/src/context.rs | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 1af8cc5f9a4..c75162c8938 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -99,6 +99,9 @@ pub type ContextCallback = Arc; /// A plugin to extend egui. /// /// Add plugins via [`Context::add_plugin`]. +/// +/// Plugins should not hold a reference to the [`Context`], since this would create a cycle +/// (which would prevent the [`Context`] from being dropped). #[expect(unused_variables)] pub trait Plugin: Send + Sync { /// Plugin name. @@ -173,17 +176,18 @@ impl Plugins { }); } - /// Remember to call [`Plugin::setup`] when adding a plugin. - /// /// Will not add the plugin if a plugin of the same type already exists. - fn add(&self, plugin: Box) { + fn add(&self, ctx: &Context, mut plugin: Box) { profiling::scope!("plugins", "add"); - let mut plugins = self.plugins.lock(); - if !plugins + let contains_plugin = self + .plugins + .lock() .iter() - .any(|p| (**p).type_id() == (*plugin).type_id()) - { - plugins.push(plugin); + .any(|p| (**p).type_id() == (*plugin).type_id()); + if !contains_plugin { + // We don't hold the lock during `setup`, so that a plugin could add more plugins. + plugin.setup(ctx); + self.plugins.lock().push(plugin); }; } } @@ -1992,13 +1996,11 @@ impl Context { /// /// A plugin of the same type can only be added once (further calls with the same type will be ignored). /// This way it's convenient to add plugins in `eframe::run_simple_native`. - pub fn add_plugin(&self, mut plugin: impl Plugin + 'static) { + pub fn add_plugin(&self, plugin: impl Plugin + 'static) { profiling::function_scope!(); - plugin.setup(self); - self.write(|ctx| { - ctx.plugins.add(Box::new(plugin)); - }); + let plugins = self.write(|ctx| ctx.plugins.clone()); + plugins.add(self, Box::new(plugin)); } } From a6c4e2f94ae2051c8054e0a150d925cc5c4762ca Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Jul 2025 15:12:20 +0200 Subject: [PATCH 09/22] Make it possible to obtain a typed reference to a plugin --- crates/egui/src/context.rs | 231 +++++++++++++++++++++++++------------ 1 file changed, 157 insertions(+), 74 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index c75162c8938..3ba8a420942 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1,8 +1,7 @@ #![warn(missing_docs)] // Let's keep `Context` well-documented. -use std::any::Any as _; -use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration}; - +#[cfg(feature = "accesskit")] +use crate::IdMap; use crate::{ Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport, ImmediateViewportRendererCallback, Key, KeyboardShortcut, Label, LayerId, Memory, @@ -26,7 +25,9 @@ use crate::{ util::IdTypeMap, viewport::ViewportClass, }; +use ahash::HashMap; use emath::{GuiRounding as _, OrderedFloat}; +use epaint::mutex::Mutex; use epaint::{ ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect, StrokeKind, TessellationOptions, TextureAtlas, TextureId, Vec2, @@ -37,9 +38,8 @@ use epaint::{ text::{FontInsert, FontPriority, Fonts}, vec2, }; - -#[cfg(feature = "accesskit")] -use crate::IdMap; +use std::any::{Any as _, TypeId}; +use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration}; use self::{hit_test::WidgetHits, interaction::InteractionSnapshot}; @@ -137,58 +137,128 @@ pub trait Plugin: Send + Sync { fn input_hook(&mut self, input: &mut RawInput) {} } +struct PluginHandle { + plugin: Box, + get_plugin: fn(&Self) -> &dyn Plugin, + get_plugin_mut: fn(&mut Self) -> &mut dyn Plugin, +} + +impl PluginHandle { + fn new(plugin: P) -> Arc> { + Arc::new(Mutex::new(Self { + plugin: Box::new(plugin), + get_plugin: |handle| { + let plugin: &P = handle.typed_plugin(); + plugin as &dyn Plugin + }, + get_plugin_mut: |handle| { + let plugin: &mut P = handle.typed_plugin_mut(); + plugin as &mut dyn Plugin + }, + })) + } + + fn plugin_type_id(&self) -> std::any::TypeId { + (*self.plugin).type_id() + } + + fn dyn_plugin(&self) -> &dyn Plugin { + (self.get_plugin)(self) + } + + fn dyn_plugin_mut(&mut self) -> &mut dyn Plugin { + (self.get_plugin_mut)(self) + } + + fn typed_plugin(&self) -> &P { + (*self.plugin) + .downcast_ref::

() + .expect("PluginHandle: plugin is not of the expected type") + } + + fn typed_plugin_mut(&mut self) -> &mut P { + (*self.plugin) + .downcast_mut::

() + .expect("PluginHandle: plugin is not of the expected type") + } +} + /// User-registered plugins. #[derive(Clone, Default)] struct Plugins { - plugins: Arc>>>, + plugins: HashMap>>, + plugins_ordered: PluginsOrdered, } -impl Plugins { +#[derive(Clone, Default)] +struct PluginsOrdered(Vec>>); + +impl PluginsOrdered { + fn for_each_dyn(&self, mut f: F) + where + F: FnMut(&mut dyn Plugin), + { + for plugin in &self.0 { + let mut plugin = plugin.lock(); + profiling::scope!("plugin", plugin.dyn_plugin().name()); + f(plugin.dyn_plugin_mut()); + } + } + fn on_begin_pass(&self, ctx: &Context) { profiling::scope!("plugins", "on_begin_pass"); - self.plugins.lock().iter_mut().for_each(|plugin| { - profiling::scope!("plugin", plugin.name()); - plugin.on_begin_pass(ctx); + self.for_each_dyn(|p| { + p.on_begin_pass(ctx); }); } fn on_end_pass(&self, ctx: &Context) { profiling::scope!("plugins", "on_end_pass"); - self.plugins.lock().iter_mut().for_each(|plugin| { - profiling::scope!("plugin", plugin.name()); - plugin.on_end_pass(ctx); + self.for_each_dyn(|p| { + p.on_end_pass(ctx); }); } fn on_input(&self, input: &mut RawInput) { profiling::scope!("plugins", "on_input"); - self.plugins.lock().iter_mut().for_each(|plugin| { - profiling::scope!("plugin", plugin.name()); + self.for_each_dyn(|plugin| { plugin.input_hook(input); }); } fn on_output(&self, output: &mut FullOutput) { profiling::scope!("plugins", "on_output"); - self.plugins.lock().iter_mut().for_each(|plugin| { - profiling::scope!("plugin", plugin.name()); + self.for_each_dyn(|plugin| { plugin.output_hook(output); }); } +} + +impl Plugins { + fn ordered_plugins(&self) -> PluginsOrdered { + self.plugins_ordered.clone() + } /// Will not add the plugin if a plugin of the same type already exists. - fn add(&self, ctx: &Context, mut plugin: Box) { + /// + /// Returns `false` if the plugin was not added, `true` if it was added. + fn add(&mut self, ctx: &Context, mut handle: Arc>) -> bool { profiling::scope!("plugins", "add"); - let contains_plugin = self - .plugins - .lock() - .iter() - .any(|p| (**p).type_id() == (*plugin).type_id()); - if !contains_plugin { - // We don't hold the lock during `setup`, so that a plugin could add more plugins. - plugin.setup(ctx); - self.plugins.lock().push(plugin); - }; + + let type_id = handle.lock().plugin_type_id(); + + if self.plugins.contains_key(&type_id) { + return false; + } + + self.plugins.insert(type_id, handle.clone()); + self.plugins_ordered.0.push(handle); + + true + } + + fn get(&self, type_id: std::any::TypeId) -> Option>> { + self.plugins.get(&type_id).cloned() } } @@ -971,7 +1041,7 @@ impl Context { pub fn begin_pass(&self, mut new_input: RawInput) { profiling::function_scope!(); - let plugins = self.read(|ctx| ctx.plugins.clone()); + let plugins = self.read(|ctx| ctx.plugins.ordered_plugins()); plugins.on_input(&mut new_input); self.write(|ctx| ctx.begin_pass(new_input)); @@ -1932,6 +2002,36 @@ impl Context { } } +#[derive(Default)] +struct CallbackPlugins { + on_begin_plugins: Vec<(&'static str, ContextCallback)>, + on_end_plugins: Vec<(&'static str, ContextCallback)>, +} + +impl Plugin for CallbackPlugins { + fn name(&self) -> &'static str { + "CallbackPlugins" + } + + fn on_begin_pass(&mut self, ctx: &Context) { + profiling::function_scope!(); + + for (debug_name, cb) in &self.on_begin_plugins { + profiling::scope!(*debug_name); + (cb)(ctx); + } + } + + fn on_end_pass(&mut self, ctx: &Context) { + profiling::function_scope!(); + + for (debug_name, cb) in &self.on_end_plugins { + profiling::scope!(*debug_name); + (cb)(ctx); + } + } +} + /// Callbacks impl Context { /// Call the given callback at the start of each pass of each viewport. @@ -1939,27 +2039,11 @@ impl Context { /// This can be used for egui _plugins_. /// See [`crate::debug_text`] for an example. pub fn on_begin_pass(&self, debug_name: &'static str, cb: ContextCallback) { - struct OnBeginPass { - debug_name: &'static str, - callback: ContextCallback, - } - - let on_begin_pass = OnBeginPass { - debug_name, - callback: cb, - }; - - impl Plugin for OnBeginPass { - fn name(&self) -> &'static str { - self.debug_name - } + self.add_plugin(CallbackPlugins::default()); - fn on_begin_pass(&mut self, ctx: &Context) { - (self.callback)(ctx); - } - } - - self.add_plugin(on_begin_pass); + self.with_plugin(|p: &mut CallbackPlugins| { + p.on_begin_plugins.push((debug_name, cb)); + }); } /// Call the given callback at the end of each pass of each viewport. @@ -1967,27 +2051,11 @@ impl Context { /// This can be used for egui _plugins_. /// See [`crate::debug_text`] for an example. pub fn on_end_pass(&self, debug_name: &'static str, cb: ContextCallback) { - struct OnEndPass { - debug_name: &'static str, - callback: ContextCallback, - } + self.add_plugin(CallbackPlugins::default()); - let on_end_pass = OnEndPass { - debug_name, - callback: cb, - }; - - impl Plugin for OnEndPass { - fn name(&self) -> &'static str { - self.debug_name - } - - fn on_end_pass(&mut self, ctx: &Context) { - (self.callback)(ctx); - } - } - - self.add_plugin(on_end_pass); + self.with_plugin(|p: &mut CallbackPlugins| { + p.on_end_plugins.push((debug_name, cb)); + }); } /// Register a [`Plugin`] @@ -1997,10 +2065,25 @@ impl Context { /// A plugin of the same type can only be added once (further calls with the same type will be ignored). /// This way it's convenient to add plugins in `eframe::run_simple_native`. pub fn add_plugin(&self, plugin: impl Plugin + 'static) { - profiling::function_scope!(); + let mut handle = PluginHandle::new(plugin); + + let added = self.write(|ctx| ctx.plugins.add(self, handle.clone())); + + if added { + handle.lock().dyn_plugin_mut().setup(self); + } + } - let plugins = self.write(|ctx| ctx.plugins.clone()); - plugins.add(self, Box::new(plugin)); + /// Call the provided closure with the plugin of type `T`, if it was registered. + /// + /// Returns `None` if the plugin was not registered. + pub fn with_plugin(&self, f: impl FnOnce(&mut T) -> R) -> Option { + let plugin = self.read(|ctx| ctx.plugins.get(TypeId::of::())); + if let Some(plugin) = plugin { + Some(f(plugin.lock().typed_plugin_mut())) + } else { + None + } } } @@ -2366,7 +2449,7 @@ impl Context { } // Plugins run just before the pass ends. - let plugins = self.read(|ctx| ctx.plugins.clone()); + let plugins = self.read(|ctx| ctx.plugins.ordered_plugins()); plugins.on_end_pass(self); #[cfg(debug_assertions)] From b42c3f6e0d78eb73f9391e0aebae46730ef4c712 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Jul 2025 15:27:45 +0200 Subject: [PATCH 10/22] Move to plugins.rs --- crates/egui/src/context.rs | 235 ++----------------------------------- crates/egui/src/lib.rs | 6 +- crates/egui/src/plugin.rs | 204 ++++++++++++++++++++++++++++++++ 3 files changed, 221 insertions(+), 224 deletions(-) create mode 100644 crates/egui/src/plugin.rs diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 3ba8a420942..e0221228b9a 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -5,7 +5,7 @@ use crate::IdMap; use crate::{ Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport, ImmediateViewportRendererCallback, Key, KeyboardShortcut, Label, LayerId, Memory, - ModifierNames, Modifiers, NumExt as _, Order, Painter, RawInput, Response, RichText, + ModifierNames, Modifiers, NumExt as _, Order, Painter, Plugin, RawInput, Response, RichText, ScrollArea, Sense, Style, TextStyle, TextureHandle, TextureOptions, Ui, ViewportBuilder, ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportOutput, Widget as _, WidgetRect, WidgetText, @@ -21,13 +21,11 @@ use crate::{ os::OperatingSystem, output::FullOutput, pass_state::PassState, - resize, response, scroll_area, + plugin, resize, response, scroll_area, util::IdTypeMap, viewport::ViewportClass, }; -use ahash::HashMap; use emath::{GuiRounding as _, OrderedFloat}; -use epaint::mutex::Mutex; use epaint::{ ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect, StrokeKind, TessellationOptions, TextureAtlas, TextureId, Vec2, @@ -38,7 +36,7 @@ use epaint::{ text::{FontInsert, FontPriority, Fonts}, vec2, }; -use std::any::{Any as _, TypeId}; +use std::any::TypeId; use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration}; use self::{hit_test::WidgetHits, interaction::InteractionSnapshot}; @@ -93,177 +91,6 @@ impl Default for WrappedTextureManager { // ---------------------------------------------------------------------------- -/// Generic event callback. -pub type ContextCallback = Arc; - -/// A plugin to extend egui. -/// -/// Add plugins via [`Context::add_plugin`]. -/// -/// Plugins should not hold a reference to the [`Context`], since this would create a cycle -/// (which would prevent the [`Context`] from being dropped). -#[expect(unused_variables)] -pub trait Plugin: Send + Sync { - /// Plugin name. - /// - /// Used when profiling. - fn name(&self) -> &'static str; - - /// Called once, when the plugin is registered. - /// - /// Useful to e.g. register image loaders. - fn setup(&mut self, ctx: &Context) {} - - /// Called at the start of each pass. - /// - /// Can be used to show ui, e.g. a [`crate::Window`] or [`crate::SidePanel`]. - fn on_begin_pass(&mut self, ctx: &Context) {} - - /// Called at the end of each pass. - /// - /// Can be used to show ui, e.g. a [`crate::Window`]. - fn on_end_pass(&mut self, ctx: &Context) {} - - /// 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. - fn output_hook(&mut self, output: &mut FullOutput) {} - - /// 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. - fn input_hook(&mut self, input: &mut RawInput) {} -} - -struct PluginHandle { - plugin: Box, - get_plugin: fn(&Self) -> &dyn Plugin, - get_plugin_mut: fn(&mut Self) -> &mut dyn Plugin, -} - -impl PluginHandle { - fn new(plugin: P) -> Arc> { - Arc::new(Mutex::new(Self { - plugin: Box::new(plugin), - get_plugin: |handle| { - let plugin: &P = handle.typed_plugin(); - plugin as &dyn Plugin - }, - get_plugin_mut: |handle| { - let plugin: &mut P = handle.typed_plugin_mut(); - plugin as &mut dyn Plugin - }, - })) - } - - fn plugin_type_id(&self) -> std::any::TypeId { - (*self.plugin).type_id() - } - - fn dyn_plugin(&self) -> &dyn Plugin { - (self.get_plugin)(self) - } - - fn dyn_plugin_mut(&mut self) -> &mut dyn Plugin { - (self.get_plugin_mut)(self) - } - - fn typed_plugin(&self) -> &P { - (*self.plugin) - .downcast_ref::

() - .expect("PluginHandle: plugin is not of the expected type") - } - - fn typed_plugin_mut(&mut self) -> &mut P { - (*self.plugin) - .downcast_mut::

() - .expect("PluginHandle: plugin is not of the expected type") - } -} - -/// User-registered plugins. -#[derive(Clone, Default)] -struct Plugins { - plugins: HashMap>>, - plugins_ordered: PluginsOrdered, -} - -#[derive(Clone, Default)] -struct PluginsOrdered(Vec>>); - -impl PluginsOrdered { - fn for_each_dyn(&self, mut f: F) - where - F: FnMut(&mut dyn Plugin), - { - for plugin in &self.0 { - let mut plugin = plugin.lock(); - profiling::scope!("plugin", plugin.dyn_plugin().name()); - f(plugin.dyn_plugin_mut()); - } - } - - fn on_begin_pass(&self, ctx: &Context) { - profiling::scope!("plugins", "on_begin_pass"); - self.for_each_dyn(|p| { - p.on_begin_pass(ctx); - }); - } - - fn on_end_pass(&self, ctx: &Context) { - profiling::scope!("plugins", "on_end_pass"); - self.for_each_dyn(|p| { - p.on_end_pass(ctx); - }); - } - - fn on_input(&self, input: &mut RawInput) { - profiling::scope!("plugins", "on_input"); - self.for_each_dyn(|plugin| { - plugin.input_hook(input); - }); - } - - fn on_output(&self, output: &mut FullOutput) { - profiling::scope!("plugins", "on_output"); - self.for_each_dyn(|plugin| { - plugin.output_hook(output); - }); - } -} - -impl Plugins { - fn ordered_plugins(&self) -> PluginsOrdered { - self.plugins_ordered.clone() - } - - /// Will not add the plugin if a plugin of the same type already exists. - /// - /// Returns `false` if the plugin was not added, `true` if it was added. - fn add(&mut self, ctx: &Context, mut handle: Arc>) -> bool { - profiling::scope!("plugins", "add"); - - let type_id = handle.lock().plugin_type_id(); - - if self.plugins.contains_key(&type_id) { - return false; - } - - self.plugins.insert(type_id, handle.clone()); - self.plugins_ordered.0.push(handle); - - true - } - - fn get(&self, type_id: std::any::TypeId) -> Option>> { - self.plugins.get(&type_id).cloned() - } -} - -// ---------------------------------------------------------------------------- - /// Repaint-logic impl ContextImpl { /// This is where we update the repaint logic. @@ -548,7 +375,7 @@ struct ContextImpl { memory: Memory, animation_manager: AnimationManager, - plugins: Plugins, + plugins: plugin::Plugins, /// All viewports share the same texture manager and texture namespace. /// @@ -905,6 +732,8 @@ impl Default for Context { }; let ctx = Self(Arc::new(RwLock::new(ctx_impl))); + ctx.add_plugin(crate::plugin::CallbackPlugin::default()); + // Register built-in plugins: crate::debug_text::register(&ctx); crate::text_selection::LabelSelectionState::register(&ctx); @@ -2002,46 +1831,14 @@ impl Context { } } -#[derive(Default)] -struct CallbackPlugins { - on_begin_plugins: Vec<(&'static str, ContextCallback)>, - on_end_plugins: Vec<(&'static str, ContextCallback)>, -} - -impl Plugin for CallbackPlugins { - fn name(&self) -> &'static str { - "CallbackPlugins" - } - - fn on_begin_pass(&mut self, ctx: &Context) { - profiling::function_scope!(); - - for (debug_name, cb) in &self.on_begin_plugins { - profiling::scope!(*debug_name); - (cb)(ctx); - } - } - - fn on_end_pass(&mut self, ctx: &Context) { - profiling::function_scope!(); - - for (debug_name, cb) in &self.on_end_plugins { - profiling::scope!(*debug_name); - (cb)(ctx); - } - } -} - /// Callbacks impl Context { /// Call the given callback at the start of each pass of each viewport. /// /// This can be used for egui _plugins_. /// See [`crate::debug_text`] for an example. - pub fn on_begin_pass(&self, debug_name: &'static str, cb: ContextCallback) { - self.add_plugin(CallbackPlugins::default()); - - self.with_plugin(|p: &mut CallbackPlugins| { + pub fn on_begin_pass(&self, debug_name: &'static str, cb: plugin::ContextCallback) { + self.with_plugin(|p: &mut crate::plugin::CallbackPlugin| { p.on_begin_plugins.push((debug_name, cb)); }); } @@ -2050,10 +1847,8 @@ impl Context { /// /// This can be used for egui _plugins_. /// See [`crate::debug_text`] for an example. - pub fn on_end_pass(&self, debug_name: &'static str, cb: ContextCallback) { - self.add_plugin(CallbackPlugins::default()); - - self.with_plugin(|p: &mut CallbackPlugins| { + pub fn on_end_pass(&self, debug_name: &'static str, cb: plugin::ContextCallback) { + self.with_plugin(|p: &mut crate::plugin::CallbackPlugin| { p.on_end_plugins.push((debug_name, cb)); }); } @@ -2065,9 +1860,9 @@ impl Context { /// A plugin of the same type can only be added once (further calls with the same type will be ignored). /// This way it's convenient to add plugins in `eframe::run_simple_native`. pub fn add_plugin(&self, plugin: impl Plugin + 'static) { - let mut handle = PluginHandle::new(plugin); + let handle = plugin::PluginHandle::new(plugin); - let added = self.write(|ctx| ctx.plugins.add(self, handle.clone())); + let added = self.write(|ctx| ctx.plugins.add(handle.clone())); if added { handle.lock().dyn_plugin_mut().setup(self); @@ -2079,11 +1874,7 @@ impl Context { /// Returns `None` if the plugin was not registered. pub fn with_plugin(&self, f: impl FnOnce(&mut T) -> R) -> Option { let plugin = self.read(|ctx| ctx.plugins.get(TypeId::of::())); - if let Some(plugin) = plugin { - Some(f(plugin.lock().typed_plugin_mut())) - } else { - None - } + plugin.map(|plugin| f(plugin.lock().typed_plugin_mut())) } } diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index d2c2eeac32c..56adc01dc53 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -406,6 +406,7 @@ #![allow(clippy::manual_range_contains)] mod animation_manager; +mod atomics; pub mod cache; pub mod containers; mod context; @@ -429,6 +430,7 @@ pub mod os; mod painter; mod pass_state; pub(crate) mod placer; +mod plugin; pub mod response; mod sense; pub mod style; @@ -442,7 +444,6 @@ mod widget_rect; pub mod widget_text; pub mod widgets; -mod atomics; #[cfg(feature = "callstack")] #[cfg(debug_assertions)] mod callstack; @@ -482,7 +483,7 @@ pub mod text { pub use self::{ atomics::*, containers::{menu::MenuBar, *}, - context::{Context, Plugin, RepaintCause, RequestRepaintInfo}, + context::{Context, RepaintCause, RequestRepaintInfo}, data::{ Key, UserData, input::*, @@ -501,6 +502,7 @@ pub use self::{ load::SizeHint, memory::{Memory, Options, Theme, ThemePreference}, painter::Painter, + plugin::Plugin, response::{InnerResponse, Response}, sense::Sense, style::{FontSelection, Spacing, Style, TextStyle, Visuals}, diff --git a/crates/egui/src/plugin.rs b/crates/egui/src/plugin.rs new file mode 100644 index 00000000000..ddf69f14e88 --- /dev/null +++ b/crates/egui/src/plugin.rs @@ -0,0 +1,204 @@ +use crate::{Context, FullOutput, RawInput}; +use ahash::HashMap; +use epaint::mutex::Mutex; +use std::sync::Arc; + +/// A plugin to extend egui. +/// +/// Add plugins via [`Context::add_plugin`]. +/// +/// Plugins should not hold a reference to the [`Context`], since this would create a cycle +/// (which would prevent the [`Context`] from being dropped). +#[expect(unused_variables)] +pub trait Plugin: Send + Sync { + /// Plugin name. + /// + /// Used when profiling. + fn name(&self) -> &'static str; + + /// Called once, when the plugin is registered. + /// + /// Useful to e.g. register image loaders. + fn setup(&mut self, ctx: &Context) {} + + /// Called at the start of each pass. + /// + /// Can be used to show ui, e.g. a [`crate::Window`] or [`crate::SidePanel`]. + fn on_begin_pass(&mut self, ctx: &Context) {} + + /// Called at the end of each pass. + /// + /// Can be used to show ui, e.g. a [`crate::Window`]. + fn on_end_pass(&mut self, ctx: &Context) {} + + /// 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. + fn output_hook(&mut self, output: &mut FullOutput) {} + + /// 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. + fn input_hook(&mut self, input: &mut RawInput) {} +} + +pub(crate) struct PluginHandle { + plugin: Box, + get_plugin: fn(&Self) -> &dyn Plugin, + get_plugin_mut: fn(&mut Self) -> &mut dyn Plugin, +} + +impl PluginHandle { + pub fn new(plugin: P) -> Arc> { + Arc::new(Mutex::new(Self { + plugin: Box::new(plugin), + get_plugin: |handle| { + let plugin: &P = handle.typed_plugin(); + plugin as &dyn Plugin + }, + get_plugin_mut: |handle| { + let plugin: &mut P = handle.typed_plugin_mut(); + plugin as &mut dyn Plugin + }, + })) + } + + fn plugin_type_id(&self) -> std::any::TypeId { + (*self.plugin).type_id() + } + + fn dyn_plugin(&self) -> &dyn Plugin { + (self.get_plugin)(self) + } + + pub fn dyn_plugin_mut(&mut self) -> &mut dyn Plugin { + (self.get_plugin_mut)(self) + } + + fn typed_plugin(&self) -> &P { + (*self.plugin) + .downcast_ref::

() + .expect("PluginHandle: plugin is not of the expected type") + } + + pub fn typed_plugin_mut(&mut self) -> &mut P { + (*self.plugin) + .downcast_mut::

() + .expect("PluginHandle: plugin is not of the expected type") + } +} + +/// User-registered plugins. +#[derive(Clone, Default)] +pub(crate) struct Plugins { + plugins: HashMap>>, + plugins_ordered: PluginsOrdered, +} + +#[derive(Clone, Default)] +pub(crate) struct PluginsOrdered(Vec>>); + +impl PluginsOrdered { + fn for_each_dyn(&self, mut f: F) + where + F: FnMut(&mut dyn Plugin), + { + for plugin in &self.0 { + let mut plugin = plugin.lock(); + profiling::scope!("plugin", plugin.dyn_plugin().name()); + f(plugin.dyn_plugin_mut()); + } + } + + pub fn on_begin_pass(&self, ctx: &Context) { + profiling::scope!("plugins", "on_begin_pass"); + self.for_each_dyn(|p| { + p.on_begin_pass(ctx); + }); + } + + pub fn on_end_pass(&self, ctx: &Context) { + profiling::scope!("plugins", "on_end_pass"); + self.for_each_dyn(|p| { + p.on_end_pass(ctx); + }); + } + + pub fn on_input(&self, input: &mut RawInput) { + profiling::scope!("plugins", "on_input"); + self.for_each_dyn(|plugin| { + plugin.input_hook(input); + }); + } + + pub fn on_output(&self, output: &mut FullOutput) { + profiling::scope!("plugins", "on_output"); + self.for_each_dyn(|plugin| { + plugin.output_hook(output); + }); + } +} + +impl Plugins { + pub fn ordered_plugins(&self) -> PluginsOrdered { + self.plugins_ordered.clone() + } + + /// Remember to call [`Plugin::setup`] on the plugin after adding it. + /// + /// Will not add the plugin if a plugin of the same type already exists. + /// Returns `false` if the plugin was not added, `true` if it was added. + pub fn add(&mut self, handle: Arc>) -> bool { + profiling::scope!("plugins", "add"); + + let type_id = handle.lock().plugin_type_id(); + + if self.plugins.contains_key(&type_id) { + return false; + } + + self.plugins.insert(type_id, handle.clone()); + self.plugins_ordered.0.push(handle); + + true + } + + pub fn get(&self, type_id: std::any::TypeId) -> Option>> { + self.plugins.get(&type_id).cloned() + } +} + +/// Generic event callback. +pub type ContextCallback = Arc; + +#[derive(Default)] +pub(crate) struct CallbackPlugin { + pub on_begin_plugins: Vec<(&'static str, ContextCallback)>, + pub on_end_plugins: Vec<(&'static str, ContextCallback)>, +} + +impl Plugin for CallbackPlugin { + fn name(&self) -> &'static str { + "CallbackPlugins" + } + + fn on_begin_pass(&mut self, ctx: &Context) { + profiling::function_scope!(); + + for (debug_name, cb) in &self.on_begin_plugins { + profiling::scope!(*debug_name); + (cb)(ctx); + } + } + + fn on_end_pass(&mut self, ctx: &Context) { + profiling::function_scope!(); + + for (debug_name, cb) in &self.on_end_plugins { + profiling::scope!(*debug_name); + (cb)(ctx); + } + } +} From d0115d781ce9f8ea0e2b6c3202749779e2f9f948 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Jul 2025 16:07:22 +0200 Subject: [PATCH 11/22] Port LabelSelectionState to new Plugin api --- crates/egui/src/context.rs | 38 +++++++++- crates/egui/src/plugin.rs | 48 +++++++++++- .../text_selection/label_text_selection.rs | 75 +++++++------------ 3 files changed, 106 insertions(+), 55 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index e0221228b9a..086077ca5b9 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1,7 +1,10 @@ #![warn(missing_docs)] // Let's keep `Context` well-documented. +use self::{hit_test::WidgetHits, interaction::InteractionSnapshot}; #[cfg(feature = "accesskit")] use crate::IdMap; +use crate::plugin::TypedPluginHandle; +use crate::text_selection::LabelSelectionState; use crate::{ Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport, ImmediateViewportRendererCallback, Key, KeyboardShortcut, Label, LayerId, Memory, @@ -39,8 +42,6 @@ use epaint::{ use std::any::TypeId; use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration}; -use self::{hit_test::WidgetHits, interaction::InteractionSnapshot}; - /// Information given to the backend about when it is time to repaint the ui. /// /// This is given in the callback set by [`Context::set_request_repaint_callback`]. @@ -736,7 +737,7 @@ impl Default for Context { // Register built-in plugins: crate::debug_text::register(&ctx); - crate::text_selection::LabelSelectionState::register(&ctx); + ctx.add_plugin(LabelSelectionState::default()); crate::DragAndDrop::register(&ctx); ctx @@ -1876,6 +1877,35 @@ impl Context { let plugin = self.read(|ctx| ctx.plugins.get(TypeId::of::())); plugin.map(|plugin| f(plugin.lock().typed_plugin_mut())) } + + /// Get a handle to the plugin of type `T`. + /// + /// ## Panics + /// If the plugin of type `T` was not registered, this will panic. + pub fn plugin(&self) -> TypedPluginHandle { + if let Some(plugin) = self.plugin_opt() { + plugin + } else { + panic!("Plugin of type {:?} not found", std::any::type_name::()); + } + } + + /// Get a handle to the plugin of type `T`, if it was registered. + pub fn plugin_opt(&self) -> Option> { + let plugin = self.read(|ctx| ctx.plugins.get(TypeId::of::())); + plugin.map(TypedPluginHandle::new) + } + + /// Get a handle to the plugin of type `T`, or insert its default. + pub fn plugin_or_default(&self) -> TypedPluginHandle { + if let Some(plugin) = self.plugin_opt() { + plugin + } else { + let default_plugin = T::default(); + self.add_plugin(default_plugin); + self.plugin() + } + } } impl Context { @@ -3179,7 +3209,7 @@ impl Context { .show(ui, |ui| { ui.label(format!( "{:#?}", - crate::text_selection::LabelSelectionState::load(ui.ctx()) + *ui.ctx().plugin::().lock() )); }); diff --git a/crates/egui/src/plugin.rs b/crates/egui/src/plugin.rs index ddf69f14e88..c81138cbf4e 100644 --- a/crates/egui/src/plugin.rs +++ b/crates/egui/src/plugin.rs @@ -1,6 +1,6 @@ use crate::{Context, FullOutput, RawInput}; use ahash::HashMap; -use epaint::mutex::Mutex; +use epaint::mutex::{Mutex, MutexGuard}; use std::sync::Arc; /// A plugin to extend egui. @@ -10,7 +10,7 @@ use std::sync::Arc; /// Plugins should not hold a reference to the [`Context`], since this would create a cycle /// (which would prevent the [`Context`] from being dropped). #[expect(unused_variables)] -pub trait Plugin: Send + Sync { +pub trait Plugin: Send + Sync + 'static { /// Plugin name. /// /// Used when profiling. @@ -50,8 +50,50 @@ pub(crate) struct PluginHandle { get_plugin_mut: fn(&mut Self) -> &mut dyn Plugin, } +pub struct TypedPluginHandle { + handle: Arc>, + _type: std::marker::PhantomData

, +} + +impl TypedPluginHandle

{ + pub(crate) fn new(handle: Arc>) -> Self { + Self { + handle, + _type: std::marker::PhantomData, + } + } + + pub fn lock(&self) -> TypedPluginGuard<'_, P> { + TypedPluginGuard { + guard: self.handle.lock(), + _type: std::marker::PhantomData, + } + } +} + +pub struct TypedPluginGuard<'a, P: Plugin> { + guard: MutexGuard<'a, PluginHandle>, + _type: std::marker::PhantomData

, +} + +impl TypedPluginGuard<'_, P> {} + +impl std::ops::Deref for TypedPluginGuard<'_, P> { + type Target = P; + + fn deref(&self) -> &Self::Target { + self.guard.typed_plugin() + } +} + +impl std::ops::DerefMut for TypedPluginGuard<'_, P> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.guard.typed_plugin_mut() + } +} + impl PluginHandle { - pub fn new(plugin: P) -> Arc> { + pub fn new(plugin: P) -> Arc> { Arc::new(Mutex::new(Self { plugin: Box::new(plugin), get_plugin: |handle| { diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index 8297bc42975..16f1bfcff93 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use emath::TSTransform; use crate::{ - Context, CursorIcon, Event, Galley, Id, LayerId, Pos2, Rect, Response, Ui, layers::ShapeIdx, - text::CCursor, text_selection::CCursorRange, + Context, CursorIcon, Event, Galley, Id, LayerId, Plugin, Pos2, Rect, Response, Ui, + layers::ShapeIdx, text::CCursor, text_selection::CCursorRange, }; use super::{ @@ -123,65 +123,45 @@ impl Default for LabelSelectionState { } } -impl LabelSelectionState { - pub(crate) fn register(ctx: &Context) { - ctx.on_begin_pass("LabelSelectionState", std::sync::Arc::new(Self::begin_pass)); - ctx.on_end_pass("LabelSelectionState", std::sync::Arc::new(Self::end_pass)); - } - - pub fn load(ctx: &Context) -> Self { - let id = Id::new(ctx.viewport_id()); - ctx.data(|data| data.get_temp::(id)) - .unwrap_or_default() +impl Plugin for LabelSelectionState { + fn name(&self) -> &'static str { + "LabelSelectionState" } - pub fn store(self, ctx: &Context) { - let id = Id::new(ctx.viewport_id()); - ctx.data_mut(|data| { - data.insert_temp(id, self); - }); - } - - fn begin_pass(ctx: &Context) { - let mut state = Self::load(ctx); - + fn on_begin_pass(&mut self, ctx: &Context) { if ctx.input(|i| i.pointer.any_pressed() && !i.modifiers.shift) { // Maybe a new selection is about to begin, but the old one is over: // state.selection = None; // TODO(emilk): this makes sense, but doesn't work as expected. } - state.selection_bbox_last_frame = state.selection_bbox_this_frame; - state.selection_bbox_this_frame = Rect::NOTHING; + self.selection_bbox_last_frame = self.selection_bbox_this_frame; + self.selection_bbox_this_frame = Rect::NOTHING; - state.any_hovered = false; - state.has_reached_primary = false; - state.has_reached_secondary = false; - state.text_to_copy.clear(); - state.last_copied_galley_rect = None; - state.painted_selections.clear(); - - state.store(ctx); + self.any_hovered = false; + self.has_reached_primary = false; + self.has_reached_secondary = false; + self.text_to_copy.clear(); + self.last_copied_galley_rect = None; + self.painted_selections.clear(); } - fn end_pass(ctx: &Context) { - let mut state = Self::load(ctx); - - if state.is_dragging { + fn on_end_pass(&mut self, ctx: &Context) { + if self.is_dragging { ctx.set_cursor_icon(CursorIcon::Text); } - if !state.has_reached_primary || !state.has_reached_secondary { + if !self.has_reached_primary || !self.has_reached_secondary { // We didn't see both cursors this frame, // maybe because they are outside the visible area (scrolling), // or one disappeared. In either case we will have horrible glitches, so let's just deselect. - let prev_selection = state.selection.take(); + let prev_selection = self.selection.take(); if let Some(selection) = prev_selection { // This was the first frame of glitch, so hide the // glitching by removing all painted selections: ctx.graphics_mut(|layers| { if let Some(list) = layers.get_mut(selection.layer_id) { - for (shape_idx, row_selections) in state.painted_selections.drain(..) { + for (shape_idx, row_selections) in self.painted_selections.drain(..) { list.mutate_shape(shape_idx, |shape| { if let epaint::Shape::Text(text_shape) = &mut shape.shape { let galley = Arc::make_mut(&mut text_shape.galley); @@ -211,25 +191,25 @@ impl LabelSelectionState { } let pressed_escape = ctx.input(|i| i.key_pressed(crate::Key::Escape)); - let clicked_something_else = ctx.input(|i| i.pointer.any_pressed()) && !state.any_hovered; + let clicked_something_else = ctx.input(|i| i.pointer.any_pressed()) && !self.any_hovered; let delected_everything = pressed_escape || clicked_something_else; if delected_everything { - state.selection = None; + self.selection = None; } if ctx.input(|i| i.pointer.any_released()) { - state.is_dragging = false; + self.is_dragging = false; } - let text_to_copy = std::mem::take(&mut state.text_to_copy); + let text_to_copy = std::mem::take(&mut self.text_to_copy); if !text_to_copy.is_empty() { ctx.copy_text(text_to_copy); } - - state.store(ctx); } +} +impl LabelSelectionState { pub fn has_selection(&self) -> bool { self.selection.is_some() } @@ -297,7 +277,8 @@ impl LabelSelectionState { fallback_color: epaint::Color32, underline: epaint::Stroke, ) { - let mut state = Self::load(ui.ctx()); + let plugin = ui.ctx().plugin::(); + let mut state = plugin.lock(); let new_vertex_indices = state.on_label(ui, response, galley_pos, &mut galley); let shape_idx = ui.painter().add( @@ -309,8 +290,6 @@ impl LabelSelectionState { .painted_selections .push((shape_idx, new_vertex_indices)); } - - state.store(ui.ctx()); } fn cursor_for( From 4590770adf9e310de7d8a952227831a934ce09ba Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Jul 2025 16:39:03 +0200 Subject: [PATCH 12/22] Port remaining plugins --- crates/egui/src/context.rs | 4 +-- crates/egui/src/debug_text.rs | 59 ++++++++++++++------------------ crates/egui/src/drag_and_drop.rs | 59 ++++++++++++++------------------ 3 files changed, 53 insertions(+), 69 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 086077ca5b9..cdf5bf7a7df 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -736,9 +736,9 @@ impl Default for Context { ctx.add_plugin(crate::plugin::CallbackPlugin::default()); // Register built-in plugins: - crate::debug_text::register(&ctx); + ctx.add_plugin(crate::debug_text::DebugTextPlugin::default()); ctx.add_plugin(LabelSelectionState::default()); - crate::DragAndDrop::register(&ctx); + ctx.add_plugin(crate::DragAndDrop::default()); ctx } diff --git a/crates/egui/src/debug_text.rs b/crates/egui/src/debug_text.rs index 2cd1a2755a0..dbcc23b24c5 100644 --- a/crates/egui/src/debug_text.rs +++ b/crates/egui/src/debug_text.rs @@ -1,24 +1,14 @@ //! This is an example of how to create a plugin for egui. //! -//! A plugin usually consist of a struct that holds some state, -//! which is stored using [`Context::data_mut`]. -//! The plugin registers itself onto a specific [`Context`] -//! to get callbacks on certain events ([`Context::on_begin_pass`], [`Context::on_end_pass`]). +//! A plugin is a struct that implements the [`Plugin`] trait and holds some state. +//! The plugin is registered with the [`Context`] using [`Context::add_plugin`] +//! to get callbacks on certain events ([`Plugin::on_begin_pass`], [`Plugin::on_end_pass`]). use crate::{ - Align, Align2, Color32, Context, FontFamily, FontId, Id, Rect, Shape, Vec2, WidgetText, text, + Align, Align2, Color32, Context, FontFamily, FontId, Plugin, Rect, Shape, Vec2, WidgetText, + text, }; -/// Register this plugin on the given egui context, -/// so that it will be called every pass. -/// -/// This is a built-in plugin in egui, -/// meaning [`Context`] calls this from its `Default` implementation, -/// so this is marked as `pub(crate)`. -pub(crate) fn register(ctx: &Context) { - ctx.on_end_pass("debug_text", std::sync::Arc::new(State::end_pass)); -} - /// Print this text next to the cursor at the end of the pass. /// /// If you call this multiple times, the text will be appended. @@ -38,15 +28,12 @@ pub fn print(ctx: &Context, text: impl Into) { let location = std::panic::Location::caller(); let location = format!("{}:{}", location.file(), location.line()); - ctx.data_mut(|data| { - // We use `Id::NULL` as the id, since we only have one instance of this plugin. - // We use the `temp` version instead of `persisted` since we don't want to - // persist state on disk when the egui app is closed. - let state = data.get_temp_mut_or_default::(Id::NULL); - state.entries.push(Entry { - location, - text: text.into(), - }); + + let plugin = ctx.plugin::(); + let mut state = plugin.lock(); + state.entries.push(Entry { + location, + text: text.into(), }); } @@ -58,24 +45,28 @@ struct Entry { /// A plugin for easily showing debug-text on-screen. /// -/// This is a built-in plugin in egui. +/// This is a built-in plugin in egui, automatically registered during [`Context`] creation. #[derive(Clone, Default)] -struct State { +pub struct DebugTextPlugin { // This gets re-filled every pass. entries: Vec, } -impl State { - fn end_pass(ctx: &Context) { - let state = ctx.data_mut(|data| data.remove_temp::(Id::NULL)); - if let Some(state) = state { - state.paint(ctx); - } +impl Plugin for DebugTextPlugin { + fn name(&self) -> &'static str { + "DebugTextPlugin" } - fn paint(self, ctx: &Context) { - let Self { entries } = self; + fn on_end_pass(&mut self, ctx: &Context) { + if !self.entries.is_empty() { + let entries = std::mem::take(&mut self.entries); + Self::paint_entries(ctx, entries); + } + } +} +impl DebugTextPlugin { + fn paint_entries(ctx: &Context, entries: Vec) { if entries.is_empty() { return; } diff --git a/crates/egui/src/drag_and_drop.rs b/crates/egui/src/drag_and_drop.rs index 6caecc24b00..2873ed29398 100644 --- a/crates/egui/src/drag_and_drop.rs +++ b/crates/egui/src/drag_and_drop.rs @@ -1,36 +1,38 @@ use std::{any::Any, sync::Arc}; -use crate::{Context, CursorIcon, Id}; +use crate::{Context, CursorIcon, Plugin}; -/// Tracking of drag-and-drop payload. +/// Plugin for tracking drag-and-drop payload. /// -/// This is a low-level API. +/// This plugin stores the current drag-and-drop payload internally and handles +/// automatic cleanup when the drag operation ends (via Escape key or mouse release). /// -/// For a higher-level API, see: +/// This is a low-level API. For a higher-level API, see: /// - [`crate::Ui::dnd_drag_source`] /// - [`crate::Ui::dnd_drop_zone`] /// - [`crate::Response::dnd_set_drag_payload`] /// - [`crate::Response::dnd_hover_payload`] /// - [`crate::Response::dnd_release_payload`] /// +/// This is a built-in plugin in egui, automatically registered during [`Context`] creation. +/// /// See [this example](https://github.com/emilk/egui/blob/main/crates/egui_demo_lib/src/demo/drag_and_drop.rs). #[doc(alias = "drag and drop")] #[derive(Clone, Default)] pub struct DragAndDrop { - /// If set, something is currently being dragged + /// The current drag-and-drop payload, if any. Automatically cleared when drag ends. payload: Option>, } -impl DragAndDrop { - pub(crate) fn register(ctx: &Context) { - ctx.on_begin_pass("drag_and_drop_begin_pass", Arc::new(Self::begin_pass)); - ctx.on_end_pass("drag_and_drop_end_pass", Arc::new(Self::end_pass)); +impl Plugin for DragAndDrop { + fn name(&self) -> &'static str { + "DragAndDrop" } /// Interrupt drag-and-drop if the user presses the escape key. /// /// This needs to happen at frame start so we can properly capture the escape key. - fn begin_pass(ctx: &Context) { + fn on_begin_pass(&mut self, ctx: &Context) { let has_any_payload = Self::has_any_payload(ctx); if has_any_payload { @@ -48,7 +50,7 @@ impl DragAndDrop { /// This is a catch-all safety net in case user code doesn't capture the drag payload itself. /// This must happen at end-of-frame such that we don't shadow the mouse release event from user /// code. - fn end_pass(ctx: &Context) { + fn on_end_pass(&mut self, ctx: &Context) { let has_any_payload = Self::has_any_payload(ctx); if has_any_payload { @@ -67,7 +69,9 @@ impl DragAndDrop { } } } +} +impl DragAndDrop { /// Set a drag-and-drop payload. /// /// This can be read by [`Self::payload`] until the pointer is released. @@ -75,18 +79,12 @@ impl DragAndDrop { where Payload: Any + Send + Sync, { - ctx.data_mut(|data| { - let state = data.get_temp_mut_or_default::(Id::NULL); - state.payload = Some(Arc::new(payload)); - }); + ctx.plugin::().lock().payload = Some(Arc::new(payload)); } /// Clears the payload, setting it to `None`. pub fn clear_payload(ctx: &Context) { - ctx.data_mut(|data| { - let state = data.get_temp_mut_or_default::(Id::NULL); - state.payload = None; - }); + ctx.plugin::().lock().payload = None; } /// Retrieve the payload, if any. @@ -99,11 +97,13 @@ impl DragAndDrop { where Payload: Any + Send + Sync, { - ctx.data(|data| { - let state = data.get_temp::(Id::NULL)?; - let payload = state.payload?; - payload.downcast().ok() - }) + ctx.plugin::() + .lock() + .payload + .as_ref()? + .clone() + .downcast() + .ok() } /// Retrieve and clear the payload, if any. @@ -116,11 +116,7 @@ impl DragAndDrop { where Payload: Any + Send + Sync, { - ctx.data_mut(|data| { - let state = data.get_temp_mut_or_default::(Id::NULL); - let payload = state.payload.take()?; - payload.downcast().ok() - }) + ctx.plugin::().lock().payload.take()?.downcast().ok() } /// Are we carrying a payload of the given type? @@ -139,9 +135,6 @@ impl DragAndDrop { /// Returns `true` both during a drag and on the frame the pointer is released /// (if there is a payload). pub fn has_any_payload(ctx: &Context) -> bool { - ctx.data(|data| { - let state = data.get_temp::(Id::NULL); - state.is_some_and(|state| state.payload.is_some()) - }) + ctx.plugin::().lock().payload.is_some() } } From 8a5317d9f3f1120455f8b1bd8c7a6f7fae05dd86 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Jul 2025 17:17:34 +0200 Subject: [PATCH 13/22] Clippy --- crates/egui/src/plugin.rs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/crates/egui/src/plugin.rs b/crates/egui/src/plugin.rs index c81138cbf4e..a9524d7098d 100644 --- a/crates/egui/src/plugin.rs +++ b/crates/egui/src/plugin.rs @@ -46,7 +46,6 @@ pub trait Plugin: Send + Sync + 'static { pub(crate) struct PluginHandle { plugin: Box, - get_plugin: fn(&Self) -> &dyn Plugin, get_plugin_mut: fn(&mut Self) -> &mut dyn Plugin, } @@ -96,10 +95,6 @@ impl PluginHandle { pub fn new(plugin: P) -> Arc> { Arc::new(Mutex::new(Self { plugin: Box::new(plugin), - get_plugin: |handle| { - let plugin: &P = handle.typed_plugin(); - plugin as &dyn Plugin - }, get_plugin_mut: |handle| { let plugin: &mut P = handle.typed_plugin_mut(); plugin as &mut dyn Plugin @@ -111,10 +106,6 @@ impl PluginHandle { (*self.plugin).type_id() } - fn dyn_plugin(&self) -> &dyn Plugin { - (self.get_plugin)(self) - } - pub fn dyn_plugin_mut(&mut self) -> &mut dyn Plugin { (self.get_plugin_mut)(self) } @@ -149,7 +140,7 @@ impl PluginsOrdered { { for plugin in &self.0 { let mut plugin = plugin.lock(); - profiling::scope!("plugin", plugin.dyn_plugin().name()); + profiling::scope!("plugin", plugin.dyn_plugin_mut().name()); f(plugin.dyn_plugin_mut()); } } @@ -229,8 +220,8 @@ impl Plugin for CallbackPlugin { fn on_begin_pass(&mut self, ctx: &Context) { profiling::function_scope!(); - for (debug_name, cb) in &self.on_begin_plugins { - profiling::scope!(*debug_name); + for (_debug_name, cb) in &self.on_begin_plugins { + profiling::scope!(*_debug_name); (cb)(ctx); } } @@ -238,8 +229,8 @@ impl Plugin for CallbackPlugin { fn on_end_pass(&mut self, ctx: &Context) { profiling::function_scope!(); - for (debug_name, cb) in &self.on_end_plugins { - profiling::scope!(*debug_name); + for (_debug_name, cb) in &self.on_end_plugins { + profiling::scope!(*_debug_name); (cb)(ctx); } } From d72323f48ec82acddd7336c47354914fbf0219f1 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Jul 2025 17:27:03 +0200 Subject: [PATCH 14/22] Fix deadlock --- crates/egui/src/drag_and_drop.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/drag_and_drop.rs b/crates/egui/src/drag_and_drop.rs index 2873ed29398..014f042225b 100644 --- a/crates/egui/src/drag_and_drop.rs +++ b/crates/egui/src/drag_and_drop.rs @@ -33,7 +33,7 @@ impl Plugin for DragAndDrop { /// /// This needs to happen at frame start so we can properly capture the escape key. fn on_begin_pass(&mut self, ctx: &Context) { - let has_any_payload = Self::has_any_payload(ctx); + let has_any_payload = self.payload.is_some(); if has_any_payload { let abort_dnd_due_to_escape_key = @@ -51,7 +51,7 @@ impl Plugin for DragAndDrop { /// This must happen at end-of-frame such that we don't shadow the mouse release event from user /// code. fn on_end_pass(&mut self, ctx: &Context) { - let has_any_payload = Self::has_any_payload(ctx); + let has_any_payload = self.payload.is_some(); if has_any_payload { let abort_dnd_due_to_mouse_release = ctx.input_mut(|i| i.pointer.any_released()); From dcf52b27e360bac3bcaee38de0c5b78e4033619c Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Jul 2025 10:49:08 +0200 Subject: [PATCH 15/22] Make second side panel a bottom panel --- .../src/accessibility_inspector.rs | 123 ++++++++++-------- 1 file changed, 66 insertions(+), 57 deletions(-) diff --git a/crates/egui_demo_app/src/accessibility_inspector.rs b/crates/egui_demo_app/src/accessibility_inspector.rs index ffcc9fa9b3b..d4f4accf986 100644 --- a/crates/egui_demo_app/src/accessibility_inspector.rs +++ b/crates/egui_demo_app/src/accessibility_inspector.rs @@ -4,8 +4,9 @@ use eframe::emath::Align; use eframe::epaint::text::TextWrapMode; use egui::collapsing_header::CollapsingState; use egui::{ - Button, CollapsingHeader, Color32, Context, Event, FullOutput, Id, Key, KeyboardShortcut, - Layout, Modifiers, Pos2, RawInput, Rect, ScrollArea, SidePanel, Ui, + Button, CollapsingHeader, Color32, Context, Event, Frame, FullOutput, Id, Key, + KeyboardShortcut, Layout, Modifiers, Pos2, RawInput, Rect, ScrollArea, SidePanel, + TopBottomPanel, Ui, }; use std::mem; @@ -68,59 +69,73 @@ impl egui::Plugin for AccessibilityInspectorPlugin { if self.open { ctx.enable_accesskit(); - if let Some(selected_node) = &self.selected_node { - SidePanel::right(Self::id().with("details")).show(ctx, |ui| { - if let Some(tree) = &self.tree { - if let Some(node) = - tree.state().node_by_id(NodeId::from(selected_node.value())) - { - let node_response = ui.ctx().read_response(*selected_node); - - if let Some(widget_response) = node_response { - ui.ctx().debug_painter().debug_rect( - widget_response.rect, - ui.style_mut().visuals.selection.bg_fill, - "", - ); - } - - ui.label(format!("Node ID: {:?}", selected_node)); - ui.label(format!("Role: {:?}", node.role())); - ui.label(format!("Label: {}", node.label().unwrap_or_default())); - ui.label(format!("Value: {}", node.value().unwrap_or_default())); - ui.label(format!("Children Count: {}", node.children().len())); - - ui.label("Supported Actions:"); - for action_n in 0..50 { - let action = Action::n(action_n); - let Some(action) = action else { - break; - }; - if node.supports_action(action) { - if ui.button(format!("{:?}", action)).clicked() { - let action_request = ActionRequest { - target: node.id(), - action, - data: None, - }; - self.queued_action = Some(action_request); - } - } - } - } else { - ui.label("Node not found"); - } - } else { - ui.label("No tree data available"); - } - }); - } SidePanel::right(Self::id()).show(ctx, |ui| { - ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate); let response = ui.heading("🔎 AccessKit Inspector"); - ui.separator(); + ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate); ctx.with_accessibility_parent(response.id, || { + if let Some(selected_node) = &self.selected_node { + TopBottomPanel::bottom(Self::id().with("details_panel")) + .frame(Frame::new()) + .show_separator_line(false) + .show_inside(ui, |ui| { + ui.separator(); + + if let Some(tree) = &self.tree { + if let Some(node) = + tree.state().node_by_id(NodeId::from(selected_node.value())) + { + let node_response = ui.ctx().read_response(*selected_node); + + if let Some(widget_response) = node_response { + ui.ctx().debug_painter().debug_rect( + widget_response.rect, + ui.style_mut().visuals.selection.bg_fill, + "", + ); + } + + ui.label(format!("Node ID: {:?}", selected_node)); + ui.label(format!("Role: {:?}", node.role())); + ui.label(format!( + "Label: {}", + node.label().unwrap_or_default() + )); + ui.label(format!( + "Value: {}", + node.value().unwrap_or_default() + )); + ui.label(format!( + "Children Count: {}", + node.children().len() + )); + + ui.label("Supported Actions:"); + for action_n in 0..50 { + let action = Action::n(action_n); + let Some(action) = action else { + break; + }; + if node.supports_action(action) { + if ui.button(format!("{:?}", action)).clicked() { + let action_request = ActionRequest { + target: node.id(), + action, + data: None, + }; + self.queued_action = Some(action_request); + } + } + } + } else { + ui.label("Node not found"); + } + } else { + ui.label("No tree data available"); + } + }); + } + ScrollArea::vertical().show(ui, |ui| { if let Some(tree) = &self.tree { Self::node_ui(ui, &tree.state().root(), &mut self.selected_node); @@ -155,12 +170,6 @@ impl AccessibilityInspectorPlugin { let node_id = Id::from_value(node.id().0.try_into().unwrap()); ui.push_id(node.id(), |ui| { - // let response = CollapsingHeader::new(label.clone()).show(ui, |ui| { - // node.children().for_each(|c| { - // Self::node_ui(ui, &c); - // }) - // }); - let child_count = node.children().len(); let has_children = child_count > 0; let default_open = child_count == 1 && node.role() != accesskit::Role::Label; From 8b2104f193bd70046749b4582527602a2bd15aea Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Jul 2025 10:53:13 +0200 Subject: [PATCH 16/22] Grid --- .../src/accessibility_inspector.rs | 48 ++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/crates/egui_demo_app/src/accessibility_inspector.rs b/crates/egui_demo_app/src/accessibility_inspector.rs index d4f4accf986..9f259b9469c 100644 --- a/crates/egui_demo_app/src/accessibility_inspector.rs +++ b/crates/egui_demo_app/src/accessibility_inspector.rs @@ -5,7 +5,7 @@ use eframe::epaint::text::TextWrapMode; use egui::collapsing_header::CollapsingState; use egui::{ Button, CollapsingHeader, Color32, Context, Event, Frame, FullOutput, Id, Key, - KeyboardShortcut, Layout, Modifiers, Pos2, RawInput, Rect, ScrollArea, SidePanel, + KeyboardShortcut, Label, Layout, Modifiers, Pos2, RawInput, Rect, ScrollArea, SidePanel, TopBottomPanel, Ui, }; use std::mem; @@ -72,7 +72,6 @@ impl egui::Plugin for AccessibilityInspectorPlugin { SidePanel::right(Self::id()).show(ctx, |ui| { let response = ui.heading("🔎 AccessKit Inspector"); - ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate); ctx.with_accessibility_parent(response.id, || { if let Some(selected_node) = &self.selected_node { TopBottomPanel::bottom(Self::id().with("details_panel")) @@ -95,20 +94,36 @@ impl egui::Plugin for AccessibilityInspectorPlugin { ); } - ui.label(format!("Node ID: {:?}", selected_node)); - ui.label(format!("Role: {:?}", node.role())); - ui.label(format!( - "Label: {}", - node.label().unwrap_or_default() - )); - ui.label(format!( - "Value: {}", - node.value().unwrap_or_default() - )); - ui.label(format!( - "Children Count: {}", - node.children().len() - )); + egui::Grid::new("node_details_grid").num_columns(2).show( + ui, + |ui| { + ui.label("Node ID:"); + ui.label(format!("{:?}", selected_node)); + ui.end_row(); + + ui.label("Role:"); + ui.label(format!("{:?}", node.role())); + ui.end_row(); + + ui.label("Label:"); + ui.add( + Label::new(node.label().unwrap_or_default()) + .truncate(), + ); + ui.end_row(); + + ui.label("Value:"); + ui.add( + Label::new(node.value().unwrap_or_default()) + .truncate(), + ); + ui.end_row(); + + ui.label("Children:"); + ui.label(format!("{}", node.children().len())); + ui.end_row(); + }, + ); ui.label("Supported Actions:"); for action_n in 0..50 { @@ -136,6 +151,7 @@ impl egui::Plugin for AccessibilityInspectorPlugin { }); } + ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate); ScrollArea::vertical().show(ui, |ui| { if let Some(tree) = &self.tree { Self::node_ui(ui, &tree.state().root(), &mut self.selected_node); From bb9ffc8c48aee8ed1a7c8f326926d4edd725dd25 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Jul 2025 11:38:44 +0200 Subject: [PATCH 17/22] Small ui improvements --- .../src/accessibility_inspector.rs | 75 ++++++++++++------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/crates/egui_demo_app/src/accessibility_inspector.rs b/crates/egui_demo_app/src/accessibility_inspector.rs index 9f259b9469c..f09130262ce 100644 --- a/crates/egui_demo_app/src/accessibility_inspector.rs +++ b/crates/egui_demo_app/src/accessibility_inspector.rs @@ -5,8 +5,8 @@ use eframe::epaint::text::TextWrapMode; use egui::collapsing_header::CollapsingState; use egui::{ Button, CollapsingHeader, Color32, Context, Event, Frame, FullOutput, Id, Key, - KeyboardShortcut, Label, Layout, Modifiers, Pos2, RawInput, Rect, ScrollArea, SidePanel, - TopBottomPanel, Ui, + KeyboardShortcut, Label, Layout, Modifiers, Pos2, RawInput, Rect, RichText, ScrollArea, + SidePanel, TopBottomPanel, Ui, }; use std::mem; @@ -98,50 +98,69 @@ impl egui::Plugin for AccessibilityInspectorPlugin { ui, |ui| { ui.label("Node ID:"); - ui.label(format!("{:?}", selected_node)); + ui.strong(format!("{:?}", selected_node)); ui.end_row(); ui.label("Role:"); - ui.label(format!("{:?}", node.role())); + ui.strong(format!("{:?}", node.role())); ui.end_row(); ui.label("Label:"); ui.add( - Label::new(node.label().unwrap_or_default()) - .truncate(), + Label::new( + RichText::new( + node.label().unwrap_or_default(), + ) + .strong(), + ) + .truncate(), ); ui.end_row(); ui.label("Value:"); ui.add( - Label::new(node.value().unwrap_or_default()) - .truncate(), + Label::new( + RichText::new( + node.value().unwrap_or_default(), + ) + .strong(), + ) + .truncate(), ); ui.end_row(); ui.label("Children:"); - ui.label(format!("{}", node.children().len())); + ui.label( + RichText::new(format!( + "{}", + node.children().len() + )) + .strong(), + ); ui.end_row(); }, ); - ui.label("Supported Actions:"); - for action_n in 0..50 { - let action = Action::n(action_n); - let Some(action) = action else { - break; - }; - if node.supports_action(action) { - if ui.button(format!("{:?}", action)).clicked() { - let action_request = ActionRequest { - target: node.id(), - action, - data: None, - }; - self.queued_action = Some(action_request); + ui.label("Actions:"); + ui.horizontal_wrapped(|ui| { + for action_n in 0..50 { + let action = Action::n(action_n); + let Some(action) = action else { + break; + }; + if node.supports_action(action) { + if ui.button(format!("{:?}", action)).clicked() + { + let action_request = ActionRequest { + target: node.id(), + action, + data: None, + }; + self.queued_action = Some(action_request); + } } } - } + }); } else { ui.label("Node not found"); } @@ -214,11 +233,9 @@ impl AccessibilityInspectorPlugin { let widget_response = ui.ctx().read_response(node_id); if let Some(widget_response) = widget_response { - ui.ctx().debug_painter().debug_rect( - widget_response.rect, - Color32::RED, - label, - ); + ui.ctx() + .debug_painter() + .debug_rect(widget_response.rect, Color32::RED, ""); } } }); From 0033e45a3366d9e457bb216c4d2588e9cf302a6f Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Jul 2025 15:06:54 +0200 Subject: [PATCH 18/22] Clippy --- crates/egui_demo_app/Cargo.toml | 5 ++- .../src/accessibility_inspector.rs | 41 +++++++++---------- crates/egui_demo_app/src/lib.rs | 2 +- crates/egui_demo_app/src/wrap_app.rs | 4 +- 4 files changed, 25 insertions(+), 27 deletions(-) diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 4d218708a12..1193b289d10 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -39,6 +39,7 @@ persistence = [ puffin = ["dep:puffin", "dep:puffin_http", "profiling/profile-with-puffin"] serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"] syntect = ["egui_demo_lib/syntect"] +accessibility_inspector = ["dep:accesskit", "dep:accesskit_consumer", "eframe/accesskit"] glow = ["eframe/glow"] wgpu = ["eframe/wgpu", "bytemuck", "dep:wgpu"] @@ -64,8 +65,8 @@ log.workspace = true profiling.workspace = true # Optional dependencies: -accesskit = "0.19.0" -accesskit_consumer = "0.28.0" +accesskit = { version = "0.19.0", optional = true } +accesskit_consumer = { version = "0.28.0", optional = true } bytemuck = { workspace = true, optional = true } puffin = { workspace = true, optional = true } diff --git a/crates/egui_demo_app/src/accessibility_inspector.rs b/crates/egui_demo_app/src/accessibility_inspector.rs index f09130262ce..2386ef09b25 100644 --- a/crates/egui_demo_app/src/accessibility_inspector.rs +++ b/crates/egui_demo_app/src/accessibility_inspector.rs @@ -1,12 +1,10 @@ use accesskit::{Action, ActionRequest, NodeId}; use accesskit_consumer::{Node, Tree, TreeChangeHandler}; -use eframe::emath::Align; use eframe::epaint::text::TextWrapMode; use egui::collapsing_header::CollapsingState; use egui::{ - Button, CollapsingHeader, Color32, Context, Event, Frame, FullOutput, Id, Key, - KeyboardShortcut, Label, Layout, Modifiers, Pos2, RawInput, Rect, RichText, ScrollArea, - SidePanel, TopBottomPanel, Ui, + Button, Color32, Context, Event, Frame, FullOutput, Id, Key, KeyboardShortcut, Label, + Modifiers, RawInput, RichText, ScrollArea, SidePanel, TopBottomPanel, Ui, }; use std::mem; @@ -21,10 +19,10 @@ pub struct AccessibilityInspectorPlugin { struct ChangeHandler; impl TreeChangeHandler for ChangeHandler { - fn node_added(&mut self, node: &Node) {} - fn node_updated(&mut self, old_node: &Node, new_node: &Node) {} - fn focus_moved(&mut self, old_node: Option<&Node>, new_node: Option<&Node>) {} - fn node_removed(&mut self, node: &Node) {} + fn node_added(&mut self, _node: &Node<'_>) {} + fn node_updated(&mut self, _old_node: &Node<'_>, _new_node: &Node<'_>) {} + fn focus_moved(&mut self, _old_node: Option<&Node<'_>>, _new_node: Option<&Node<'_>>) {} + fn node_removed(&mut self, _node: &Node<'_>) {} } impl egui::Plugin for AccessibilityInspectorPlugin { @@ -98,7 +96,7 @@ impl egui::Plugin for AccessibilityInspectorPlugin { ui, |ui| { ui.label("Node ID:"); - ui.strong(format!("{:?}", selected_node)); + ui.strong(format!("{selected_node:?}")); ui.end_row(); ui.label("Role:"); @@ -148,16 +146,15 @@ impl egui::Plugin for AccessibilityInspectorPlugin { let Some(action) = action else { break; }; - if node.supports_action(action) { - if ui.button(format!("{:?}", action)).clicked() - { - let action_request = ActionRequest { - target: node.id(), - action, - data: None, - }; - self.queued_action = Some(action_request); - } + if node.supports_action(action) + && ui.button(format!("{action:?}")).clicked() + { + let action_request = ActionRequest { + target: node.id(), + action, + data: None, + }; + self.queued_action = Some(action_request); } } }); @@ -187,7 +184,7 @@ impl AccessibilityInspectorPlugin { Id::new("Accessibility Inspector") } - fn node_ui(ui: &mut Ui, node: &Node, selected_node: &mut Option) { + fn node_ui(ui: &mut Ui, node: &Node<'_>, selected_node: &mut Option) { if node.id() == Self::id().value().into() || node .value() @@ -202,7 +199,7 @@ impl AccessibilityInspectorPlugin { .unwrap_or(node.id().0.to_string()); let label = format!("({:?}) {}", node.role(), label); - let node_id = Id::from_value(node.id().0.try_into().unwrap()); + let node_id = Id::from_value(node.id().0); ui.push_id(node.id(), |ui| { let child_count = node.children().len(); @@ -244,7 +241,7 @@ impl AccessibilityInspectorPlugin { collapsing.show_body_indented(&header_response.response, ui, |ui| { node.children().for_each(|c| { Self::node_ui(ui, &c, selected_node); - }) + }); }); } diff --git a/crates/egui_demo_app/src/lib.rs b/crates/egui_demo_app/src/lib.rs index d97349bf1b0..c2d523816f9 100644 --- a/crates/egui_demo_app/src/lib.rs +++ b/crates/egui_demo_app/src/lib.rs @@ -16,7 +16,7 @@ pub(crate) fn seconds_since_midnight() -> f64 { } // ---------------------------------------------------------------------------- - +#[cfg(feature = "accessibility_inspector")] mod accessibility_inspector; #[cfg(target_arch = "wasm32")] mod web; diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index d636fcf62a1..3df454c03a3 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -3,7 +3,6 @@ use egui_demo_lib::is_mobile; #[cfg(feature = "glow")] use eframe::glow; -use crate::accessibility_inspector::AccessibilityInspectorPlugin; #[cfg(target_arch = "wasm32")] use core::any::Any; @@ -188,8 +187,9 @@ impl WrapApp { pub fn new(cc: &eframe::CreationContext<'_>) -> Self { // This gives us image support: egui_extras::install_image_loaders(&cc.egui_ctx); + #[cfg(feature = "accessibility_inspector")] cc.egui_ctx - .add_plugin(AccessibilityInspectorPlugin::default()); + .add_plugin(crate::accessibility_inspector::AccessibilityInspectorPlugin::default()); #[allow(unused_mut, clippy::allow_attributes)] let mut slf = Self { From 1fc9f1b904e9309392f2822dae6d4a4211f493f3 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Jul 2025 15:10:12 +0200 Subject: [PATCH 19/22] Lint --- crates/egui_demo_app/src/accessibility_inspector.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/egui_demo_app/src/accessibility_inspector.rs b/crates/egui_demo_app/src/accessibility_inspector.rs index 2386ef09b25..874aeffdc26 100644 --- a/crates/egui_demo_app/src/accessibility_inspector.rs +++ b/crates/egui_demo_app/src/accessibility_inspector.rs @@ -20,8 +20,11 @@ struct ChangeHandler; impl TreeChangeHandler for ChangeHandler { fn node_added(&mut self, _node: &Node<'_>) {} + fn node_updated(&mut self, _old_node: &Node<'_>, _new_node: &Node<'_>) {} + fn focus_moved(&mut self, _old_node: Option<&Node<'_>>, _new_node: Option<&Node<'_>>) {} + fn node_removed(&mut self, _node: &Node<'_>) {} } From 5ce5b7f36c1032c51d7bb3083dd769ec017b6c28 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Mon, 28 Jul 2025 12:40:20 +0200 Subject: [PATCH 20/22] Fix dnd deadlock --- crates/egui/src/drag_and_drop.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/drag_and_drop.rs b/crates/egui/src/drag_and_drop.rs index 014f042225b..148cf169ead 100644 --- a/crates/egui/src/drag_and_drop.rs +++ b/crates/egui/src/drag_and_drop.rs @@ -40,7 +40,7 @@ impl Plugin for DragAndDrop { ctx.input_mut(|i| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape)); if abort_dnd_due_to_escape_key { - Self::clear_payload(ctx); + self.payload = None; } } } @@ -57,7 +57,7 @@ impl Plugin for DragAndDrop { let abort_dnd_due_to_mouse_release = ctx.input_mut(|i| i.pointer.any_released()); if abort_dnd_due_to_mouse_release { - Self::clear_payload(ctx); + self.payload = None; } else { // We set the cursor icon only if its default, as the user code might have // explicitly set it already. From c857fb949162ca96d68ff5883a85ce6bb6f2d3cb Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 7 Aug 2025 12:11:21 +0200 Subject: [PATCH 21/22] Fixes from review --- crates/egui/src/context.rs | 6 ++---- crates/egui/src/debug_text.rs | 8 +++----- crates/egui/src/drag_and_drop.rs | 2 +- crates/egui/src/id.rs | 4 ---- crates/egui/src/plugin.rs | 18 +++++++++--------- .../src/text_selection/label_text_selection.rs | 2 +- 6 files changed, 16 insertions(+), 24 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index cdf5bf7a7df..cc5bf2012c0 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1836,8 +1836,7 @@ impl Context { impl Context { /// Call the given callback at the start of each pass of each viewport. /// - /// This can be used for egui _plugins_. - /// See [`crate::debug_text`] for an example. + /// This is a convenience wrapper around [`Self::add_plugin`]. pub fn on_begin_pass(&self, debug_name: &'static str, cb: plugin::ContextCallback) { self.with_plugin(|p: &mut crate::plugin::CallbackPlugin| { p.on_begin_plugins.push((debug_name, cb)); @@ -1846,8 +1845,7 @@ impl Context { /// Call the given callback at the end of each pass of each viewport. /// - /// This can be used for egui _plugins_. - /// See [`crate::debug_text`] for an example. + /// This is a convenience wrapper around [`Self::add_plugin`]. pub fn on_end_pass(&self, debug_name: &'static str, cb: plugin::ContextCallback) { self.with_plugin(|p: &mut crate::plugin::CallbackPlugin| { p.on_end_plugins.push((debug_name, cb)); diff --git a/crates/egui/src/debug_text.rs b/crates/egui/src/debug_text.rs index dbcc23b24c5..c5fe043fe4e 100644 --- a/crates/egui/src/debug_text.rs +++ b/crates/egui/src/debug_text.rs @@ -53,15 +53,13 @@ pub struct DebugTextPlugin { } impl Plugin for DebugTextPlugin { - fn name(&self) -> &'static str { + fn debug_name(&self) -> &'static str { "DebugTextPlugin" } fn on_end_pass(&mut self, ctx: &Context) { - if !self.entries.is_empty() { - let entries = std::mem::take(&mut self.entries); - Self::paint_entries(ctx, entries); - } + let entries = std::mem::take(&mut self.entries); + Self::paint_entries(ctx, entries); } } diff --git a/crates/egui/src/drag_and_drop.rs b/crates/egui/src/drag_and_drop.rs index 148cf169ead..21468ed46db 100644 --- a/crates/egui/src/drag_and_drop.rs +++ b/crates/egui/src/drag_and_drop.rs @@ -25,7 +25,7 @@ pub struct DragAndDrop { } impl Plugin for DragAndDrop { - fn name(&self) -> &'static str { + fn debug_name(&self) -> &'static str { "DragAndDrop" } diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index c55f67c5d3e..7bcef8dc220 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -79,10 +79,6 @@ impl Id { self.0.get() } - pub fn from_value(value: u64) -> Self { - Self::from_hash(value) - } - #[cfg(feature = "accesskit")] pub(crate) fn accesskit_id(&self) -> accesskit::NodeId { self.value().into() diff --git a/crates/egui/src/plugin.rs b/crates/egui/src/plugin.rs index a9524d7098d..32f9c6ef65a 100644 --- a/crates/egui/src/plugin.rs +++ b/crates/egui/src/plugin.rs @@ -14,7 +14,7 @@ pub trait Plugin: Send + Sync + 'static { /// Plugin name. /// /// Used when profiling. - fn name(&self) -> &'static str; + fn debug_name(&self) -> &'static str; /// Called once, when the plugin is registered. /// @@ -31,17 +31,17 @@ pub trait Plugin: Send + Sync + 'static { /// Can be used to show ui, e.g. a [`crate::Window`]. fn on_end_pass(&mut self, ctx: &Context) {} - /// 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. - fn output_hook(&mut self, output: &mut FullOutput) {} - /// 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. 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. + fn output_hook(&mut self, output: &mut FullOutput) {} } pub(crate) struct PluginHandle { @@ -140,7 +140,7 @@ impl PluginsOrdered { { for plugin in &self.0 { let mut plugin = plugin.lock(); - profiling::scope!("plugin", plugin.dyn_plugin_mut().name()); + profiling::scope!("plugin", plugin.dyn_plugin_mut().debug_name()); f(plugin.dyn_plugin_mut()); } } @@ -213,7 +213,7 @@ pub(crate) struct CallbackPlugin { } impl Plugin for CallbackPlugin { - fn name(&self) -> &'static str { + fn debug_name(&self) -> &'static str { "CallbackPlugins" } diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index 16f1bfcff93..da248a0f5cb 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -124,7 +124,7 @@ impl Default for LabelSelectionState { } impl Plugin for LabelSelectionState { - fn name(&self) -> &'static str { + fn debug_name(&self) -> &'static str { "LabelSelectionState" } From da3d65db272ab6f7b393c44699b5764c5bd844ee Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 7 Aug 2025 12:35:52 +0200 Subject: [PATCH 22/22] Add `Id::from_high_entropy_bits` --- crates/egui/src/id.rs | 19 +++++++++++++++++++ .../src/accessibility_inspector.rs | 8 +++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index 7bcef8dc220..cce99dd230b 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -83,6 +83,25 @@ impl Id { pub(crate) fn accesskit_id(&self) -> accesskit::NodeId { self.value().into() } + + /// Create a new [`Id`] from a high-entropy value. No hashing is done. + /// + /// This can be useful if you have an [`Id`] that was converted to some other type + /// (e.g. accesskit::NodeId) and you want to convert it back to an [`Id`]. + /// + /// # Safety + /// You need to ensure that the value is high-entropy since it might be used in + /// a [`IdSet`] or [`IdMap`], which rely on the assumption that [`Id`]s have good entropy. + /// + /// The method is not unsafe in terms of memory safety. + /// + /// # Panics + /// If the value is zero, this will panic. + #[doc(hidden)] + #[allow(unsafe_code)] + pub unsafe fn from_high_entropy_bits(value: u64) -> Self { + Self(NonZeroU64::new(value).expect("Id must be non-zero."), ) + } } impl std::fmt::Debug for Id { diff --git a/crates/egui_demo_app/src/accessibility_inspector.rs b/crates/egui_demo_app/src/accessibility_inspector.rs index 874aeffdc26..505ebdced30 100644 --- a/crates/egui_demo_app/src/accessibility_inspector.rs +++ b/crates/egui_demo_app/src/accessibility_inspector.rs @@ -29,7 +29,7 @@ impl TreeChangeHandler for ChangeHandler { } impl egui::Plugin for AccessibilityInspectorPlugin { - fn name(&self) -> &'static str { + fn debug_name(&self) -> &'static str { "Accessibility Inspector" } @@ -202,7 +202,9 @@ impl AccessibilityInspectorPlugin { .unwrap_or(node.id().0.to_string()); let label = format!("({:?}) {}", node.role(), label); - let node_id = Id::from_value(node.id().0); + // Safety: This is safe since the `accesskit::NodeId` was created from an `egui::Id`. + #[expect(unsafe_code)] + let egui_node_id = unsafe {Id::from_high_entropy_bits(node.id().0)}; ui.push_id(node.id(), |ui| { let child_count = node.children().len(); @@ -226,7 +228,7 @@ impl AccessibilityInspectorPlugin { }; let label_response = ui.selectable_value( selected_node, - Some(Id::from_value(node.id().0)), + Some(egui_node_id), label.clone(), ); if label_response.hovered() {