Skip to content

Commit

Permalink
Make inbox work without egui
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasmerlin committed Feb 9, 2024
1 parent c4074f5 commit 092e9b1
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 21 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions crates/egui_inbox/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
8 changes: 6 additions & 2 deletions crates/egui_inbox/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]}
tokio = { version = "1", features = ["full"] }
57 changes: 57 additions & 0 deletions crates/egui_inbox/examples/without_egui.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use egui_inbox::{AsRequestRepaint, RequestRepaintContext, UiInbox};

pub struct MyApplicationState {
state: Option<String>,
inbox: UiInbox<String>,
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();
}
120 changes: 103 additions & 17 deletions crates/egui_inbox/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<F> RequestRepaintTrait for F
where
F: Fn() + Send + Sync + 'static,
{
fn request_repaint(&self) {
self();
}
}

enum RequestRepaintInner {
#[cfg(feature = "egui")]
Ctx(egui::Context),
Box(Box<dyn RequestRepaintTrait + Send + Sync>),
}

/// 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: 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: 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.
Expand Down Expand Up @@ -42,7 +129,6 @@ use egui::{Context, Ui};
/// )
/// }
/// ```
pub struct UiInbox<T> {
state: Arc<Mutex<State<T>>>,
#[cfg(feature = "async")]
Expand All @@ -57,13 +143,13 @@ impl<T> Debug for UiInbox<T> {

#[derive(Debug)]
struct State<T> {
ctx: Option<Context>,
ctx: Option<RequestRepaintContext>,
queue: Vec<T>,
dropped: bool,
}

impl<T> State<T> {
fn new(ctx: Option<Context>) -> Self {
fn new(ctx: Option<RequestRepaintContext>) -> Self {
Self {
ctx,
queue: Vec::new(),
Expand Down Expand Up @@ -121,11 +207,11 @@ impl<T> UiInbox<T> {
}

/// 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<Context>) -> Self {
fn _new(ctx: Option<RequestRepaintContext>) -> Self {
let state = Arc::new(Mutex::new(State::new(ctx)));
Self {
state,
Expand All @@ -142,16 +228,16 @@ impl<T> UiInbox<T> {
}

/// Create a inbox with a context and a sender for it.
pub fn channel_with_ctx(ctx: Context) -> (UiInboxSender<T>, Self) {
pub fn channel_with_ctx(ctx: &impl AsRequestRepaint) -> (UiInboxSender<T>, Self) {
let inbox = Self::new_with_ctx(ctx);
let sender = inbox.sender();
(sender, inbox)
}

/// 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.
Expand All @@ -160,18 +246,18 @@ impl<T> UiInbox<T> {
/// 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<Item = T> {
pub fn read(&self, ui: &impl AsRequestRepaint) -> impl Iterator<Item = T> {
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()
}

/// 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<Item = T> {
pub fn read_without_ctx(&self) -> impl Iterator<Item = T> {
let mut state = self.state.lock();
mem::take(&mut state.queue).into_iter()
}
Expand All @@ -184,10 +270,10 @@ impl<T> UiInbox<T> {
/// 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();
Expand All @@ -202,7 +288,7 @@ impl<T> UiInbox<T> {
/// 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 {
Expand Down
4 changes: 2 additions & 2 deletions fancy-example/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down

0 comments on commit 092e9b1

Please sign in to comment.