From 68e45301cbf3b99c76c3018bedb58f45a37e8236 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 16 Jul 2025 16:41:39 +0200 Subject: [PATCH 01/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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 593efaf97eff4c2f2447036703954e4c26ac141a Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 17 Jul 2025 16:39:03 +0200 Subject: [PATCH 08/47] Group accessibility nodes by Ui --- crates/egui/src/containers/area.rs | 1 + crates/egui/src/containers/window.rs | 191 +++++++++--------- crates/egui/src/context.rs | 149 ++++++++++---- crates/egui/src/data/output.rs | 1 + crates/egui/src/pass_state.rs | 10 +- crates/egui/src/response.rs | 1 + .../egui/src/text_selection/accesskit_text.rs | 101 +++++---- crates/egui/src/ui.rs | 32 ++- crates/egui/src/ui_builder.rs | 13 ++ .../src/accessibility_inspector.rs | 12 +- 10 files changed, 315 insertions(+), 196 deletions(-) diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index d3d2a722821..0eb09811039 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -594,6 +594,7 @@ impl Prepared { .layer_id(self.layer_id) .max_rect(max_rect) .layout(self.layout) + .accessibility_parent(self.move_response.id) .closable(); if !self.enabled { diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index e93b046e589..59b8fb17943 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -505,15 +505,14 @@ impl Window<'_> { // First check for resize to avoid frame delay: let last_frame_outer_rect = area.state().rect(); - let resize_interaction = ctx.with_accessibility_parent(area.id(), || { - resize_interaction( - ctx, - possible, - area_layer_id, - last_frame_outer_rect, - window_frame, - ) - }); + let resize_interaction = resize_interaction( + ctx, + possible, + area.id(), + area_layer_id, + last_frame_outer_rect, + window_frame, + ); { let margins = window_frame.total_margin().sum() @@ -538,109 +537,107 @@ impl Window<'_> { } let content_inner = { - ctx.with_accessibility_parent(area.id(), || { - // BEGIN FRAME -------------------------------- - let mut frame = window_frame.begin(&mut area_content_ui); - - let show_close_button = open.is_some(); - - let where_to_put_header_background = &area_content_ui.painter().add(Shape::Noop); - - let title_bar = if with_title_bar { - let title_bar = TitleBar::new( - &frame.content_ui, - title, - show_close_button, - collapsible, - window_frame, - title_bar_height_with_margin, - ); - resize.min_size.x = resize.min_size.x.at_least(title_bar.inner_rect.width()); // Prevent making window smaller than title bar width - - frame.content_ui.set_min_size(title_bar.inner_rect.size()); - - // Skip the title bar (and separator): - if is_collapsed { - frame.content_ui.add_space(title_bar.inner_rect.height()); - } else { - frame.content_ui.add_space( - title_bar.inner_rect.height() - + title_content_spacing - + window_frame.inner_margin.sum().y, - ); - } + // BEGIN FRAME -------------------------------- + let mut frame = window_frame.begin(&mut area_content_ui); - Some(title_bar) - } else { - None - }; + let show_close_button = open.is_some(); - let (content_inner, content_response) = collapsing - .show_body_unindented(&mut frame.content_ui, |ui| { - resize.show(ui, |ui| { - if scroll.is_any_scroll_enabled() { - scroll.show(ui, add_contents).inner - } else { - add_contents(ui) - } - }) - }) - .map_or((None, None), |ir| (Some(ir.inner), Some(ir.response))); - - let outer_rect = frame.end(&mut area_content_ui).rect; - paint_resize_corner( - &area_content_ui, - &possible, - outer_rect, - &window_frame, - resize_interaction, + let where_to_put_header_background = &area_content_ui.painter().add(Shape::Noop); + + let title_bar = if with_title_bar { + let title_bar = TitleBar::new( + &frame.content_ui, + title, + show_close_button, + collapsible, + window_frame, + title_bar_height_with_margin, ); + resize.min_size.x = resize.min_size.x.at_least(title_bar.inner_rect.width()); // Prevent making window smaller than title bar width - // END FRAME -------------------------------- + frame.content_ui.set_min_size(title_bar.inner_rect.size()); - if let Some(mut title_bar) = title_bar { - title_bar.inner_rect = outer_rect.shrink(window_frame.stroke.width); - title_bar.inner_rect.max.y = - title_bar.inner_rect.min.y + title_bar_height_with_margin; + // Skip the title bar (and separator): + if is_collapsed { + frame.content_ui.add_space(title_bar.inner_rect.height()); + } else { + frame.content_ui.add_space( + title_bar.inner_rect.height() + + title_content_spacing + + window_frame.inner_margin.sum().y, + ); + } - if on_top && area_content_ui.visuals().window_highlight_topmost { - let mut round = - window_frame.corner_radius - window_frame.stroke.width.round() as u8; + Some(title_bar) + } else { + None + }; - if !is_collapsed { - round.se = 0; - round.sw = 0; + let (content_inner, content_response) = collapsing + .show_body_unindented(&mut frame.content_ui, |ui| { + resize.show(ui, |ui| { + if scroll.is_any_scroll_enabled() { + scroll.show(ui, add_contents).inner + } else { + add_contents(ui) } + }) + }) + .map_or((None, None), |ir| (Some(ir.inner), Some(ir.response))); + + let outer_rect = frame.end(&mut area_content_ui).rect; + paint_resize_corner( + &area_content_ui, + &possible, + outer_rect, + &window_frame, + resize_interaction, + ); + + // END FRAME -------------------------------- + + if let Some(mut title_bar) = title_bar { + title_bar.inner_rect = outer_rect.shrink(window_frame.stroke.width); + title_bar.inner_rect.max.y = + title_bar.inner_rect.min.y + title_bar_height_with_margin; - area_content_ui.painter().set( - *where_to_put_header_background, - RectShape::filled(title_bar.inner_rect, round, header_color), - ); - }; - - if false { - ctx.debug_painter().debug_rect( - title_bar.inner_rect, - Color32::LIGHT_BLUE, - "title_bar.rect", - ); + if on_top && area_content_ui.visuals().window_highlight_topmost { + let mut round = + window_frame.corner_radius - window_frame.stroke.width.round() as u8; + + if !is_collapsed { + round.se = 0; + round.sw = 0; } - title_bar.ui( - &mut area_content_ui, - &content_response, - open.as_deref_mut(), - &mut collapsing, - collapsible, + area_content_ui.painter().set( + *where_to_put_header_background, + RectShape::filled(title_bar.inner_rect, round, header_color), + ); + }; + + if false { + ctx.debug_painter().debug_rect( + title_bar.inner_rect, + Color32::LIGHT_BLUE, + "title_bar.rect", ); } - collapsing.store(ctx); + title_bar.ui( + &mut area_content_ui, + &content_response, + open.as_deref_mut(), + &mut collapsing, + collapsible, + ); + } + + collapsing.store(ctx); - paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction); + paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction); - content_inner - }) + content_inner }; let full_response = area.end(ctx, area_content_ui); @@ -882,6 +879,7 @@ fn move_and_resize_window(ctx: &Context, interaction: &ResizeInteraction) -> Opt fn resize_interaction( ctx: &Context, possible: PossibleInteractions, + accessibility_parent: Id, layer_id: LayerId, outer_rect: Rect, window_frame: Frame, @@ -901,6 +899,7 @@ fn resize_interaction( let rect = outer_rect.shrink(window_frame.stroke.width / 2.0); let side_response = |rect, id| { + ctx.register_accesskit_parent(id, accessibility_parent); let response = ctx.create_widget( WidgetRect { layer_id, diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 1af8cc5f9a4..6b84c783d98 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -38,10 +38,10 @@ use epaint::{ vec2, }; +use self::{hit_test::WidgetHits, interaction::InteractionSnapshot}; #[cfg(feature = "accesskit")] use crate::IdMap; - -use self::{hit_test::WidgetHits, interaction::InteractionSnapshot}; +use crate::pass_state::AccessKitParentChildren; /// Information given to the backend about when it is time to repaint the ui. /// @@ -617,7 +617,8 @@ impl ContextImpl { nodes.insert(id, root_node); viewport.this_pass.accesskit_state = Some(AccessKitPassState { nodes, - parent_stack: vec![id], + parent_map: IdMap::default(), + child_map: IdMap::default(), }); } @@ -709,8 +710,27 @@ impl ContextImpl { let builders = &mut state.nodes; if let std::collections::hash_map::Entry::Vacant(entry) = builders.entry(id) { entry.insert(Default::default()); - let parent_id = state.parent_stack.last().unwrap(); - let parent_builder = builders.get_mut(parent_id).unwrap(); + /// Search the first parent that has an existing accesskit node. + fn find_parent_recursively( + parent_map: &IdMap, + node_map: &IdMap, + id: Id, + ) -> Option { + if let Some(parent_id) = parent_map.get(&id) { + if node_map.contains_key(parent_id) { + Some(*parent_id) + } else { + find_parent_recursively(parent_map, node_map, *parent_id) + } + } else { + None + } + } + + let parent_id = find_parent_recursively(&state.parent_map, builders, id) + .unwrap_or(crate::accesskit_root_id()); + + let parent_builder = builders.get_mut(&parent_id).unwrap(); parent_builder.push_child(id.accesskit_id()); } builders.get_mut(&id).unwrap() @@ -2577,8 +2597,76 @@ impl ContextImpl { { profiling::scope!("accesskit"); let state = viewport.this_pass.accesskit_state.take(); - if let Some(state) = state { + if let Some(mut state) = state { let root_id = crate::accesskit_root_id().accesskit_id(); + + // /// Search the first parent that has an existing accesskit node. + // fn find_parent_recursively( + // parent_map: &IdMap<(Id, usize)>, + // node_map: &IdMap, + // id: Id, + // ) -> Option { + // if let Some(parent_id) = parent_map.get(&id) { + // if node_map.contains_key(parent_id) { + // Some(*parent_id) + // } else { + // find_parent_recursively(parent_map, node_map, *parent_id) + // } + // } else { + // None + // } + // } + // + // for id in state.nodes.keys().copied().collect::>() { + // if id == crate::accesskit_root_id() { + // continue; + // } + // if state.nodes.contains_key(&id) { + // let parent_id = + // find_parent_recursively(&state.parent_map, &state.nodes, id) + // .unwrap_or(crate::accesskit_root_id()); + // state + // .nodes + // .get_mut(&parent_id) + // .unwrap() + // .push_child(id.accesskit_id()); + // } + // } + + // fn children_recursively( + // id: Id, + // node_map: &IdMap, + // parent_children_map: &IdMap, + // children: &mut Vec, + // root_children: &mut Vec, + // ) { + // if let Some(node) = parent_children_map.get(&id) { + // for child_id in &node.children { + // if node_map.contains_key(child_id) { + // if !children.contains(&child_id.accesskit_id()) { + // children.push(child_id.accesskit_id()); + // } + // } else { + // children_recursively(*child_id, node_map, parent_children_map, children, root_children); + // } + // } + // if node.parent.is_none() || node.parent == Some(crate::accesskit_root_id()) { + // if !root_children.contains(&id.accesskit_id()) { + // root_children.push(id.accesskit_id()); + // } + // } + // } + // } + // let mut root_children = vec![]; + // + // for id in state.nodes.keys().copied().collect::>() { + // let mut children = vec![]; + // children_recursively(id, &state.nodes, &state.child_map, &mut children, &mut root_children); + // state.nodes.get_mut(&id).unwrap().set_children(children) + // } + // + // state.nodes.get_mut(&crate::accesskit_root_id()).unwrap().set_children(root_children); + let nodes = { state .nodes @@ -3553,39 +3641,6 @@ impl Context { /// ## Accessibility impl Context { - /// Call the provided function with the given ID pushed on the stack of - /// parent IDs for accessibility purposes. If the `accesskit` feature - /// is disabled or if AccessKit support is not active for this frame, - /// the function is still called, but with no other effect. - /// - /// No locks are held while the given closure is called. - #[allow(clippy::unused_self, clippy::let_and_return, clippy::allow_attributes)] - #[inline] - pub fn with_accessibility_parent(&self, _id: Id, f: impl FnOnce() -> R) -> R { - // TODO(emilk): this isn't thread-safe - another thread can call this function between the push/pop calls - #[cfg(feature = "accesskit")] - self.pass_state_mut(|fs| { - if let Some(state) = fs.accesskit_state.as_mut() { - state.parent_stack.push(_id); - } - }); - - let result = f(); - - #[cfg(feature = "accesskit")] - self.pass_state_mut(|fs| { - if let Some(state) = fs.accesskit_state.as_mut() { - assert_eq!( - state.parent_stack.pop(), - Some(_id), - "Mismatched push/pop in with_accessibility_parent" - ); - } - }); - - result - } - /// If AccessKit support is active for the current frame, get or create /// a node builder with the specified ID and return a mutable reference to it. /// For newly created nodes, the parent is the node with the ID at the top @@ -3611,6 +3666,22 @@ impl Context { }) } + #[cfg(feature = "accesskit")] + pub(crate) fn register_accesskit_parent(&self, id: Id, parent_id: Id) { + self.write(|ctx| { + if let Some(state) = ctx.viewport().this_pass.accesskit_state.as_mut() { + state.parent_map.insert(id, parent_id); + state.child_map.entry(id).or_default().parent = Some(parent_id); + state + .child_map + .entry(parent_id) + .or_default() + .children + .push(id); + } + }); + } + /// Enable generation of AccessKit tree updates in all future frames. #[cfg(feature = "accesskit")] pub fn enable_accesskit(&self) { diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index 233f75a4256..4eb9bab38f9 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -714,6 +714,7 @@ impl WidgetInfo { WidgetType::ImageButton => "image button", WidgetType::Image => "image", WidgetType::CollapsingHeader => "collapsing header", + WidgetType::Panel => "panel", WidgetType::ProgressIndicator => "progress indicator", WidgetType::Window => "window", WidgetType::Label | WidgetType::Other => "", diff --git a/crates/egui/src/pass_state.rs b/crates/egui/src/pass_state.rs index ca0d1572009..edb1560a560 100644 --- a/crates/egui/src/pass_state.rs +++ b/crates/egui/src/pass_state.rs @@ -71,7 +71,15 @@ impl ScrollTarget { #[derive(Clone)] pub struct AccessKitPassState { pub nodes: IdMap, - pub parent_stack: Vec, + pub parent_map: IdMap, + pub child_map: IdMap, +} + +#[cfg(feature = "accesskit")] +#[derive(Clone, Debug, Default)] +pub struct AccessKitParentChildren { + pub parent: Option, + pub children: Vec, } #[cfg(debug_assertions)] diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 85dc8a607c5..c343e263092 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -859,6 +859,7 @@ impl Response { WidgetType::Slider => Role::Slider, WidgetType::DragValue => Role::SpinButton, WidgetType::ColorButton => Role::ColorWell, + WidgetType::Panel => Role::Pane, WidgetType::ProgressIndicator => Role::ProgressIndicator, WidgetType::Window => Role::Window, WidgetType::Other => Role::Unknown, diff --git a/crates/egui/src/text_selection/accesskit_text.rs b/crates/egui/src/text_selection/accesskit_text.rs index b189955429e..0229f9754db 100644 --- a/crates/egui/src/text_selection/accesskit_text.rs +++ b/crates/egui/src/text_selection/accesskit_text.rs @@ -40,60 +40,59 @@ pub fn update_accesskit_for_text_widget( return; }; - ctx.with_accessibility_parent(parent_id, || { - for (row_index, row) in galley.rows.iter().enumerate() { - let row_id = parent_id.with(row_index); - ctx.accesskit_node_builder(row_id, |builder| { - builder.set_role(accesskit::Role::TextRun); - let rect = global_from_galley * row.rect_without_leading_space(); - builder.set_bounds(accesskit::Rect { - x0: rect.min.x.into(), - y0: rect.min.y.into(), - x1: rect.max.x.into(), - y1: rect.max.y.into(), - }); - builder.set_text_direction(accesskit::TextDirection::LeftToRight); - // TODO(mwcampbell): Set more node fields for the row - // once AccessKit adapters expose text formatting info. + for (row_index, row) in galley.rows.iter().enumerate() { + let row_id = parent_id.with(row_index); + ctx.register_accesskit_parent(row_id, parent_id); + ctx.accesskit_node_builder(row_id, |builder| { + builder.set_role(accesskit::Role::TextRun); + let rect = global_from_galley * row.rect_without_leading_space(); + builder.set_bounds(accesskit::Rect { + x0: rect.min.x.into(), + y0: rect.min.y.into(), + x1: rect.max.x.into(), + y1: rect.max.y.into(), + }); + builder.set_text_direction(accesskit::TextDirection::LeftToRight); + // TODO(mwcampbell): Set more node fields for the row + // once AccessKit adapters expose text formatting info. - let glyph_count = row.glyphs.len(); - let mut value = String::new(); - value.reserve(glyph_count); - let mut character_lengths = Vec::::with_capacity(glyph_count); - let mut character_positions = Vec::::with_capacity(glyph_count); - let mut character_widths = Vec::::with_capacity(glyph_count); - let mut word_lengths = Vec::::new(); - let mut was_at_word_end = false; - let mut last_word_start = 0usize; + let glyph_count = row.glyphs.len(); + let mut value = String::new(); + value.reserve(glyph_count); + let mut character_lengths = Vec::::with_capacity(glyph_count); + let mut character_positions = Vec::::with_capacity(glyph_count); + let mut character_widths = Vec::::with_capacity(glyph_count); + let mut word_lengths = Vec::::new(); + let mut was_at_word_end = false; + let mut last_word_start = 0usize; - for glyph in &row.glyphs { - let is_word_char = is_word_char(glyph.chr); - if is_word_char && was_at_word_end { - word_lengths.push((character_lengths.len() - last_word_start) as _); - last_word_start = character_lengths.len(); - } - was_at_word_end = !is_word_char; - let old_len = value.len(); - value.push(glyph.chr); - character_lengths.push((value.len() - old_len) as _); - character_positions.push(glyph.pos.x - row.pos.x); - character_widths.push(glyph.advance_width); + for glyph in &row.glyphs { + let is_word_char = is_word_char(glyph.chr); + if is_word_char && was_at_word_end { + word_lengths.push((character_lengths.len() - last_word_start) as _); + last_word_start = character_lengths.len(); } + was_at_word_end = !is_word_char; + let old_len = value.len(); + value.push(glyph.chr); + character_lengths.push((value.len() - old_len) as _); + character_positions.push(glyph.pos.x - row.pos.x); + character_widths.push(glyph.advance_width); + } - if row.ends_with_newline { - value.push('\n'); - character_lengths.push(1); - character_positions.push(row.size.x); - character_widths.push(0.0); - } - word_lengths.push((character_lengths.len() - last_word_start) as _); + if row.ends_with_newline { + value.push('\n'); + character_lengths.push(1); + character_positions.push(row.size.x); + character_widths.push(0.0); + } + word_lengths.push((character_lengths.len() - last_word_start) as _); - builder.set_value(value); - builder.set_character_lengths(character_lengths); - builder.set_character_positions(character_positions); - builder.set_character_widths(character_widths); - builder.set_word_lengths(word_lengths); - }); - } - }); + builder.set_value(value); + builder.set_character_lengths(character_lengths); + builder.set_character_positions(character_positions); + builder.set_character_widths(character_widths); + builder.set_word_lengths(word_lengths); + }); + } } diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 56392156adb..27762999b98 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -133,6 +133,7 @@ impl Ui { sizing_pass, style, sense, + accessibility_parent, } = ui_builder; let layer_id = layer_id.unwrap_or(LayerId::background()); @@ -173,6 +174,12 @@ impl Ui { min_rect_already_remembered: false, }; + #[cfg(feature = "accesskit")] + if let Some(accessibility_parent) = accessibility_parent { + ui.ctx() + .register_accesskit_parent(ui.unique_id, accessibility_parent); + } + // Register in the widget stack early, to ensure we are behind all widgets we contain: let start_rect = Rect::NOTHING; // This will be overwritten when `remember_min_rect` is called ui.ctx().create_widget( @@ -194,6 +201,11 @@ impl Ui { ui.set_invisible(); } + #[cfg(feature = "accesskit")] + ui.ctx().accesskit_node_builder(ui.unique_id, |node| { + node.set_role(accesskit::Role::GenericContainer) + }); + ui } @@ -259,6 +271,7 @@ impl Ui { sizing_pass, style, sense, + accessibility_parent, } = ui_builder; let mut painter = self.painter.clone(); @@ -321,6 +334,12 @@ impl Ui { child_ui.disable(); } + #[cfg(feature = "accesskit")] + child_ui.ctx().register_accesskit_parent( + child_ui.unique_id, + accessibility_parent.unwrap_or(self.unique_id), + ); + // Register in the widget stack early, to ensure we are behind all widgets we contain: let start_rect = Rect::NOTHING; // This will be overwritten when `remember_min_rect` is called child_ui.ctx().create_widget( @@ -335,6 +354,13 @@ impl Ui { true, ); + #[cfg(feature = "accesskit")] + child_ui + .ctx() + .accesskit_node_builder(child_ui.unique_id, |node| { + node.set_role(Role::GenericContainer) + }); + child_ui } @@ -1064,7 +1090,8 @@ impl Ui { impl Ui { /// Check for clicks, drags and/or hover on a specific region of this [`Ui`]. pub fn interact(&self, rect: Rect, id: Id, sense: Sense) -> Response { - self.ctx().create_widget( + self.ctx().register_accesskit_parent(id, self.unique_id); + let response = self.ctx().create_widget( WidgetRect { id, layer_id: self.layer_id(), @@ -1074,7 +1101,8 @@ impl Ui { enabled: self.enabled, }, true, - ) + ); + response } /// Deprecated: use [`Self::interact`] instead. diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index 549170182ce..17939a29692 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -23,6 +23,8 @@ pub struct UiBuilder { pub sizing_pass: bool, pub style: Option>, pub sense: Option, + #[cfg(feature = "accesskit")] + pub accessibility_parent: Option, } impl UiBuilder { @@ -151,4 +153,15 @@ impl UiBuilder { .insert(ClosableTag::NAME, Some(Arc::new(ClosableTag::default()))); self } + + /// Set the accessibility parent for this [`Ui`]. + /// + /// This will override the automatic parent assignment for accessibility purposes. + /// If not set, the parent [`Ui`]'s ID will be used as the accessibility parent. + #[cfg(feature = "accesskit")] + #[inline] + pub fn accessibility_parent(mut self, parent_id: Id) -> Self { + self.accessibility_parent = Some(parent_id); + self + } } diff --git a/crates/egui_demo_app/src/accessibility_inspector.rs b/crates/egui_demo_app/src/accessibility_inspector.rs index ffcc9fa9b3b..bdd972f6fe2 100644 --- a/crates/egui_demo_app/src/accessibility_inspector.rs +++ b/crates/egui_demo_app/src/accessibility_inspector.rs @@ -118,14 +118,12 @@ impl egui::Plugin for AccessibilityInspectorPlugin { SidePanel::right(Self::id()).show(ctx, |ui| { ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate); - let response = ui.heading("🔎 AccessKit Inspector"); + 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); - } - }); + ScrollArea::vertical().show(ui, |ui| { + if let Some(tree) = &self.tree { + Self::node_ui(ui, &tree.state().root(), &mut self.selected_node); + } }); }); } From c1d8d10e7eb6cb8c86ec93ac994b12cd9c651215 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 17 Jul 2025 16:39:36 +0200 Subject: [PATCH 09/47] Add panel widget type --- crates/egui/src/containers/panel.rs | 6 +++++- crates/egui/src/lib.rs | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index 2869f598cf4..1f8043e09f9 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -19,7 +19,8 @@ use emath::GuiRounding as _; use crate::{ Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, NumExt as _, Rangef, - Rect, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp, vec2, + Rect, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetInfo, WidgetType, lerp, + vec2, }; fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 { @@ -389,6 +390,9 @@ impl SidePanel { .max_rect(available_rect), ); panel_ui.set_clip_rect(ctx.screen_rect()); + panel_ui + .response() + .widget_info(|| WidgetInfo::new(WidgetType::Panel)); let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); let rect = inner_response.response.rect; diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index d2c2eeac32c..1f889eada6c 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -674,6 +674,8 @@ pub enum WidgetType { CollapsingHeader, + Panel, + ProgressIndicator, Window, From fb99bba020c0c29613725257054e262006d7c5c9 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 23 Jul 2025 11:54:52 +0200 Subject: [PATCH 10/47] 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 11/47] 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 12/47] 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 13/47] 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 14/47] 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 15/47] 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 16/47] 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 17/47] 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 18/47] 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 19/47] 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 4b18cc5ffb79b90e42682fecebb83ae0fa377515 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Jul 2025 12:11:49 +0200 Subject: [PATCH 20/47] Fixes after merge --- crates/egui/src/context.rs | 2 +- crates/egui/src/ui.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 2d81a2eae7e..7a7fa697767 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -24,7 +24,7 @@ use crate::{ os::OperatingSystem, output::FullOutput, pass_state::PassState, - resize, response, scroll_area, + plugin, resize, response, scroll_area, util::IdTypeMap, viewport::ViewportClass, }; diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 27762999b98..a370dbbb24c 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -1,10 +1,6 @@ #![warn(missing_docs)] // Let's keep `Ui` well-documented. #![allow(clippy::use_self)] -use emath::GuiRounding as _; -use epaint::mutex::RwLock; -use std::{any::Any, hash::Hash, sync::Arc}; - use crate::ClosableTag; #[cfg(debug_assertions)] use crate::Stroke; @@ -30,6 +26,10 @@ use crate::{ Separator, Spinner, TextEdit, Widget, color_picker, }, }; +use accesskit::Role; +use emath::GuiRounding as _; +use epaint::mutex::RwLock; +use std::{any::Any, hash::Hash, sync::Arc}; // ---------------------------------------------------------------------------- /// This is what you use to place widgets. From 0033e45a3366d9e457bb216c4d2588e9cf302a6f Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Jul 2025 15:06:54 +0200 Subject: [PATCH 21/47] 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 22/47] 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 37ec69a0c67aa608957b49de6f02b354575a93f5 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Jul 2025 15:46:48 +0200 Subject: [PATCH 23/47] Clippy --- crates/egui/src/context.rs | 2 +- crates/egui/src/ui.rs | 4 ++-- .../egui_demo_app/src/accessibility_inspector.rs | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 7a7fa697767..6f3437b9b0f 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2503,7 +2503,7 @@ impl ContextImpl { { profiling::scope!("accesskit"); let state = viewport.this_pass.accesskit_state.take(); - if let Some(mut state) = state { + if let Some(state) = state { let root_id = crate::accesskit_root_id().accesskit_id(); // /// Search the first parent that has an existing accesskit node. diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index a370dbbb24c..ae975bffce9 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -203,7 +203,7 @@ impl Ui { #[cfg(feature = "accesskit")] ui.ctx().accesskit_node_builder(ui.unique_id, |node| { - node.set_role(accesskit::Role::GenericContainer) + node.set_role(accesskit::Role::GenericContainer); }); ui @@ -358,7 +358,7 @@ impl Ui { child_ui .ctx() .accesskit_node_builder(child_ui.unique_id, |node| { - node.set_role(Role::GenericContainer) + node.set_role(Role::GenericContainer); }); child_ui diff --git a/crates/egui_demo_app/src/accessibility_inspector.rs b/crates/egui_demo_app/src/accessibility_inspector.rs index 6a8e67787fd..bbdd3ef7269 100644 --- a/crates/egui_demo_app/src/accessibility_inspector.rs +++ b/crates/egui_demo_app/src/accessibility_inspector.rs @@ -142,14 +142,14 @@ impl egui::Plugin for AccessibilityInspectorPlugin { break; }; 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); - + && ui.button(format!("{action:?}")).clicked() + { + let action_request = ActionRequest { + target: node.id(), + action, + data: None, + }; + self.queued_action = Some(action_request); } } }); From 6eb19b00a884055663e218de71b33e5cd95dfefa Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Jul 2025 16:28:22 +0200 Subject: [PATCH 24/47] Fix features --- crates/egui/src/text_selection/accesskit_text.rs | 1 + crates/egui/src/ui.rs | 13 ++++++++----- crates/egui/src/ui_builder.rs | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/egui/src/text_selection/accesskit_text.rs b/crates/egui/src/text_selection/accesskit_text.rs index 0229f9754db..4d64229c540 100644 --- a/crates/egui/src/text_selection/accesskit_text.rs +++ b/crates/egui/src/text_selection/accesskit_text.rs @@ -42,6 +42,7 @@ pub fn update_accesskit_for_text_widget( for (row_index, row) in galley.rows.iter().enumerate() { let row_id = parent_id.with(row_index); + #[cfg(feature = "accesskit")] ctx.register_accesskit_parent(row_id, parent_id); ctx.accesskit_node_builder(row_id, |builder| { builder.set_role(accesskit::Role::TextRun); diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index ae975bffce9..27e554aa8c4 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -1,6 +1,10 @@ #![warn(missing_docs)] // Let's keep `Ui` well-documented. #![allow(clippy::use_self)] +use emath::GuiRounding as _; +use epaint::mutex::RwLock; +use std::{any::Any, hash::Hash, sync::Arc}; + use crate::ClosableTag; #[cfg(debug_assertions)] use crate::Stroke; @@ -26,10 +30,6 @@ use crate::{ Separator, Spinner, TextEdit, Widget, color_picker, }, }; -use accesskit::Role; -use emath::GuiRounding as _; -use epaint::mutex::RwLock; -use std::{any::Any, hash::Hash, sync::Arc}; // ---------------------------------------------------------------------------- /// This is what you use to place widgets. @@ -133,6 +133,7 @@ impl Ui { sizing_pass, style, sense, + #[cfg(feature = "accesskit")] accessibility_parent, } = ui_builder; @@ -271,6 +272,7 @@ impl Ui { sizing_pass, style, sense, + #[cfg(feature = "accesskit")] accessibility_parent, } = ui_builder; @@ -358,7 +360,7 @@ impl Ui { child_ui .ctx() .accesskit_node_builder(child_ui.unique_id, |node| { - node.set_role(Role::GenericContainer); + node.set_role(accesskit::Role::GenericContainer); }); child_ui @@ -1090,6 +1092,7 @@ impl Ui { impl Ui { /// Check for clicks, drags and/or hover on a specific region of this [`Ui`]. pub fn interact(&self, rect: Rect, id: Id, sense: Sense) -> Response { + #[cfg(feature = "accesskit")] self.ctx().register_accesskit_parent(id, self.unique_id); let response = self.ctx().create_widget( WidgetRect { diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index 17939a29692..3d9ad8b13b1 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -158,9 +158,9 @@ impl UiBuilder { /// /// This will override the automatic parent assignment for accessibility purposes. /// If not set, the parent [`Ui`]'s ID will be used as the accessibility parent. - #[cfg(feature = "accesskit")] #[inline] pub fn accessibility_parent(mut self, parent_id: Id) -> Self { + #[cfg(feature = "accesskit")] self.accessibility_parent = Some(parent_id); self } From f1facab70ac0c80cfe990d452b1c1f31484665fe Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Jul 2025 16:32:45 +0200 Subject: [PATCH 25/47] Fix features --- crates/egui/src/ui_builder.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index 3d9ad8b13b1..619a629e49a 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -158,10 +158,14 @@ impl UiBuilder { /// /// This will override the automatic parent assignment for accessibility purposes. /// If not set, the parent [`Ui`]'s ID will be used as the accessibility parent. + /// + /// This does nothing if the `accesskit` feature is not enabled. #[inline] pub fn accessibility_parent(mut self, parent_id: Id) -> Self { #[cfg(feature = "accesskit")] - self.accessibility_parent = Some(parent_id); + { + self.accessibility_parent = Some(parent_id); + } self } } From 51068caecdc3f09c02821f5cbefb5f8ffa586162 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Jul 2025 16:34:19 +0200 Subject: [PATCH 26/47] Fix space --- crates/egui/src/context.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 6f3437b9b0f..62846204f44 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -612,6 +612,7 @@ impl ContextImpl { let builders = &mut state.nodes; if let std::collections::hash_map::Entry::Vacant(entry) = builders.entry(id) { entry.insert(Default::default()); + /// Search the first parent that has an existing accesskit node. fn find_parent_recursively( parent_map: &IdMap, From bda05df01064fc7c5c09f8925e48ec5419b5c86e Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Jul 2025 16:36:08 +0200 Subject: [PATCH 27/47] More fixes --- crates/egui/src/containers/window.rs | 1 + crates/egui/src/ui_builder.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 59b8fb17943..4c2a5e1aac9 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -899,6 +899,7 @@ fn resize_interaction( let rect = outer_rect.shrink(window_frame.stroke.width / 2.0); let side_response = |rect, id| { + #[cfg(feature = "accesskit")] ctx.register_accesskit_parent(id, accessibility_parent); let response = ctx.create_widget( WidgetRect { diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index 619a629e49a..0c1125e5a83 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -161,6 +161,7 @@ impl UiBuilder { /// /// This does nothing if the `accesskit` feature is not enabled. #[inline] + #[allow(unused_mut, unused_variables)] pub fn accessibility_parent(mut self, parent_id: Id) -> Self { #[cfg(feature = "accesskit")] { From 729a590da3ed7aac8644b72ff6fffd0811428e7f Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Jul 2025 16:38:12 +0200 Subject: [PATCH 28/47] Fix --- crates/egui/src/ui_builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index 0c1125e5a83..f9432f44f4d 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -160,8 +160,8 @@ impl UiBuilder { /// If not set, the parent [`Ui`]'s ID will be used as the accessibility parent. /// /// This does nothing if the `accesskit` feature is not enabled. - #[inline] #[allow(unused_mut, unused_variables)] + #[inline] pub fn accessibility_parent(mut self, parent_id: Id) -> Self { #[cfg(feature = "accesskit")] { From cb542e7ce5153f153d3d397ea0f761d7f36496c3 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Jul 2025 16:38:53 +0200 Subject: [PATCH 29/47] Fix --- crates/egui/src/containers/window.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 4c2a5e1aac9..da424dc2943 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -879,7 +879,7 @@ fn move_and_resize_window(ctx: &Context, interaction: &ResizeInteraction) -> Opt fn resize_interaction( ctx: &Context, possible: PossibleInteractions, - accessibility_parent: Id, + _accessibility_parent: Id, layer_id: LayerId, outer_rect: Rect, window_frame: Frame, @@ -900,7 +900,7 @@ fn resize_interaction( let side_response = |rect, id| { #[cfg(feature = "accesskit")] - ctx.register_accesskit_parent(id, accessibility_parent); + ctx.register_accesskit_parent(id, _accessibility_parent); let response = ctx.create_widget( WidgetRect { layer_id, From e895575a0cba50bf8aea6545a3d216af9a04ecb3 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Jul 2025 16:50:46 +0200 Subject: [PATCH 30/47] Fix doc comment --- crates/egui/src/context.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 62846204f44..add35fd62ac 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -3550,8 +3550,8 @@ impl Context { impl Context { /// If AccessKit support is active for the current frame, get or create /// a node builder with the specified ID and return a mutable reference to it. - /// For newly created nodes, the parent is the node with the ID at the top - /// of the stack managed by [`Context::with_accessibility_parent`]. + /// For newly created nodes, the parent is the parent [`Ui`]s ID. + /// And an [`Ui`]s parent can be set with [`UiBuilder::accessibility_parent`]. /// /// The `Context` lock is held while the given closure is called! /// From e816265a52dba76b9b26a9dffbe60ff2a4e3388f Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Jul 2025 16:53:00 +0200 Subject: [PATCH 31/47] Use cfg attr and expect --- crates/egui/src/ui_builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index f9432f44f4d..7199f0e3cc6 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -160,7 +160,7 @@ impl UiBuilder { /// If not set, the parent [`Ui`]'s ID will be used as the accessibility parent. /// /// This does nothing if the `accesskit` feature is not enabled. - #[allow(unused_mut, unused_variables)] + #[cfg_attr(not(feature = "accesskit"), expect(unused_mut, unused_variables))] #[inline] pub fn accessibility_parent(mut self, parent_id: Id) -> Self { #[cfg(feature = "accesskit")] From a226d8f25c4a6369ae06e3e344f2d325bfdb48a6 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 24 Jul 2025 16:55:02 +0200 Subject: [PATCH 32/47] Fix doc link --- crates/egui/src/context.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index add35fd62ac..90243395932 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -3551,7 +3551,7 @@ impl Context { /// If AccessKit support is active for the current frame, get or create /// a node builder with the specified ID and return a mutable reference to it. /// For newly created nodes, the parent is the parent [`Ui`]s ID. - /// And an [`Ui`]s parent can be set with [`UiBuilder::accessibility_parent`]. + /// And an [`Ui`]s parent can be set with [`crate::UiBuilder::accessibility_parent`]. /// /// The `Context` lock is held while the given closure is called! /// From 5ce5b7f36c1032c51d7bb3083dd769ec017b6c28 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Mon, 28 Jul 2025 12:40:20 +0200 Subject: [PATCH 33/47] 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 34/47] 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 35/47] 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() { From 2c3ab28351fc38688d360bedde589a22c5ed5d96 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 7 Oct 2025 10:43:43 +0200 Subject: [PATCH 36/47] Clippy --- crates/egui/src/id.rs | 4 ++-- crates/egui_demo_app/src/accessibility_inspector.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index cce99dd230b..0565dc5670f 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -98,9 +98,9 @@ impl Id { /// # Panics /// If the value is zero, this will panic. #[doc(hidden)] - #[allow(unsafe_code)] + #[expect(unsafe_code)] pub unsafe fn from_high_entropy_bits(value: u64) -> Self { - Self(NonZeroU64::new(value).expect("Id must be non-zero."), ) + Self(NonZeroU64::new(value).expect("Id must be non-zero.")) } } diff --git a/crates/egui_demo_app/src/accessibility_inspector.rs b/crates/egui_demo_app/src/accessibility_inspector.rs index 202d633ff99..3fbdf698352 100644 --- a/crates/egui_demo_app/src/accessibility_inspector.rs +++ b/crates/egui_demo_app/src/accessibility_inspector.rs @@ -149,7 +149,7 @@ impl egui::Plugin for AccessibilityInspectorPlugin { let Some(action) = action else { break; }; - if node.supports_action(action, &|node| { + if node.supports_action(action, &|_node| { FilterResult::Include }) && ui.button(format!("{action:?}")).clicked() { @@ -226,7 +226,7 @@ impl AccessibilityInspectorPlugin { .clicked() { collapsing.set_open(!collapsing.is_open()); - }; + } let label_response = ui.selectable_value(selected_node, Some(egui_node_id), label.clone()); if label_response.hovered() { From 9291e8023519beb9426ff5a252ff36576452a7ed Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 7 Oct 2025 12:34:14 +0200 Subject: [PATCH 37/47] Fmt --- crates/egui/src/ui.rs | 6 +++--- crates/egui_demo_app/src/accessibility_inspector.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 47d06ea42d6..2cbcf52308f 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -1131,7 +1131,8 @@ impl Ui { pub fn interact(&self, rect: Rect, id: Id, sense: Sense) -> Response { #[cfg(feature = "accesskit")] self.ctx().register_accesskit_parent(id, self.unique_id); - let response = self.ctx().create_widget( + + self.ctx().create_widget( WidgetRect { id, layer_id: self.layer_id(), @@ -1141,8 +1142,7 @@ impl Ui { enabled: self.enabled, }, true, - ); - response + ) } /// Deprecated: use [`Self::interact`] instead. diff --git a/crates/egui_demo_app/src/accessibility_inspector.rs b/crates/egui_demo_app/src/accessibility_inspector.rs index 9ec33d048a7..3bc2f8d434a 100644 --- a/crates/egui_demo_app/src/accessibility_inspector.rs +++ b/crates/egui_demo_app/src/accessibility_inspector.rs @@ -142,8 +142,8 @@ impl egui::Plugin for AccessibilityInspectorPlugin { break; }; if node.supports_action(action, &|_node| { - FilterResult::Include - })&& ui.button(format!("{action:?}")).clicked() + FilterResult::Include + }) && ui.button(format!("{action:?}")).clicked() { let action_request = ActionRequest { target: node.id(), From 2d21997b05fa0ae6256ce6902d55639ebc1d8ac6 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 7 Oct 2025 12:44:53 +0200 Subject: [PATCH 38/47] Resolve some review comments --- .../src/accessibility_inspector.rs | 209 +++++++++--------- crates/egui_demo_app/src/lib.rs | 2 +- 2 files changed, 106 insertions(+), 105 deletions(-) diff --git a/crates/egui_demo_app/src/accessibility_inspector.rs b/crates/egui_demo_app/src/accessibility_inspector.rs index 3fbdf698352..f283df16ebb 100644 --- a/crates/egui_demo_app/src/accessibility_inspector.rs +++ b/crates/egui_demo_app/src/accessibility_inspector.rs @@ -8,6 +8,18 @@ use egui::{ }; use std::mem; +/// This [`egui::Plugin`] adds an inspector Panel. +/// +/// It can be opened with the `(Cmd/Ctrl)+Alt+I`. It shows the current AccessKit tree and details +/// for the selected node. +/// Useful when debugging accessibility issues or trying to understand the structure of the Ui. +/// +/// Add via +/// ``` +/// # use egui_demo_app::accessibility_inspector::AccessibilityInspectorPlugin; +/// # let ctx = egui::Context::default(); +/// ctx.add_plugin(AccessibilityInspectorPlugin::default()); +/// ``` #[derive(Default, Debug)] pub struct AccessibilityInspectorPlugin { pub open: bool, @@ -68,118 +80,107 @@ impl egui::Plugin for AccessibilityInspectorPlugin { self.open = !self.open; } - if self.open { - ctx.enable_accesskit(); - - SidePanel::right(Self::id()).show(ctx, |ui| { - let response = ui.heading("🔎 AccessKit Inspector"); - 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, - "", - ); - } + if !self.open { + return; + } + + ctx.enable_accesskit(); + + SidePanel::right(Self::id()).show(ctx, |ui| { + let response = ui.heading("🔎 AccessKit Inspector"); + 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 + && 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, + "", + ); + } - egui::Grid::new("node_details_grid").num_columns(2).show( - ui, - |ui| { - ui.label("Node ID:"); - ui.strong(format!("{selected_node:?}")); - ui.end_row(); - - ui.label("Role:"); - ui.strong(format!("{:?}", node.role())); - ui.end_row(); - - ui.label("Label:"); - ui.add( - Label::new( - RichText::new( - node.label().unwrap_or_default(), - ) - .strong(), - ) - .truncate(), - ); - ui.end_row(); - - ui.label("Value:"); - ui.add( - Label::new( - RichText::new( - node.value().unwrap_or_default(), - ) - .strong(), - ) - .truncate(), - ); - ui.end_row(); - - ui.label("Children:"); - ui.label( - RichText::new(format!( - "{}", - node.children().len() - )) + egui::Grid::new("node_details_grid").num_columns(2).show( + ui, + |ui| { + ui.label("Node ID"); + ui.strong(format!("{selected_node:?}")); + ui.end_row(); + + ui.label("Role"); + ui.strong(format!("{:?}", node.role())); + ui.end_row(); + + ui.label("Label"); + ui.add( + Label::new( + RichText::new(node.label().unwrap_or_default()) .strong(), - ); - ui.end_row(); - }, + ) + .truncate(), ); + ui.end_row(); - 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, &|_node| { - FilterResult::Include - }) && 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"); + ui.label("Value"); + ui.add( + Label::new( + RichText::new(node.value().unwrap_or_default()) + .strong(), + ) + .truncate(), + ); + ui.end_row(); + + ui.label("Children"); + ui.label(RichText::new(node.children().len()).strong()); + ui.end_row(); + }, + ); + + 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, &|_node| FilterResult::Include) + && ui.button(format!("{action:?}")).clicked() + { + let action_request = ActionRequest { + target: node.id(), + action, + data: None, + }; + self.queued_action = Some(action_request); + } } - } else { - ui.label("No tree data available"); - } - }); - } + }); + } else { + ui.label("Node not found"); + } + }); + } - 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); - } - }); + 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); + } }); }); - } + }); } } diff --git a/crates/egui_demo_app/src/lib.rs b/crates/egui_demo_app/src/lib.rs index c2d523816f9..05b3c4bd652 100644 --- a/crates/egui_demo_app/src/lib.rs +++ b/crates/egui_demo_app/src/lib.rs @@ -17,7 +17,7 @@ pub(crate) fn seconds_since_midnight() -> f64 { // ---------------------------------------------------------------------------- #[cfg(feature = "accessibility_inspector")] -mod accessibility_inspector; +pub mod accessibility_inspector; #[cfg(target_arch = "wasm32")] mod web; From bbf6af808a8e69b93dd742bc22cdbc49ea01fa24 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 7 Oct 2025 12:59:41 +0200 Subject: [PATCH 39/47] Resolve more review comments --- crates/egui_demo_app/Cargo.toml | 3 +-- .../src/accessibility_inspector.rs | 19 +++++++++++++------ crates/egui_demo_app/src/wrap_app.rs | 1 + 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 0f071c039a2..703347a3239 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -28,6 +28,7 @@ default = ["glow", "persistence"] # image_viewer adds about 0.9 MB of WASM web_app = ["http", "persistence"] +accessibility_inspector = ["dep:accesskit", "dep:accesskit_consumer", "eframe/accesskit"] http = ["ehttp", "image/jpeg", "poll-promise", "egui_extras/image"] image_viewer = ["image/jpeg", "egui_extras/all_loaders", "rfd"] persistence = [ @@ -39,7 +40,6 @@ 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"] @@ -67,7 +67,6 @@ profiling.workspace = true # Optional dependencies: accesskit = { workspace = true, optional = true } accesskit_consumer = { workspace = true, optional = true } - bytemuck = { workspace = true, optional = true } puffin = { workspace = true, optional = true } puffin_http = { 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 f283df16ebb..c087e98ac69 100644 --- a/crates/egui_demo_app/src/accessibility_inspector.rs +++ b/crates/egui_demo_app/src/accessibility_inspector.rs @@ -142,18 +142,25 @@ impl egui::Plugin for AccessibilityInspectorPlugin { ui.end_row(); ui.label("Children"); - ui.label(RichText::new(node.children().len()).strong()); + ui.label( + RichText::new(node.children().len().to_string()) + .strong(), + ); ui.end_row(); }, ); 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; - }; + // Iterate through all possible actions via the `Action::n` helper. + let mut current_action = 0; + let all_actions = std::iter::from_fn(|| { + let action = Action::n(current_action); + current_action += 1; + action + }); + + for action in all_actions { if node .supports_action(action, &|_node| FilterResult::Include) && ui.button(format!("{action:?}")).clicked() diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index d0d1f419c9d..7085e8bd726 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -182,6 +182,7 @@ 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(crate::accessibility_inspector::AccessibilityInspectorPlugin::default()); From c187dff5434571ad800d40ca062bd5160d297337 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 7 Oct 2025 13:04:07 +0200 Subject: [PATCH 40/47] Pull out selection_ui --- .../src/accessibility_inspector.rs | 172 +++++++++--------- 1 file changed, 88 insertions(+), 84 deletions(-) diff --git a/crates/egui_demo_app/src/accessibility_inspector.rs b/crates/egui_demo_app/src/accessibility_inspector.rs index c087e98ac69..113a3de7beb 100644 --- a/crates/egui_demo_app/src/accessibility_inspector.rs +++ b/crates/egui_demo_app/src/accessibility_inspector.rs @@ -89,94 +89,12 @@ impl egui::Plugin for AccessibilityInspectorPlugin { SidePanel::right(Self::id()).show(ctx, |ui| { let response = ui.heading("🔎 AccessKit Inspector"); ctx.with_accessibility_parent(response.id, || { - if let Some(selected_node) = &self.selected_node { + 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 - && 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, - "", - ); - } - - egui::Grid::new("node_details_grid").num_columns(2).show( - ui, - |ui| { - ui.label("Node ID"); - ui.strong(format!("{selected_node:?}")); - ui.end_row(); - - ui.label("Role"); - ui.strong(format!("{:?}", node.role())); - ui.end_row(); - - ui.label("Label"); - ui.add( - Label::new( - RichText::new(node.label().unwrap_or_default()) - .strong(), - ) - .truncate(), - ); - ui.end_row(); - - ui.label("Value"); - ui.add( - Label::new( - RichText::new(node.value().unwrap_or_default()) - .strong(), - ) - .truncate(), - ); - ui.end_row(); - - ui.label("Children"); - ui.label( - RichText::new(node.children().len().to_string()) - .strong(), - ); - ui.end_row(); - }, - ); - - ui.label("Actions"); - ui.horizontal_wrapped(|ui| { - // Iterate through all possible actions via the `Action::n` helper. - let mut current_action = 0; - let all_actions = std::iter::from_fn(|| { - let action = Action::n(current_action); - current_action += 1; - action - }); - - for action in all_actions { - if node - .supports_action(action, &|_node| FilterResult::Include) - && 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"); - } + self.selection_ui(ui, selected_node); }); } @@ -196,6 +114,92 @@ impl AccessibilityInspectorPlugin { Id::new("Accessibility Inspector") } + fn selection_ui(&mut self, ui: &mut Ui, selected_node: Id) { + ui.separator(); + + if let Some(tree) = &self.tree + && 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, + "", + ); + } + + egui::Grid::new("node_details_grid").num_columns(2).show( + ui, + |ui| { + ui.label("Node ID"); + ui.strong(format!("{selected_node:?}")); + ui.end_row(); + + ui.label("Role"); + ui.strong(format!("{:?}", node.role())); + ui.end_row(); + + ui.label("Label"); + ui.add( + Label::new( + RichText::new(node.label().unwrap_or_default()) + .strong(), + ) + .truncate(), + ); + ui.end_row(); + + ui.label("Value"); + ui.add( + Label::new( + RichText::new(node.value().unwrap_or_default()) + .strong(), + ) + .truncate(), + ); + ui.end_row(); + + ui.label("Children"); + ui.label( + RichText::new(node.children().len().to_string()) + .strong(), + ); + ui.end_row(); + }, + ); + + ui.label("Actions"); + ui.horizontal_wrapped(|ui| { + // Iterate through all possible actions via the `Action::n` helper. + let mut current_action = 0; + let all_actions = std::iter::from_fn(|| { + let action = Action::n(current_action); + current_action += 1; + action + }); + + for action in all_actions { + if node + .supports_action(action, &|_node| FilterResult::Include) + && 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"); + } + } + fn node_ui(ui: &mut Ui, node: &Node<'_>, selected_node: &mut Option) { if node.id() == Self::id().value().into() || node From d42b88bc6c4805160d0b3958434419809447d6d5 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 7 Oct 2025 13:05:00 +0200 Subject: [PATCH 41/47] Clippy / fmt --- .../src/accessibility_inspector.rs | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/crates/egui_demo_app/src/accessibility_inspector.rs b/crates/egui_demo_app/src/accessibility_inspector.rs index 113a3de7beb..df80149dee3 100644 --- a/crates/egui_demo_app/src/accessibility_inspector.rs +++ b/crates/egui_demo_app/src/accessibility_inspector.rs @@ -118,8 +118,7 @@ impl AccessibilityInspectorPlugin { ui.separator(); if let Some(tree) = &self.tree - && let Some(node) = - tree.state().node_by_id(NodeId::from(selected_node.value())) + && let Some(node) = tree.state().node_by_id(NodeId::from(selected_node.value())) { let node_response = ui.ctx().read_response(selected_node); @@ -131,9 +130,9 @@ impl AccessibilityInspectorPlugin { ); } - egui::Grid::new("node_details_grid").num_columns(2).show( - ui, - |ui| { + egui::Grid::new("node_details_grid") + .num_columns(2) + .show(ui, |ui| { ui.label("Node ID"); ui.strong(format!("{selected_node:?}")); ui.end_row(); @@ -144,32 +143,22 @@ impl AccessibilityInspectorPlugin { ui.label("Label"); ui.add( - Label::new( - RichText::new(node.label().unwrap_or_default()) - .strong(), - ) + Label::new(RichText::new(node.label().unwrap_or_default()).strong()) .truncate(), ); ui.end_row(); ui.label("Value"); ui.add( - Label::new( - RichText::new(node.value().unwrap_or_default()) - .strong(), - ) + Label::new(RichText::new(node.value().unwrap_or_default()).strong()) .truncate(), ); ui.end_row(); ui.label("Children"); - ui.label( - RichText::new(node.children().len().to_string()) - .strong(), - ); + ui.label(RichText::new(node.children().len().to_string()).strong()); ui.end_row(); - }, - ); + }); ui.label("Actions"); ui.horizontal_wrapped(|ui| { @@ -182,8 +171,7 @@ impl AccessibilityInspectorPlugin { }); for action in all_actions { - if node - .supports_action(action, &|_node| FilterResult::Include) + if node.supports_action(action, &|_node| FilterResult::Include) && ui.button(format!("{action:?}")).clicked() { let action_request = ActionRequest { From 730c59ad11fdf857c4ac49f4a67aca0ba6b66d70 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 7 Oct 2025 17:11:50 +0200 Subject: [PATCH 42/47] Cleanup --- crates/egui/src/context.rs | 76 ----------------------------------- crates/egui/src/pass_state.rs | 1 - 2 files changed, 77 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index a8bf28bde23..56e352fa332 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -515,7 +515,6 @@ impl ContextImpl { viewport.this_pass.accesskit_state = Some(AccessKitPassState { nodes, parent_map: IdMap::default(), - child_map: IdMap::default(), }); } @@ -2491,74 +2490,6 @@ impl ContextImpl { let state = viewport.this_pass.accesskit_state.take(); if let Some(state) = state { let root_id = crate::accesskit_root_id().accesskit_id(); - - // /// Search the first parent that has an existing accesskit node. - // fn find_parent_recursively( - // parent_map: &IdMap<(Id, usize)>, - // node_map: &IdMap, - // id: Id, - // ) -> Option { - // if let Some(parent_id) = parent_map.get(&id) { - // if node_map.contains_key(parent_id) { - // Some(*parent_id) - // } else { - // find_parent_recursively(parent_map, node_map, *parent_id) - // } - // } else { - // None - // } - // } - // - // for id in state.nodes.keys().copied().collect::>() { - // if id == crate::accesskit_root_id() { - // continue; - // } - // if state.nodes.contains_key(&id) { - // let parent_id = - // find_parent_recursively(&state.parent_map, &state.nodes, id) - // .unwrap_or(crate::accesskit_root_id()); - // state - // .nodes - // .get_mut(&parent_id) - // .unwrap() - // .push_child(id.accesskit_id()); - // } - // } - - // fn children_recursively( - // id: Id, - // node_map: &IdMap, - // parent_children_map: &IdMap, - // children: &mut Vec, - // root_children: &mut Vec, - // ) { - // if let Some(node) = parent_children_map.get(&id) { - // for child_id in &node.children { - // if node_map.contains_key(child_id) { - // if !children.contains(&child_id.accesskit_id()) { - // children.push(child_id.accesskit_id()); - // } - // } else { - // children_recursively(*child_id, node_map, parent_children_map, children, root_children); - // } - // } - // if node.parent.is_none() || node.parent == Some(crate::accesskit_root_id()) { - // if !root_children.contains(&id.accesskit_id()) { - // root_children.push(id.accesskit_id()); - // } - // } - // } - // } - // let mut root_children = vec![]; - // - // for id in state.nodes.keys().copied().collect::>() { - // let mut children = vec![]; - // children_recursively(id, &state.nodes, &state.child_map, &mut children, &mut root_children); - // state.nodes.get_mut(&id).unwrap().set_children(children) - // } - // - // state.nodes.get_mut(&crate::accesskit_root_id()).unwrap().set_children(root_children); - let nodes = { state .nodes @@ -3548,13 +3479,6 @@ impl Context { self.write(|ctx| { if let Some(state) = ctx.viewport().this_pass.accesskit_state.as_mut() { state.parent_map.insert(id, parent_id); - state.child_map.entry(id).or_default().parent = Some(parent_id); - state - .child_map - .entry(parent_id) - .or_default() - .children - .push(id); } }); } diff --git a/crates/egui/src/pass_state.rs b/crates/egui/src/pass_state.rs index edb1560a560..cf4ecaba152 100644 --- a/crates/egui/src/pass_state.rs +++ b/crates/egui/src/pass_state.rs @@ -72,7 +72,6 @@ impl ScrollTarget { pub struct AccessKitPassState { pub nodes: IdMap, pub parent_map: IdMap, - pub child_map: IdMap, } #[cfg(feature = "accesskit")] From 3f45b62cfddb6b402fdb8208f80c57f0c4849dcd Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 8 Oct 2025 10:54:31 +0200 Subject: [PATCH 43/47] Remove unused code --- crates/egui/src/pass_state.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/crates/egui/src/pass_state.rs b/crates/egui/src/pass_state.rs index cf4ecaba152..be0084d3dcb 100644 --- a/crates/egui/src/pass_state.rs +++ b/crates/egui/src/pass_state.rs @@ -74,13 +74,6 @@ pub struct AccessKitPassState { pub parent_map: IdMap, } -#[cfg(feature = "accesskit")] -#[derive(Clone, Debug, Default)] -pub struct AccessKitParentChildren { - pub parent: Option, - pub children: Vec, -} - #[cfg(debug_assertions)] #[derive(Clone)] pub struct DebugRect { From 28feaf9f91b6ed7d8f50a588e4273968aa1fb611 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 8 Oct 2025 11:09:30 +0200 Subject: [PATCH 44/47] Fix accesskit tests --- crates/egui_kittest/tests/accesskit.rs | 59 ++++++++++++-------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/crates/egui_kittest/tests/accesskit.rs b/crates/egui_kittest/tests/accesskit.rs index 3f1f33ba9e1..08a96bd7a41 100644 --- a/crates/egui_kittest/tests/accesskit.rs +++ b/crates/egui_kittest/tests/accesskit.rs @@ -18,9 +18,20 @@ fn empty_ui_should_return_tree_with_only_root_window() { assert_eq!( output.nodes.len(), - 1, - "Empty ui should produce only the root window." + 4, + "Expected the root node and two Uis and a Frame for the panel" + ); + + assert_eq!( + output + .nodes + .iter() + .filter(|(_, n)| n.role() == Role::GenericContainer) + .count(), + 3, + "Expected two Uis and one Frame as GenericContainer nodes.", ); + let (id, root) = &output.nodes[0]; assert_eq!(*id, output.tree.unwrap().root); @@ -35,12 +46,6 @@ fn button_node() { CentralPanel::default().show(ctx, |ui| ui.button(button_text)); }); - assert_eq!( - output.nodes.len(), - 2, - "Expected only the root node and the button." - ); - let (_, button) = output .nodes .iter() @@ -61,12 +66,6 @@ fn disabled_button_node() { }); }); - assert_eq!( - output.nodes.len(), - 2, - "Expected only the root node and the button." - ); - let (_, button) = output .nodes .iter() @@ -86,12 +85,6 @@ fn toggle_button_node() { CentralPanel::default().show(ctx, |ui| ui.toggle_value(&mut selected, button_text)); }); - assert_eq!( - output.nodes.len(), - 2, - "Expected only the root node and the button." - ); - let (_, toggle) = output .nodes .iter() @@ -114,12 +107,6 @@ fn multiple_disabled_widgets() { }); }); - assert_eq!( - output.nodes.len(), - 4, - "Expected the root node and all the child widgets." - ); - assert_eq!( output .nodes @@ -194,15 +181,25 @@ fn assert_window_exists(tree: &TreeUpdate, title: &str, parent: NodeId) -> NodeI } #[track_caller] -fn assert_parent_child(tree: &TreeUpdate, parent: NodeId, child: NodeId) { +fn assert_parent_child(tree: &TreeUpdate, parent_id: NodeId, child: NodeId) { + assert!( + has_child_recursively(tree, parent_id, child), + "Node is not a child of the given parent." + ); +} + +fn has_child_recursively(tree: &TreeUpdate, parent: NodeId, child: NodeId) -> bool { let (_, parent) = tree .nodes .iter() .find(|(id, _)| id == &parent) .expect("Parent does not exist."); - assert!( - parent.children().contains(&child), - "Node is not a child of the given parent." - ); + for &c in parent.children() { + if c == child || has_child_recursively(tree, c, child) { + return true; + } + } + + false } From edf5973e39ec186852dbb6832e47164f907035a5 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 8 Oct 2025 11:19:02 +0200 Subject: [PATCH 45/47] Fix test --- tests/egui_tests/tests/test_widgets.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index a6050e95bcb..bad0c839602 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -4,6 +4,7 @@ use egui::{ Grid, IntoAtoms as _, Layout, PointerButton, Response, Slider, Stroke, StrokeKind, TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, Widget as _, include_image, }; +use egui::accesskit::Role; use egui_kittest::kittest::{Queryable as _, by}; use egui_kittest::{Harness, Node, SnapshotResult, SnapshotResults}; @@ -277,8 +278,8 @@ impl<'a> VisualTests<'a> { node.hover(); }); self.add("pressed", |harness| { - harness.get_next().hover(); - let rect = harness.get_next().rect(); + harness.get_next_widget().hover(); + let rect = harness.get_next_widget().rect(); harness.input_mut().events.push(Event::PointerButton { button: PointerButton::Primary, pos: rect.center(), @@ -329,7 +330,7 @@ impl<'a> VisualTests<'a> { pub fn add_node(&mut self, name: &str, test: impl FnOnce(&Node<'_>)) { self.add(name, |harness| { - let node = harness.get_next(); + let node = harness.get_next_widget(); test(&node); }); } @@ -375,11 +376,11 @@ impl<'a> VisualTests<'a> { } trait HarnessExt { - fn get_next(&self) -> Node<'_>; + fn get_next_widget(&self) -> Node<'_>; } impl HarnessExt for Harness<'_> { - fn get_next(&self) -> Node<'_> { - self.get_all(by()).next().unwrap() + fn get_next_widget(&self) -> Node<'_> { + self.get_all(by().predicate(|node| node.role() != Role::GenericContainer)).next().unwrap() } } From 86c3ccefb8585a99d87836681bcddbe79542f946 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 8 Oct 2025 11:21:58 +0200 Subject: [PATCH 46/47] Rename fn --- crates/egui/src/context.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 35c5e90c1a0..aab81ab2c3f 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -596,8 +596,8 @@ impl ContextImpl { if let std::collections::hash_map::Entry::Vacant(entry) = builders.entry(id) { entry.insert(Default::default()); - /// Search the first parent that has an existing accesskit node. - fn find_parent_recursively( + /// Find the first ancestor that already has an accesskit node. + fn find_accesskit_parent( parent_map: &IdMap, node_map: &IdMap, id: Id, @@ -606,14 +606,14 @@ impl ContextImpl { if node_map.contains_key(parent_id) { Some(*parent_id) } else { - find_parent_recursively(parent_map, node_map, *parent_id) + find_accesskit_parent(parent_map, node_map, *parent_id) } } else { None } } - let parent_id = find_parent_recursively(&state.parent_map, builders, id) + let parent_id = find_accesskit_parent(&state.parent_map, builders, id) .unwrap_or(crate::accesskit_root_id()); let parent_builder = builders.get_mut(&parent_id).unwrap(); From 380953482ea6260e84d1bee7bd65766e7f06d9af Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 8 Oct 2025 11:23:08 +0200 Subject: [PATCH 47/47] Clippy --- tests/egui_tests/tests/test_widgets.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index bad0c839602..6a75e36a3a8 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -1,10 +1,10 @@ +use egui::accesskit::Role; use egui::load::SizedTexture; use egui::{ Align, AtomExt as _, AtomLayout, Button, Color32, ColorImage, Direction, DragValue, Event, Grid, IntoAtoms as _, Layout, PointerButton, Response, Slider, Stroke, StrokeKind, TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, Widget as _, include_image, }; -use egui::accesskit::Role; use egui_kittest::kittest::{Queryable as _, by}; use egui_kittest::{Harness, Node, SnapshotResult, SnapshotResults}; @@ -381,6 +381,8 @@ trait HarnessExt { impl HarnessExt for Harness<'_> { fn get_next_widget(&self) -> Node<'_> { - self.get_all(by().predicate(|node| node.role() != Role::GenericContainer)).next().unwrap() + self.get_all(by().predicate(|node| node.role() != Role::GenericContainer)) + .next() + .unwrap() } }