Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Register callbacks with Context::on_begin_frame and on_end_frame. #3886

Merged
merged 8 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading