From ee09019c558f7539b0bb8d99bf251202dad191ad Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 25 Jan 2024 11:24:47 +0100 Subject: [PATCH 1/8] Optimize `IdTypeMap::remove_temp` --- crates/egui/src/util/id_type_map.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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. From 43ad3d6a40251883cacce1793f956ac7b29cd4e2 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 25 Jan 2024 11:24:59 +0100 Subject: [PATCH 2/8] bacon: `w` is for word-wrapping --- bacon.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From ca82fcf01a647f7557686c966e5933f84943387f Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 25 Jan 2024 11:25:47 +0100 Subject: [PATCH 3/8] Introduce context callbacks for `on_begin_frame` and `on_end_frame` --- crates/egui/src/context.rs | 160 ++++++++++++++++++---------------- crates/egui/src/debug_text.rs | 132 ++++++++++++++++++++++++++++ crates/egui/src/lib.rs | 1 + 3 files changed, 217 insertions(+), 76 deletions(-) create mode 100644 crates/egui/src/debug_text.rs diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index ab1091216f0..51ad7113a62 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, + callback, + } in callbacks + { + crate::profile_scope!(debug_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,16 @@ 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); + + ctx } } @@ -626,6 +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,37 @@ 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, @@ -1617,63 +1681,7 @@ impl Context { } 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..dd2283a0d15 --- /dev/null +++ b/crates/egui/src/debug_text.rs @@ -0,0 +1,132 @@ +//! 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(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| { + 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 pluging 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: entires } = self; + + if entires.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 entires { + { + // 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; From f9e5cdc18c083646aa49091bc67a85a4a077eff5 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 25 Jan 2024 11:28:29 +0100 Subject: [PATCH 4/8] Register `LabelSelectionState` like a plugin --- crates/egui/src/context.rs | 3 +-- .../egui/src/text_selection/label_text_selection.rs | 12 ++++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 51ad7113a62..9c35edcecab 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -599,6 +599,7 @@ impl Default for Context { // Register built-in plugins: crate::debug_text::register(&ctx); + crate::text_selection::LabelSelectionState::register(&ctx); ctx } @@ -665,7 +666,6 @@ 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)); } @@ -1680,7 +1680,6 @@ impl Context { crate::gui_zoom::zoom_with_keyboard(self); } - crate::text_selection::LabelSelectionState::end_frame(self); self.read(|ctx| ctx.plugins.clone()).on_end_frame(self); self.write(|ctx| ctx.end_frame()) 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 { From 18f42c9937a1d9e13e5a448a6a1f28eec28bd0c7 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 25 Jan 2024 11:39:19 +0100 Subject: [PATCH 5/8] Fix warnings about unused stuff --- 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 9c35edcecab..d4f2c7689e6 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -83,14 +83,14 @@ struct Plugins { } impl Plugins { - fn call(ctx: &Context, cb_name: &str, callbacks: &[NamedContextCallback]) { - crate::profile_scope!("plugins", cb_name); + fn call(ctx: &Context, _cb_name: &str, callbacks: &[NamedContextCallback]) { + crate::profile_scope!("plugins", _cb_name); for NamedContextCallback { - debug_name, + debug_name: _name, callback, } in callbacks { - crate::profile_scope!(debug_name); + crate::profile_scope!(_name); (callback)(ctx); } } From 3f3d7039eab9aab70be2063f50fc6a39a48ea392 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 25 Jan 2024 11:40:00 +0100 Subject: [PATCH 6/8] Improve docs and fix typos --- crates/egui/src/context.rs | 1 + crates/egui/src/debug_text.rs | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index d4f2c7689e6..6e73b22eebc 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1388,6 +1388,7 @@ impl Context { }; self.write(|ctx| ctx.plugins.on_begin_frame.push(named_cb)); } + /// Call the given callback at the end of each frame /// of each viewport. /// diff --git a/crates/egui/src/debug_text.rs b/crates/egui/src/debug_text.rs index dd2283a0d15..6ce9cd57d14 100644 --- a/crates/egui/src/debug_text.rs +++ b/crates/egui/src/debug_text.rs @@ -37,6 +37,9 @@ 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, @@ -51,7 +54,7 @@ struct Entry { text: WidgetText, } -/// A pluging for easily showing debug-text on-screen. +/// A plugin for easily showing debug-text on-screen. /// /// This is a built-in plugin in egui. #[derive(Clone, Default)] @@ -69,9 +72,9 @@ impl State { } fn paint(self, ctx: &Context) { - let Self { entries: entires } = self; + let Self { entries } = self; - if entires.is_empty() { + if entries.is_empty() { return; } @@ -89,7 +92,7 @@ impl State { let color = Color32::GRAY; let font_id = FontId::new(10.0, FontFamily::Proportional); - for Entry { location, text } in entires { + for Entry { location, text } in entries { { // Paint location to left of `pos`: let location_galley = From b981c4aa82cf1339ce180b1cbde7bf29b63dd8be Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 25 Jan 2024 11:46:53 +0100 Subject: [PATCH 7/8] Fix doc-test --- crates/egui/src/debug_text.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/debug_text.rs b/crates/egui/src/debug_text.rs index 6ce9cd57d14..60d118032ba 100644 --- a/crates/egui/src/debug_text.rs +++ b/crates/egui/src/debug_text.rs @@ -26,7 +26,7 @@ pub(crate) fn register(ctx: &Context) { /// ``` /// # let ctx = egui::Context::default(); /// # let state = true; -/// egui::debug_text::print(format!("State: {state:?}")); +/// egui::debug_text::print(ctx, format!("State: {state:?}")); /// ``` #[track_caller] pub fn print(ctx: &Context, text: impl Into) { From e75066cc9bdb3d7561444070c92d11e89aa2c9b8 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 25 Jan 2024 11:51:54 +0100 Subject: [PATCH 8/8] Fix doctest again --- crates/egui/src/debug_text.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/debug_text.rs b/crates/egui/src/debug_text.rs index 60d118032ba..b2551de9ddf 100644 --- a/crates/egui/src/debug_text.rs +++ b/crates/egui/src/debug_text.rs @@ -24,7 +24,7 @@ pub(crate) fn register(ctx: &Context) { /// This only works if compiled with `debug_assertions`. /// /// ``` -/// # let ctx = egui::Context::default(); +/// # let ctx = &egui::Context::default(); /// # let state = true; /// egui::debug_text::print(ctx, format!("State: {state:?}")); /// ```