mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 22:53:14 -04:00
Add external eventloop support (#6750)
<!-- Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md) before opening a Pull Request! * Keep your PR:s small and focused. * The PR title is what ends up in the changelog, so make it descriptive! * If applicable, add a screenshot or gif. * If it is a non-trivial addition, consider adding a demo for it to `egui_demo_lib`, or a new example. * Do NOT open PR:s from your `master` branch, as that makes it hard for maintainers to test and add commits to your PR. * Remember to run `cargo fmt` and `cargo clippy`. * Open the PR as a draft until you have self-reviewed it and run `./scripts/check.sh`. * When you have addressed a PR comment, mark it as resolved. Please be patient! I will review your PR, but my time is limited! --> * Closes #2875 * Closes https://github.com/emilk/egui/pull/3340 * [x] I have followed the instructions in the PR template Adds `create_native`. Similiar to `run_native` but it returns an `EframeWinitApplication` which is a `winit::ApplicationHandler`. This can be run on your own event loop. A helper fn `pump_eframe_app` is provided to pump the event loop and get the control flow state back. I have been using this approach for a few months. --------- Co-authored-by: Will Brown <opensource@rebeagle.com>
This commit is contained in:
35
examples/external_eventloop_async/Cargo.toml
Normal file
35
examples/external_eventloop_async/Cargo.toml
Normal file
@@ -0,0 +1,35 @@
|
||||
[package]
|
||||
name = "external_eventloop_async"
|
||||
version = "0.1.0"
|
||||
authors = ["Will Brown <opensource@rebeagle.com>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.84"
|
||||
publish = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
linux-example = []
|
||||
|
||||
[[bin]]
|
||||
name = "external_eventloop_async"
|
||||
required-features = ["linux-example"]
|
||||
|
||||
[dependencies]
|
||||
eframe = { workspace = true, features = [
|
||||
"default",
|
||||
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
|
||||
] }
|
||||
|
||||
env_logger = { version = "0.10", default-features = false, features = [
|
||||
"auto-color",
|
||||
"humantime",
|
||||
] }
|
||||
|
||||
log = { workspace = true }
|
||||
|
||||
winit = { workspace = true }
|
||||
|
||||
tokio = { version = "1", features = ["rt", "time", "net"] }
|
||||
10
examples/external_eventloop_async/README.md
Normal file
10
examples/external_eventloop_async/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
Example running an eframe application on an external eventloop on top of a tokio executor on Linux.
|
||||
|
||||
By running the event loop, eframe, and tokio in the same thread, one can leverage local async tasks.
|
||||
These tasks can share data with the UI without the need for locks or message passing.
|
||||
|
||||
In tokio CPU-bound async tasks can be run with `spawn_blocking` to avoid impacting the UI frame rate.
|
||||
|
||||
```sh
|
||||
cargo run -p external_eventloop_async --features linux-example
|
||||
```
|
||||
130
examples/external_eventloop_async/src/app.rs
Normal file
130
examples/external_eventloop_async/src/app.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
use eframe::{egui, EframePumpStatus, UserEvent};
|
||||
use std::{cell::Cell, io, os::fd::AsRawFd as _, rc::Rc, time::Duration};
|
||||
use tokio::task::LocalSet;
|
||||
use winit::event_loop::{ControlFlow, EventLoop};
|
||||
|
||||
pub fn run() -> io::Result<()> {
|
||||
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut eventloop = EventLoop::<UserEvent>::with_user_event().build().unwrap();
|
||||
eventloop.set_control_flow(ControlFlow::Poll);
|
||||
|
||||
let mut winit_app = eframe::create_native(
|
||||
"External Eventloop Application",
|
||||
options,
|
||||
Box::new(|_| Ok(Box::<MyApp>::default())),
|
||||
&eventloop,
|
||||
);
|
||||
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let local = LocalSet::new();
|
||||
local.block_on(&rt, async {
|
||||
let eventloop_fd = tokio::io::unix::AsyncFd::new(eventloop.as_raw_fd())?;
|
||||
let mut control_flow = ControlFlow::Poll;
|
||||
|
||||
loop {
|
||||
let mut guard = match control_flow {
|
||||
ControlFlow::Poll => None,
|
||||
ControlFlow::Wait => Some(eventloop_fd.readable().await?),
|
||||
ControlFlow::WaitUntil(deadline) => {
|
||||
tokio::time::timeout_at(deadline.into(), eventloop_fd.readable())
|
||||
.await
|
||||
.ok()
|
||||
.transpose()?
|
||||
}
|
||||
};
|
||||
|
||||
match winit_app.pump_eframe_app(&mut eventloop, None) {
|
||||
EframePumpStatus::Continue(next) => control_flow = next,
|
||||
EframePumpStatus::Exit(code) => {
|
||||
log::info!("exit code: {code}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut guard) = guard.take() {
|
||||
guard.clear_ready();
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<_, io::Error>(())
|
||||
})
|
||||
}
|
||||
|
||||
struct MyApp {
|
||||
value: Rc<Cell<u32>>,
|
||||
spin: bool,
|
||||
blinky: bool,
|
||||
}
|
||||
|
||||
impl Default for MyApp {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
value: Rc::new(Cell::new(42)),
|
||||
spin: false,
|
||||
blinky: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for MyApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.heading("My External Eventloop Application");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Increment Now").clicked() {
|
||||
self.value.set(self.value.get() + 1);
|
||||
}
|
||||
if ui.button("Increment Later").clicked() {
|
||||
let value = self.value.clone();
|
||||
let ctx = ctx.clone();
|
||||
tokio::task::spawn_local(async move {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
value.set(value.get() + 1);
|
||||
ctx.request_repaint();
|
||||
});
|
||||
}
|
||||
});
|
||||
ui.label(format!("Value: {}", self.value.get()));
|
||||
|
||||
if ui.button("Toggle Spinner").clicked() {
|
||||
self.spin = !self.spin;
|
||||
}
|
||||
|
||||
if ui.button("Toggle Blinky").clicked() {
|
||||
self.blinky = !self.blinky;
|
||||
}
|
||||
|
||||
if self.spin {
|
||||
ui.spinner();
|
||||
}
|
||||
|
||||
if self.blinky {
|
||||
let now = ui.ctx().input(|i| i.time);
|
||||
let blink = now % 1.0 < 0.5;
|
||||
egui::Frame::new()
|
||||
.inner_margin(3)
|
||||
.corner_radius(5)
|
||||
.fill(if blink {
|
||||
egui::Color32::RED
|
||||
} else {
|
||||
egui::Color32::TRANSPARENT
|
||||
})
|
||||
.show(ui, |ui| {
|
||||
ui.label("Blinky!");
|
||||
});
|
||||
|
||||
ctx.request_repaint_after_secs((0.5 - (now % 0.5)) as f32);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
15
examples/external_eventloop_async/src/main.rs
Normal file
15
examples/external_eventloop_async/src/main.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
#![allow(rustdoc::missing_crate_level_docs)] // it's an example
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod app;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn main() -> std::io::Result<()> {
|
||||
app::run()
|
||||
}
|
||||
|
||||
// Do not check `app` on unsupported platforms when check "--all-features" is used in CI.
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn main() {
|
||||
println!("This example only supports Linux.");
|
||||
}
|
||||
Reference in New Issue
Block a user