diff --git a/Cargo.lock b/Cargo.lock index cbcc4c6..d274a4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1272,6 +1272,7 @@ dependencies = [ "futures", "futures-channel", "hello_egui_utils", + "parking_lot", "tokio", ] diff --git a/crates/egui_inbox/CHANGELOG.md b/crates/egui_inbox/CHANGELOG.md index d0e9722..ba99295 100644 --- a/crates/egui_inbox/CHANGELOG.md +++ b/crates/egui_inbox/CHANGELOG.md @@ -1,6 +1,14 @@ # egui_inbox changelog ## Unreleased +- egui_inbox now can be used without egui + - There is a new trait AsRequestRepaint, which can be implemented for anything that can request a repaint + - **Breaking**: new_with_ctx now takes a reference to the context + - **Breaking**: read_without_ui and replace_without_ui have been renamed to read_without_ctx and replace_without_ctx + - All other methods now take a impl AsRequestRepaint instead of a &Ui + but this should not break existing code. A benefit is that you can also + call the methods with a &Context instead of a &Ui now. + - Added `async` and `tokio` features that add the following: - `UiInbox::spawn` to conveniently spawn a task that will be cancelled when the inbox is dropped. - `UiInbox::spawn_detached` to spawn a task that will not be cancelled when the inbox is dropped. diff --git a/crates/egui_inbox/Cargo.toml b/crates/egui_inbox/Cargo.toml index 406cf7b..75d59f9 100644 --- a/crates/egui_inbox/Cargo.toml +++ b/crates/egui_inbox/Cargo.toml @@ -11,17 +11,21 @@ repository = "https://github.com/lucasmerlin/hello_egui/tree/main/crates/egui_in [features] async = ["dep:hello_egui_utils", "hello_egui_utils/async", "dep:futures-channel", "dep:futures"] tokio = ["async", "hello_egui_utils/tokio"] +egui = ["dep:egui"] +default = ["egui"] [[example]] name = "inbox_spawn" required-features = ["tokio"] [dependencies] -egui.workspace = true +egui = { workspace = true, optional = true } hello_egui_utils = { workspace = true, optional = true } futures-channel = { version = "0.3", optional = true } futures = { version = "0.3", optional = true } +# Egui uses parking_lot so we should be fine with using it too (regarding compile times). +parking_lot = "0.12" [dev-dependencies] eframe = { workspace = true, default-features = true } -tokio = { version = "1", features = ["full"]} \ No newline at end of file +tokio = { version = "1", features = ["full"] } \ No newline at end of file diff --git a/crates/egui_inbox/examples/without_egui.rs b/crates/egui_inbox/examples/without_egui.rs new file mode 100644 index 0000000..0c29bf3 --- /dev/null +++ b/crates/egui_inbox/examples/without_egui.rs @@ -0,0 +1,57 @@ +use egui_inbox::{AsRequestRepaint, RequestRepaintContext, UiInbox}; + +pub struct MyApplicationState { + state: Option, + inbox: UiInbox, + repaint_rx: std::sync::mpsc::Receiver<()>, + repaint_tx: std::sync::mpsc::Sender<()>, +} + +impl AsRequestRepaint for MyApplicationState { + fn as_request_repaint(&self) -> RequestRepaintContext { + let repaint_tx = self.repaint_tx.clone(); + RequestRepaintContext::from_callback(move || { + repaint_tx.send(()).unwrap(); + }) + } +} + +impl Default for MyApplicationState { + fn default() -> Self { + let (repaint_tx, repaint_rx) = std::sync::mpsc::channel(); + Self { + state: None, + inbox: UiInbox::new(), + repaint_rx, + repaint_tx, + } + } +} + +impl MyApplicationState { + pub fn run(mut self) { + let sender = self.inbox.sender(); + std::thread::spawn(move || { + let mut count = 0; + loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + count += 1; + sender.send(format!("Count: {}", count)).ok(); + } + }); + + loop { + self.inbox.read(&self).for_each(|msg| { + self.state = Some(msg); + }); + + println!("State: {:?}", self.state); + + self.repaint_rx.recv().unwrap(); + } + } +} + +fn main() { + MyApplicationState::default().run(); +} diff --git a/crates/egui_inbox/src/lib.rs b/crates/egui_inbox/src/lib.rs index e92e296..2b769c8 100644 --- a/crates/egui_inbox/src/lib.rs +++ b/crates/egui_inbox/src/lib.rs @@ -6,8 +6,95 @@ use std::fmt::Debug; use std::mem; use std::sync::Arc; -use egui::mutex::Mutex; -use egui::{Context, Ui}; +use parking_lot::Mutex; + +/// Trait to request a repaint. +pub trait RequestRepaintTrait { + /// Request a repaint. + fn request_repaint(&self); +} + +impl RequestRepaintTrait for F +where + F: Fn() + Send + Sync + 'static, +{ + fn request_repaint(&self) { + self(); + } +} + +enum RequestRepaintInner { + #[cfg(feature = "egui")] + Ctx(egui::Context), + Box(Box), +} + +/// Usually holds a reference to [egui::Context], but can also hold a boxed callback. +#[derive(Debug)] +pub struct RequestRepaintContext(RequestRepaintInner); + +impl RequestRepaintContext { + /// Create a new [RequestRepaintContext] from a callback function. + pub fn from_callback(f: F) -> Self + where + F: Fn() + Send + Sync + 'static, + { + Self(RequestRepaintInner::Box(Box::new(f))) + } + + /// Create a new [RequestRepaintContext] from something that implements [RequestRepaintTrait]. + pub fn from_trait(t: T) -> Self + where + T: RequestRepaintTrait + Send + Sync + 'static, + { + Self(RequestRepaintInner::Box(Box::new(t))) + } + + /// Create a new [RequestRepaintContext] from an [egui::Context]. + #[cfg(feature = "egui")] + pub fn from_egui_ctx(ctx: egui::Context) -> Self { + Self(RequestRepaintInner::Ctx(ctx)) + } +} + +impl RequestRepaintContext { + fn request_repaint(&self) { + match &self.0 { + RequestRepaintInner::Ctx(ctx) => ctx.request_repaint(), + RequestRepaintInner::Box(boxed) => boxed.request_repaint(), + } + } +} + +impl Debug for RequestRepaintInner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RequestRepaint").finish_non_exhaustive() + } +} + +/// Trait to get a [RequestRepaintContext] from. +pub trait AsRequestRepaint { + /// Should return a [RequestRepaintContext] that can be used to request a repaint. + fn as_request_repaint(&self) -> RequestRepaintContext; +} + +#[cfg(feature = "egui")] +mod egui_impl { + use crate::{AsRequestRepaint, RequestRepaintContext}; + use egui::Context; + + impl AsRequestRepaint for Context { + fn as_request_repaint(&self) -> RequestRepaintContext { + RequestRepaintContext::from_egui_ctx(self.clone()) + } + } + + impl AsRequestRepaint for egui::Ui { + fn as_request_repaint(&self) -> RequestRepaintContext { + RequestRepaintContext::from_egui_ctx(self.ctx().clone()) + } + } +} /// Utility to send messages to egui views from async functions, callbacks, etc. without /// having to use interior mutability. @@ -42,7 +129,6 @@ use egui::{Context, Ui}; /// ) /// } /// ``` - pub struct UiInbox { state: Arc>>, #[cfg(feature = "async")] @@ -57,13 +143,13 @@ impl Debug for UiInbox { #[derive(Debug)] struct State { - ctx: Option, + ctx: Option, queue: Vec, dropped: bool, } impl State { - fn new(ctx: Option) -> Self { + fn new(ctx: Option) -> Self { Self { ctx, queue: Vec::new(), @@ -121,11 +207,11 @@ impl UiInbox { } /// Create a new inbox with a context. - pub fn new_with_ctx(ctx: Context) -> Self { - Self::_new(Some(ctx)) + pub fn new_with_ctx(ctx: &impl AsRequestRepaint) -> Self { + Self::_new(Some(ctx.as_request_repaint())) } - fn _new(ctx: Option) -> Self { + fn _new(ctx: Option) -> Self { let state = Arc::new(Mutex::new(State::new(ctx))); Self { state, @@ -142,7 +228,7 @@ impl UiInbox { } /// Create a inbox with a context and a sender for it. - pub fn channel_with_ctx(ctx: Context) -> (UiInboxSender, Self) { + pub fn channel_with_ctx(ctx: &impl AsRequestRepaint) -> (UiInboxSender, Self) { let inbox = Self::new_with_ctx(ctx); let sender = inbox.sender(); (sender, inbox) @@ -150,8 +236,8 @@ impl UiInbox { /// Set the [Context] to use for requesting repaints. /// Usually this is not needed, since the [Context] is grabbed from the [Ui] passed to [UiInbox::read]. - pub fn set_ctx(&mut self, ctx: Context) { - self.state.lock().ctx = Some(ctx); + pub fn set_ctx(&mut self, ctx: &impl AsRequestRepaint) { + self.state.lock().ctx = Some(ctx.as_request_repaint()); } /// Returns an iterator over all items sent to the inbox. @@ -160,10 +246,10 @@ impl UiInbox { /// The ui is only passed here so we can grab a reference to [Context]. /// This is mostly done for convenience, so you don't have to pass a reference to [Context] /// to every struct that uses an inbox on creation. - pub fn read(&self, ui: &mut Ui) -> impl Iterator { + pub fn read(&self, ui: &impl AsRequestRepaint) -> impl Iterator { let mut state = self.state.lock(); if state.ctx.is_none() { - state.ctx = Some(ui.ctx().clone()); + state.ctx = Some(ui.as_request_repaint()); } mem::take(&mut state.queue).into_iter() } @@ -171,7 +257,7 @@ impl UiInbox { /// Same as [UiInbox::read], but you don't need to pass a reference to [Ui]. /// If you use this, make sure you set the [Context] with [UiInbox::set_ctx] or /// [UiInbox::new_with_ctx] manually. - pub fn read_without_ui(&self) -> impl Iterator { + pub fn read_without_ctx(&self) -> impl Iterator { let mut state = self.state.lock(); mem::take(&mut state.queue).into_iter() } @@ -184,10 +270,10 @@ impl UiInbox { /// The ui is only passed here so we can grab a reference to [Context]. /// This is mostly done for convenience, so you don't have to pass a reference to [Context] /// to every struct that uses an inbox on creation. - pub fn replace(&self, ui: &mut Ui, target: &mut T) -> bool { + pub fn replace(&self, ui: &impl AsRequestRepaint, target: &mut T) -> bool { let mut state = self.state.lock(); if state.ctx.is_none() { - state.ctx = Some(ui.ctx().clone()); + state.ctx = Some(ui.as_request_repaint()); } let item = mem::take(&mut state.queue).pop(); @@ -202,7 +288,7 @@ impl UiInbox { /// Same as [UiInbox::replace], but you don't need to pass a reference to [Ui]. /// If you use this, make sure you set the [Context] with [UiInbox::set_ctx] or /// [UiInbox::new_with_ctx] manually. - pub fn replace_without_ui(&self, target: &mut T) -> bool { + pub fn replace_without_ctx(&self, target: &mut T) -> bool { let mut state = self.state.lock(); let item = mem::take(&mut state.queue).pop(); if let Some(item) = item { diff --git a/fancy-example/src/main.rs b/fancy-example/src/main.rs index 9217667..6d957ed 100644 --- a/fancy-example/src/main.rs +++ b/fancy-example/src/main.rs @@ -73,8 +73,8 @@ impl App { impl eframe::App for App { fn update(&mut self, ctx: &Context, _frame: &mut Frame) { - self.inbox.set_ctx(ctx.clone()); - self.inbox.read_without_ui().for_each(|msg| match msg { + self.inbox.set_ctx(ctx); + self.inbox.read_without_ctx().for_each(|msg| match msg { FancyMessage::SelectPage(active) => { self.sidebar.active = active; }