Skip to content

Commit

Permalink
Register callbacks with Context::on_begin_frame and on_end_frame. (
Browse files Browse the repository at this point in the history
…#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.
  • Loading branch information
emilk authored Jan 25, 2024
1 parent e3cfcf7 commit d190df7
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 85 deletions.
2 changes: 1 addition & 1 deletion bacon.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
164 changes: 86 additions & 78 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,46 @@ impl Default for WrappedTextureManager {

// ----------------------------------------------------------------------------

/// Generic event callback.
pub type ContextCallback = Arc<dyn Fn(&Context) + Send + Sync>;

#[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<NamedContextCallback>,
pub on_end_frame: Vec<NamedContextCallback>,
}

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.
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -283,8 +320,6 @@ struct ContextImpl {
accesskit_node_classes: accesskit::NodeClassSet,

loaders: Arc<Loaders>,

debug_texts: Vec<DebugText>,
}

impl ContextImpl {
Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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));
}
}
Expand Down Expand Up @@ -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<WidgetText>) {
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?
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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())
}
Expand Down
135 changes: 135 additions & 0 deletions crates/egui/src/debug_text.rs
Original file line number Diff line number Diff line change
@@ -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<WidgetText>) {
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::<State>(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<Entry>,
}

impl State {
fn end_frame(ctx: &Context) {
let state = ctx.data_mut(|data| data.remove_temp::<Self>(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),
),
);
}
}
1 change: 1 addition & 0 deletions crates/egui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit d190df7

Please sign in to comment.