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:
parent
fed2ab5df3
commit
c075053391
59
Cargo.lock
59
Cargo.lock
|
|
@ -1614,6 +1614,26 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "external_eventloop"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"env_logger",
|
||||
"winit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "external_eventloop_async"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"env_logger",
|
||||
"log",
|
||||
"tokio",
|
||||
"winit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fancy-regex"
|
||||
version = "0.11.0"
|
||||
|
|
@ -2446,9 +2466,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.161"
|
||||
version = "0.2.168"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
|
||||
checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
|
|
@ -2603,6 +2623,17 @@ version = "0.5.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multiple_viewports"
|
||||
version = "0.1.0"
|
||||
|
|
@ -3860,6 +3891,16 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spirv"
|
||||
version = "0.3.0+sdk-1.3.268.0"
|
||||
|
|
@ -4178,6 +4219,20 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.44.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.8"
|
||||
|
|
|
|||
|
|
@ -182,6 +182,14 @@ pub use web::{WebLogger, WebRunner};
|
|||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
mod native;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
pub use native::run::EframeWinitApplication;
|
||||
|
||||
#[cfg(not(any(target_arch = "wasm32", target_os = "ios")))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
pub use native::run::EframePumpStatus;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(feature = "persistence")]
|
||||
|
|
@ -242,26 +250,7 @@ pub fn run_native(
|
|||
mut native_options: NativeOptions,
|
||||
app_creator: AppCreator<'_>,
|
||||
) -> Result {
|
||||
#[cfg(not(feature = "__screenshot"))]
|
||||
assert!(
|
||||
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
|
||||
"EFRAME_SCREENSHOT_TO found without compiling with the '__screenshot' feature"
|
||||
);
|
||||
|
||||
if native_options.viewport.title.is_none() {
|
||||
native_options.viewport.title = Some(app_name.to_owned());
|
||||
}
|
||||
|
||||
let renderer = native_options.renderer;
|
||||
|
||||
#[cfg(all(feature = "glow", feature = "wgpu"))]
|
||||
{
|
||||
match renderer {
|
||||
Renderer::Glow => "glow",
|
||||
Renderer::Wgpu => "wgpu",
|
||||
};
|
||||
log::info!("Both the glow and wgpu renderers are available. Using {renderer}.");
|
||||
}
|
||||
let renderer = init_native(app_name, &mut native_options);
|
||||
|
||||
match renderer {
|
||||
#[cfg(feature = "glow")]
|
||||
|
|
@ -278,6 +267,113 @@ pub fn run_native(
|
|||
}
|
||||
}
|
||||
|
||||
/// Provides a proxy for your native eframe application to run on your own event loop.
|
||||
///
|
||||
/// See `run_native` for details about `app_name`.
|
||||
///
|
||||
/// Call from `fn main` like this:
|
||||
/// ``` no_run
|
||||
/// use eframe::{egui, UserEvent};
|
||||
/// use winit::event_loop::{ControlFlow, EventLoop};
|
||||
///
|
||||
/// fn main() -> eframe::Result {
|
||||
/// let native_options = eframe::NativeOptions::default();
|
||||
/// let eventloop = EventLoop::<UserEvent>::with_user_event().build()?;
|
||||
/// eventloop.set_control_flow(ControlFlow::Poll);
|
||||
///
|
||||
/// let mut winit_app = eframe::create_native(
|
||||
/// "MyExtApp",
|
||||
/// native_options,
|
||||
/// Box::new(|cc| Ok(Box::new(MyEguiApp::new(cc)))),
|
||||
/// &eventloop,
|
||||
/// );
|
||||
///
|
||||
/// eventloop.run_app(&mut winit_app)?;
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Default)]
|
||||
/// struct MyEguiApp {}
|
||||
///
|
||||
/// impl MyEguiApp {
|
||||
/// fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
/// Self::default()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl eframe::App for MyEguiApp {
|
||||
/// fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
/// egui::CentralPanel::default().show(ctx, |ui| {
|
||||
/// ui.heading("Hello World!");
|
||||
/// });
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// See the `external_eventloop` example for a more complete example.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
pub fn create_native<'a>(
|
||||
app_name: &str,
|
||||
mut native_options: NativeOptions,
|
||||
app_creator: AppCreator<'a>,
|
||||
event_loop: &winit::event_loop::EventLoop<UserEvent>,
|
||||
) -> EframeWinitApplication<'a> {
|
||||
let renderer = init_native(app_name, &mut native_options);
|
||||
|
||||
match renderer {
|
||||
#[cfg(feature = "glow")]
|
||||
Renderer::Glow => {
|
||||
log::debug!("Using the glow renderer");
|
||||
EframeWinitApplication::new(native::run::create_glow(
|
||||
app_name,
|
||||
native_options,
|
||||
app_creator,
|
||||
event_loop,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
Renderer::Wgpu => {
|
||||
log::debug!("Using the wgpu renderer");
|
||||
EframeWinitApplication::new(native::run::create_wgpu(
|
||||
app_name,
|
||||
native_options,
|
||||
app_creator,
|
||||
event_loop,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
fn init_native(app_name: &str, native_options: &mut NativeOptions) -> Renderer {
|
||||
#[cfg(not(feature = "__screenshot"))]
|
||||
assert!(
|
||||
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
|
||||
"EFRAME_SCREENSHOT_TO found without compiling with the '__screenshot' feature"
|
||||
);
|
||||
|
||||
if native_options.viewport.title.is_none() {
|
||||
native_options.viewport.title = Some(app_name.to_owned());
|
||||
}
|
||||
|
||||
let renderer = native_options.renderer;
|
||||
|
||||
#[cfg(all(feature = "glow", feature = "wgpu"))]
|
||||
{
|
||||
match native_options.renderer {
|
||||
Renderer::Glow => "glow",
|
||||
Renderer::Wgpu => "wgpu",
|
||||
};
|
||||
log::info!("Both the glow and wgpu renderers are available. Using {renderer}.");
|
||||
}
|
||||
|
||||
renderer
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// The simplest way to get started when writing a native app.
|
||||
|
|
|
|||
|
|
@ -362,6 +362,19 @@ pub fn run_glow(
|
|||
run_and_exit(event_loop, glow_eframe)
|
||||
}
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
pub fn create_glow<'a>(
|
||||
app_name: &str,
|
||||
native_options: epi::NativeOptions,
|
||||
app_creator: epi::AppCreator<'a>,
|
||||
event_loop: &EventLoop<UserEvent>,
|
||||
) -> impl ApplicationHandler<UserEvent> + 'a {
|
||||
use super::glow_integration::GlowWinitApp;
|
||||
|
||||
let glow_eframe = GlowWinitApp::new(event_loop, app_name, native_options, app_creator);
|
||||
WinitAppWrapper::new(glow_eframe, true)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
|
|
@ -386,3 +399,120 @@ pub fn run_wgpu(
|
|||
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
|
||||
run_and_exit(event_loop, wgpu_eframe)
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
pub fn create_wgpu<'a>(
|
||||
app_name: &str,
|
||||
native_options: epi::NativeOptions,
|
||||
app_creator: epi::AppCreator<'a>,
|
||||
event_loop: &EventLoop<UserEvent>,
|
||||
) -> impl ApplicationHandler<UserEvent> + 'a {
|
||||
use super::wgpu_integration::WgpuWinitApp;
|
||||
|
||||
let wgpu_eframe = WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
|
||||
WinitAppWrapper::new(wgpu_eframe, true)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A proxy to the eframe application that implements [`ApplicationHandler`].
|
||||
///
|
||||
/// This can be run directly on your own [`EventLoop`] by itself or with other
|
||||
/// windows you manage outside of eframe.
|
||||
pub struct EframeWinitApplication<'a> {
|
||||
wrapper: Box<dyn ApplicationHandler<UserEvent> + 'a>,
|
||||
control_flow: ControlFlow,
|
||||
}
|
||||
|
||||
impl ApplicationHandler<UserEvent> for EframeWinitApplication<'_> {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.wrapper.resumed(event_loop);
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
window_id: winit::window::WindowId,
|
||||
event: winit::event::WindowEvent,
|
||||
) {
|
||||
self.wrapper.window_event(event_loop, window_id, event);
|
||||
}
|
||||
|
||||
fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: winit::event::StartCause) {
|
||||
self.wrapper.new_events(event_loop, cause);
|
||||
}
|
||||
|
||||
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
|
||||
self.wrapper.user_event(event_loop, event);
|
||||
}
|
||||
|
||||
fn device_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
device_id: winit::event::DeviceId,
|
||||
event: winit::event::DeviceEvent,
|
||||
) {
|
||||
self.wrapper.device_event(event_loop, device_id, event);
|
||||
}
|
||||
|
||||
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.wrapper.about_to_wait(event_loop);
|
||||
self.control_flow = event_loop.control_flow();
|
||||
}
|
||||
|
||||
fn suspended(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.wrapper.suspended(event_loop);
|
||||
}
|
||||
|
||||
fn exiting(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.wrapper.exiting(event_loop);
|
||||
}
|
||||
|
||||
fn memory_warning(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.wrapper.memory_warning(event_loop);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> EframeWinitApplication<'a> {
|
||||
pub(crate) fn new<T: ApplicationHandler<UserEvent> + 'a>(app: T) -> Self {
|
||||
Self {
|
||||
wrapper: Box::new(app),
|
||||
control_flow: ControlFlow::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pump the `EventLoop` to check for and dispatch pending events to this application.
|
||||
///
|
||||
/// Returns either the exit code for the application or the final state of the [`ControlFlow`]
|
||||
/// after all events have been dispatched in this iteration.
|
||||
///
|
||||
/// This is useful when your [`EventLoop`] is not the main event loop for your application.
|
||||
/// See the `external_eventloop_async` example.
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub fn pump_eframe_app(
|
||||
&mut self,
|
||||
event_loop: &mut EventLoop<UserEvent>,
|
||||
timeout: Option<std::time::Duration>,
|
||||
) -> EframePumpStatus {
|
||||
use winit::platform::pump_events::{EventLoopExtPumpEvents as _, PumpStatus};
|
||||
|
||||
match event_loop.pump_app_events(timeout, self) {
|
||||
PumpStatus::Continue => EframePumpStatus::Continue(self.control_flow),
|
||||
PumpStatus::Exit(code) => EframePumpStatus::Exit(code),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Either an exit code or a [`ControlFlow`] from the [`ActiveEventLoop`].
|
||||
///
|
||||
/// The result of [`EframeWinitApplication::pump_eframe_app`].
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub enum EframePumpStatus {
|
||||
/// The final state of the [`ControlFlow`] after all events have been dispatched
|
||||
///
|
||||
/// Callers should perform the action that is appropriate for the [`ControlFlow`] value.
|
||||
Continue(ControlFlow),
|
||||
|
||||
/// The exit code for the application
|
||||
Exit(i32),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "external_eventloop"
|
||||
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
|
||||
|
||||
|
||||
[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",
|
||||
] }
|
||||
|
||||
winit = { workspace = true }
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
Example running an eframe application on an external eventloop.
|
||||
|
||||
This allows you to run your eframe application alongside other windows and/or toolkits on the same event loop.
|
||||
|
||||
```sh
|
||||
cargo run -p external_eventloop
|
||||
```
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||
#![allow(rustdoc::missing_crate_level_docs)] // it's an example
|
||||
|
||||
use eframe::{egui, UserEvent};
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
use winit::event_loop::{ControlFlow, EventLoop};
|
||||
|
||||
fn main() -> eframe::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 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,
|
||||
);
|
||||
|
||||
eventloop.run_app(&mut winit_app)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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"] }
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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.");
|
||||
}
|
||||
Loading…
Reference in New Issue