From d190df7d259e664a41d220f29c812da2aeb2a93e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 25 Jan 2024 12:33:42 +0100 Subject: [PATCH] Register callbacks with `Context::on_begin_frame` and `on_end_frame`. (#3886) This can be useful for creating simple plugins for egui. The state can be stored in the egui data store, or by the user. An example plugin for painting debug text on screen is provided. --- bacon.toml | 2 +- crates/egui/src/context.rs | 164 +++++++++--------- crates/egui/src/debug_text.rs | 135 ++++++++++++++ crates/egui/src/lib.rs | 1 + .../text_selection/label_text_selection.rs | 12 +- crates/egui/src/util/id_type_map.rs | 7 +- 6 files changed, 236 insertions(+), 85 deletions(-) create mode 100644 crates/egui/src/debug_text.rs diff --git a/bacon.toml b/bacon.toml index 46b1bfff523..63d72eeb055 100644 --- a/bacon.toml +++ b/bacon.toml @@ -68,7 +68,7 @@ need_stdout = true [keybindings] i = "job:initial" c = "job:cranky" -w = "job:wasm" +a = "job:wasm" d = "job:doc-open" t = "job:test" r = "job:run" diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index ab1091216f0..6e73b22eebc 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -66,6 +66,46 @@ impl Default for WrappedTextureManager { // ---------------------------------------------------------------------------- +/// Generic event callback. +pub type ContextCallback = Arc; + +#[derive(Clone)] +struct NamedContextCallback { + debug_name: &'static str, + callback: ContextCallback, +} + +/// Callbacks that users can register +#[derive(Clone, Default)] +struct Plugins { + pub on_begin_frame: Vec, + pub on_end_frame: Vec, +} + +impl Plugins { + fn call(ctx: &Context, _cb_name: &str, callbacks: &[NamedContextCallback]) { + crate::profile_scope!("plugins", _cb_name); + for NamedContextCallback { + debug_name: _name, + callback, + } in callbacks + { + crate::profile_scope!(_name); + (callback)(ctx); + } + } + + fn on_begin_frame(&self, ctx: &Context) { + Self::call(ctx, "on_begin_frame", &self.on_begin_frame); + } + + fn on_end_frame(&self, ctx: &Context) { + Self::call(ctx, "on_end_frame", &self.on_end_frame); + } +} + +// ---------------------------------------------------------------------------- + /// Repaint-logic impl ContextImpl { /// This is where we update the repaint logic. @@ -231,11 +271,6 @@ impl ViewportRepaintInfo { // ---------------------------------------------------------------------------- -struct DebugText { - location: String, - text: WidgetText, -} - #[derive(Default)] struct ContextImpl { /// Since we could have multiple viewport across multiple monitors with @@ -249,6 +284,8 @@ struct ContextImpl { memory: Memory, animation_manager: AnimationManager, + plugins: Plugins, + /// All viewports share the same texture manager and texture namespace. /// /// In all viewports, [`TextureId::default`] is special, and points to the font atlas. @@ -283,8 +320,6 @@ struct ContextImpl { accesskit_node_classes: accesskit::NodeClassSet, loaders: Arc, - - debug_texts: Vec, } impl ContextImpl { @@ -556,11 +591,17 @@ impl std::cmp::PartialEq for Context { impl Default for Context { fn default() -> Self { - let ctx = ContextImpl { + let ctx_impl = ContextImpl { embed_viewports: true, ..Default::default() }; - Self(Arc::new(RwLock::new(ctx))) + let ctx = Self(Arc::new(RwLock::new(ctx_impl))); + + // Register built-in plugins: + crate::debug_text::register(&ctx); + crate::text_selection::LabelSelectionState::register(&ctx); + + ctx } } @@ -625,7 +666,7 @@ impl Context { /// ``` pub fn begin_frame(&self, new_input: RawInput) { crate::profile_function!(); - crate::text_selection::LabelSelectionState::begin_frame(self); + self.read(|ctx| ctx.plugins.clone()).on_begin_frame(self); self.write(|ctx| ctx.begin_frame_mut(new_input)); } } @@ -1084,18 +1125,11 @@ impl Context { /// # let state = true; /// ctx.debug_text(format!("State: {state:?}")); /// ``` + /// + /// This is just a convenience for calling [`crate::debug_text::print`]. #[track_caller] pub fn debug_text(&self, text: impl Into) { - if cfg!(debug_assertions) { - let location = std::panic::Location::caller(); - let location = format!("{}:{}", location.file(), location.line()); - self.write(|c| { - c.debug_texts.push(DebugText { - location, - text: text.into(), - }); - }); - } + crate::debug_text::print(self, text); } /// What operating system are we running on? @@ -1338,7 +1372,38 @@ impl Context { let callback = Box::new(callback); self.write(|ctx| ctx.request_repaint_callback = Some(callback)); } +} + +/// Callbacks +impl Context { + /// Call the given callback at the start of each frame + /// of each viewport. + /// + /// This can be used for egui _plugins_. + /// See [`crate::debug_text`] for an example. + pub fn on_begin_frame(&self, debug_name: &'static str, cb: ContextCallback) { + let named_cb = NamedContextCallback { + debug_name, + callback: cb, + }; + self.write(|ctx| ctx.plugins.on_begin_frame.push(named_cb)); + } + /// Call the given callback at the end of each frame + /// of each viewport. + /// + /// This can be used for egui _plugins_. + /// See [`crate::debug_text`] for an example. + pub fn on_end_frame(&self, debug_name: &'static str, cb: ContextCallback) { + let named_cb = NamedContextCallback { + debug_name, + callback: cb, + }; + self.write(|ctx| ctx.plugins.on_end_frame.push(named_cb)); + } +} + +impl Context { /// Tell `egui` which fonts to use. /// /// The default `egui` fonts only support latin and cyrillic alphabets, @@ -1616,64 +1681,7 @@ impl Context { crate::gui_zoom::zoom_with_keyboard(self); } - crate::text_selection::LabelSelectionState::end_frame(self); - - let debug_texts = self.write(|ctx| std::mem::take(&mut ctx.debug_texts)); - if !debug_texts.is_empty() { - // Show debug-text next to the cursor. - let mut pos = self - .input(|i| i.pointer.latest_pos()) - .unwrap_or_else(|| self.screen_rect().center()) - + 8.0 * Vec2::Y; - - let painter = self.debug_painter(); - let where_to_put_background = painter.add(Shape::Noop); - - let mut bounding_rect = Rect::from_points(&[pos]); - - let color = Color32::GRAY; - let font_id = FontId::new(10.0, FontFamily::Proportional); - - for DebugText { location, text } in debug_texts { - { - // Paint location to left of `pos`: - let location_galley = - self.fonts(|f| f.layout(location, font_id.clone(), color, f32::INFINITY)); - let location_rect = - Align2::RIGHT_TOP.anchor_size(pos - 4.0 * Vec2::X, location_galley.size()); - painter.galley(location_rect.min, location_galley, color); - bounding_rect = bounding_rect.union(location_rect); - } - - { - // Paint `text` to right of `pos`: - let wrap = true; - let available_width = self.screen_rect().max.x - pos.x; - let galley = text.into_galley_impl( - self, - &self.style(), - wrap, - available_width, - font_id.clone().into(), - Align::TOP, - ); - let rect = Align2::LEFT_TOP.anchor_size(pos, galley.size()); - painter.galley(rect.min, galley, color); - bounding_rect = bounding_rect.union(rect); - } - - pos.y = bounding_rect.max.y + 4.0; - } - - painter.set( - where_to_put_background, - Shape::rect_filled( - bounding_rect.expand(4.0), - 2.0, - Color32::from_black_alpha(192), - ), - ); - } + self.read(|ctx| ctx.plugins.clone()).on_end_frame(self); self.write(|ctx| ctx.end_frame()) } diff --git a/crates/egui/src/debug_text.rs b/crates/egui/src/debug_text.rs new file mode 100644 index 00000000000..b2551de9ddf --- /dev/null +++ b/crates/egui/src/debug_text.rs @@ -0,0 +1,135 @@ +//! 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_frame`], [`Context::on_end_frame`]). + +use crate::*; + +/// Register this plugin on the given egui context, +/// so that it will be called every frame. +/// +/// This is a built-in plugin in egui, +/// meaning [`Context`] calls this from its `Default` implementation, +/// so this i marked as `pub(crate)`. +pub(crate) fn register(ctx: &Context) { + ctx.on_end_frame("debug_text", std::sync::Arc::new(State::end_frame)); +} + +/// Print this text next to the cursor at the end of the frame. +/// +/// If you call this multiple times, the text will be appended. +/// +/// This only works if compiled with `debug_assertions`. +/// +/// ``` +/// # let ctx = &egui::Context::default(); +/// # let state = true; +/// egui::debug_text::print(ctx, format!("State: {state:?}")); +/// ``` +#[track_caller] +pub fn print(ctx: &Context, text: impl Into) { + if !cfg!(debug_assertions) { + return; + } + + 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(), + }); + }); +} + +#[derive(Clone)] +struct Entry { + location: String, + text: WidgetText, +} + +/// A plugin for easily showing debug-text on-screen. +/// +/// This is a built-in plugin in egui. +#[derive(Clone, Default)] +struct State { + // This gets re-filled every frame. + entries: Vec, +} + +impl State { + fn end_frame(ctx: &Context) { + let state = ctx.data_mut(|data| data.remove_temp::(Id::NULL)); + if let Some(state) = state { + state.paint(ctx); + } + } + + fn paint(self, ctx: &Context) { + let Self { entries } = self; + + if entries.is_empty() { + return; + } + + // Show debug-text next to the cursor. + let mut pos = ctx + .input(|i| i.pointer.latest_pos()) + .unwrap_or_else(|| ctx.screen_rect().center()) + + 8.0 * Vec2::Y; + + let painter = ctx.debug_painter(); + let where_to_put_background = painter.add(Shape::Noop); + + let mut bounding_rect = Rect::from_points(&[pos]); + + let color = Color32::GRAY; + let font_id = FontId::new(10.0, FontFamily::Proportional); + + for Entry { location, text } in entries { + { + // Paint location to left of `pos`: + let location_galley = + ctx.fonts(|f| f.layout(location, font_id.clone(), color, f32::INFINITY)); + let location_rect = + Align2::RIGHT_TOP.anchor_size(pos - 4.0 * Vec2::X, location_galley.size()); + painter.galley(location_rect.min, location_galley, color); + bounding_rect = bounding_rect.union(location_rect); + } + + { + // Paint `text` to right of `pos`: + let wrap = true; + let available_width = ctx.screen_rect().max.x - pos.x; + let galley = text.into_galley_impl( + ctx, + &ctx.style(), + wrap, + available_width, + font_id.clone().into(), + Align::TOP, + ); + let rect = Align2::LEFT_TOP.anchor_size(pos, galley.size()); + painter.galley(rect.min, galley, color); + bounding_rect = bounding_rect.union(rect); + } + + pos.y = bounding_rect.max.y + 4.0; + } + + painter.set( + where_to_put_background, + Shape::rect_filled( + bounding_rect.expand(4.0), + 2.0, + Color32::from_black_alpha(192), + ), + ); + } +} diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 86330546113..cd1882411d6 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -347,6 +347,7 @@ mod animation_manager; pub mod containers; mod context; mod data; +pub mod debug_text; mod frame_state; pub(crate) mod grid; pub mod gui_zoom; diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index 45730375ba7..5685bd8b7fe 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -145,6 +145,14 @@ impl Default for LabelSelectionState { } impl LabelSelectionState { + pub(crate) fn register(ctx: &Context) { + ctx.on_begin_frame( + "LabelSelectionState", + std::sync::Arc::new(Self::begin_frame), + ); + ctx.on_end_frame("LabelSelectionState", std::sync::Arc::new(Self::end_frame)); + } + pub fn load(ctx: &Context) -> Self { ctx.data(|data| data.get_temp::(Id::NULL)) .unwrap_or_default() @@ -156,7 +164,7 @@ impl LabelSelectionState { }); } - pub fn begin_frame(ctx: &Context) { + fn begin_frame(ctx: &Context) { let mut state = Self::load(ctx); if ctx.input(|i| i.pointer.any_pressed() && !i.modifiers.shift) { @@ -177,7 +185,7 @@ impl LabelSelectionState { state.store(ctx); } - pub fn end_frame(ctx: &Context) { + fn end_frame(ctx: &Context) { let mut state = Self::load(ctx); if state.is_dragging { diff --git a/crates/egui/src/util/id_type_map.rs b/crates/egui/src/util/id_type_map.rs index 694d1de326e..7e812ae5572 100644 --- a/crates/egui/src/util/id_type_map.rs +++ b/crates/egui/src/util/id_type_map.rs @@ -485,11 +485,10 @@ impl IdTypeMap { /// Remove and fetch the state of this type and id. #[inline] - pub fn remove_temp(&mut self, id: Id) -> Option { + pub fn remove_temp(&mut self, id: Id) -> Option { let hash = hash(TypeId::of::(), id); - self.map - .remove(&hash) - .and_then(|element| element.get_temp().cloned()) + let mut element = self.map.remove(&hash)?; + Some(std::mem::take(element.get_mut_temp()?)) } /// Note all state of the given type.