diff --git a/Cargo.lock b/Cargo.lock index 4411be0c6ac..7447d2dbc8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1620,6 +1620,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" @@ -2452,9 +2472,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" @@ -2609,6 +2629,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" @@ -3865,6 +3896,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" @@ -4183,6 +4224,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" diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index ec27b05a056..aefe4333b55 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -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,6 +250,106 @@ pub fn run_native( mut native_options: NativeOptions, app_creator: AppCreator<'_>, ) -> Result { + let renderer = init_native(app_name, &mut native_options); + + match renderer { + #[cfg(feature = "glow")] + Renderer::Glow => { + log::debug!("Using the glow renderer"); + native::run::run_glow(app_name, native_options, app_creator) + } + + #[cfg(feature = "wgpu")] + Renderer::Wgpu => { + log::debug!("Using the wgpu renderer"); + native::run::run_wgpu(app_name, native_options, app_creator) + } + } +} + +/// 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::::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, +) -> 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(), @@ -256,26 +364,14 @@ pub fn run_native( #[cfg(all(feature = "glow", feature = "wgpu"))] { - match renderer { + match native_options.renderer { Renderer::Glow => "glow", Renderer::Wgpu => "wgpu", }; log::info!("Both the glow and wgpu renderers are available. Using {renderer}."); } - match renderer { - #[cfg(feature = "glow")] - Renderer::Glow => { - log::debug!("Using the glow renderer"); - native::run::run_glow(app_name, native_options, app_creator) - } - - #[cfg(feature = "wgpu")] - Renderer::Wgpu => { - log::debug!("Using the wgpu renderer"); - native::run::run_wgpu(app_name, native_options, app_creator) - } - } + renderer } // ---------------------------------------------------------------------------- diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 9bcb49686a3..8edfdbe2ef4 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -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, +) -> impl ApplicationHandler + '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, +) -> impl ApplicationHandler + '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 + 'a>, + control_flow: ControlFlow, +} + +impl ApplicationHandler 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 + '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, + timeout: Option, + ) -> 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), +} diff --git a/examples/external_eventloop/Cargo.toml b/examples/external_eventloop/Cargo.toml new file mode 100644 index 00000000000..301f3025120 --- /dev/null +++ b/examples/external_eventloop/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "external_eventloop" +version = "0.1.0" +authors = ["Will Brown "] +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 } diff --git a/examples/external_eventloop/README.md b/examples/external_eventloop/README.md new file mode 100644 index 00000000000..11b06389b43 --- /dev/null +++ b/examples/external_eventloop/README.md @@ -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 +``` diff --git a/examples/external_eventloop/src/main.rs b/examples/external_eventloop/src/main.rs new file mode 100644 index 00000000000..178c2865fa6 --- /dev/null +++ b/examples/external_eventloop/src/main.rs @@ -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::::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::::default())), + &eventloop, + ); + + eventloop.run_app(&mut winit_app)?; + + Ok(()) +} + +struct MyApp { + value: Rc>, + 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); + } + }); + } +} diff --git a/examples/external_eventloop_async/Cargo.toml b/examples/external_eventloop_async/Cargo.toml new file mode 100644 index 00000000000..399ff7c393b --- /dev/null +++ b/examples/external_eventloop_async/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "external_eventloop_async" +version = "0.1.0" +authors = ["Will Brown "] +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"] } diff --git a/examples/external_eventloop_async/README.md b/examples/external_eventloop_async/README.md new file mode 100644 index 00000000000..37755e08deb --- /dev/null +++ b/examples/external_eventloop_async/README.md @@ -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 +``` diff --git a/examples/external_eventloop_async/src/app.rs b/examples/external_eventloop_async/src/app.rs new file mode 100644 index 00000000000..de3326b19c3 --- /dev/null +++ b/examples/external_eventloop_async/src/app.rs @@ -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::::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::::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>, + 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); + } + }); + } +} diff --git a/examples/external_eventloop_async/src/main.rs b/examples/external_eventloop_async/src/main.rs new file mode 100644 index 00000000000..bbb52084fc4 --- /dev/null +++ b/examples/external_eventloop_async/src/main.rs @@ -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."); +}