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.