From b8a1ac32515130ef4d51f7ff13c5698a8e7831fb Mon Sep 17 00:00:00 2001 From: rockisch Date: Thu, 14 Sep 2023 22:31:54 -0300 Subject: [PATCH 1/4] Allow eframe apps to be detached --- crates/eframe/src/lib.rs | 90 ++++++++++++++++ crates/eframe/src/native/run.rs | 186 ++++++++++++++++++++++++++------ 2 files changed, 243 insertions(+), 33 deletions(-) diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 4ceae307a9f..e60e64fff34 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -161,6 +161,10 @@ 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::{Detached, DetachedResult}; + #[cfg(not(target_arch = "wasm32"))] #[cfg(any(feature = "glow", feature = "wgpu"))] #[cfg(feature = "persistence")] @@ -295,6 +299,92 @@ pub fn run_simple_native( // ---------------------------------------------------------------------------- +/// Execute an app in a detached state. +/// +/// This allows you to control the event loop itself, which is necessary in a few +/// ocasions, like when you need a screen not managed by `egui`. +/// +/// See [`Detached`] for more info on how to run detached apps. +/// +/// # Example +/// ``` no_run +/// fn main() { +/// let native_options = eframe::NativeOptions::default(); +/// let event_loop: winit::event_loop::EventLoop = +/// EventLoopBuilder::with_user_event().build(); +/// let mut detached = eframe::run_detached_native( +/// "MyApp", +/// &event_loop, +/// native_options, +/// Box::new(|cc| Box::new(MyEguiApp::new(cc))), +/// ); +/// event_loop.run(move |event, event_loop, control_flow| { +/// *control_flow = match detached.on_event(&event, event_loop).unwrap() { +/// DetachedResult::RepaintNext => ControlFlow::Poll, +/// DetachedResult::RepaintAt(next_repaint) => ControlFlow::WaitUntil(next_repaint), +/// DetachedResult::Exit => ControlFlow::Exit, +/// } +/// }) +/// } +/// +/// #[derive(Default)] +/// struct MyEguiApp {} +/// +/// impl MyEguiApp { +/// fn new(cc: &eframe::CreationContext<'_>) -> Self { +/// // Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals. +/// // Restore app state using cc.storage (requires the "persistence" feature). +/// // Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use +/// // for e.g. egui::PaintCallback. +/// 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!"); +/// }); +/// } +/// } +/// ``` +#[cfg(not(target_arch = "wasm32"))] +#[cfg(any(feature = "glow", feature = "wgpu"))] +#[allow(clippy::needless_pass_by_value)] +pub fn run_detached_native( + app_name: &str, + event_loop: &winit::event_loop::EventLoop, + native_options: NativeOptions, + app_creator: AppCreator, +) -> Box { + let renderer = native_options.renderer; + + match renderer { + #[cfg(feature = "glow")] + Renderer::Glow => { + log::debug!("Using the glow renderer"); + Box::new(native::run::detached_glow( + app_name, + event_loop, + native_options, + app_creator, + )) + } + #[cfg(feature = "wgpu")] + Renderer::Wgpu => { + log::debug!("Using the wgpu renderer"); + Box::new(native::run::detached_wgpu( + app_name, + event_loop, + native_options, + app_creator, + )) + } + } +} + +// ---------------------------------------------------------------------------- + /// The different problems that can occur when trying to run `eframe`. #[derive(thiserror::Error, Debug)] pub enum Error { diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 6b276e96b9e..bf97cb8a854 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -4,8 +4,9 @@ use std::time::Instant; use raw_window_handle::{HasRawDisplayHandle as _, HasRawWindowHandle as _}; -use winit::event_loop::{ - ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget, +use winit::{ + event::Event, + event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget}, }; #[cfg(feature = "accesskit")] @@ -270,14 +271,59 @@ fn run_and_return( returned_result } -fn run_and_exit(event_loop: EventLoop, mut winit_app: impl WinitApp + 'static) -> ! { - log::debug!("Entering the winit event loop (run)…"); +/// An eframe app detached from the event loop. +/// +/// This is useful to run `eframe` apps while still having control over the event loop. +/// For example, when you need to have a window managed by `egui` and another managed +/// by yourself. +/// +/// The `winit` window is managed internally, and can be accessed through [`Detached::window`]. +/// It can be closed by dropping this object. +/// +/// The app state and window drawing will only get updated on calls to [`Detached::on_event`]. +pub trait Detached { + /// Returns the managed window object + fn window(&self) -> Option<&winit::window::Window>; + /// Should be called with event loop events. + /// + /// Events targeted at windows not managed by the app will be ignored. + fn on_event( + &mut self, + event: &Event<'_, UserEvent>, + event_loop: &EventLoopWindowTarget, + ) -> Result; +} - let mut next_repaint_time = Instant::now(); +/// Indicates what is the current state of the app after a [`Detached::on_event`] call. +pub enum DetachedResult { + /// Indicates the next call to [`Detached::on_event`] will result in either a state or paint update. + /// Ideally, the event loop control should be set to [`winit::event_loop::ControlFlow::Poll`]. + UpdateNext, + /// Indicates a call to [`Detached::on_event`] will only be required after the specified time. + /// Ideally, the event loop control should be set to [`winit::event_loop::ControlFlow::WaitUntil`]. + UpdateAt(Instant), + /// Indicates the app received a close request. + Exit, +} - event_loop.run(move |event, event_loop, control_flow| { - crate::profile_scope!("winit_event", short_event_description(&event)); +struct DetachedRunner { + winit_app: T, + next_repaint_time: Instant, +} +impl DetachedRunner { + pub fn new(winit_app: T) -> Self { + Self { + winit_app, + next_repaint_time: Instant::now(), + } + } + + fn on_event_internal( + &mut self, + event: &Event<'_, UserEvent>, + event_loop: &EventLoopWindowTarget, + ) -> Result { let event_result = match event { winit::event::Event::LoopDestroyed => { log::debug!("Received Event::LoopDestroyed"); @@ -287,18 +333,18 @@ fn run_and_exit(event_loop: EventLoop, mut winit_app: impl WinitApp + // Platform-dependent event handlers to workaround a winit bug // See: https://github.com/rust-windowing/winit/issues/987 // See: https://github.com/rust-windowing/winit/issues/1619 - winit::event::Event::RedrawEventsCleared if cfg!(target_os = "windows") => { - next_repaint_time = extremely_far_future(); - winit_app.run_ui_and_paint() + winit::event::Event::RedrawEventsCleared if cfg!(windows) => { + self.next_repaint_time = extremely_far_future(); + self.winit_app.run_ui_and_paint() } - winit::event::Event::RedrawRequested(_) if !cfg!(target_os = "windows") => { - next_repaint_time = extremely_far_future(); - winit_app.run_ui_and_paint() + winit::event::Event::RedrawRequested(_) if !cfg!(windows) => { + self.next_repaint_time = extremely_far_future(); + self.winit_app.run_ui_and_paint() } winit::event::Event::UserEvent(UserEvent::RequestRepaint { when, frame_nr }) => { - if winit_app.frame_nr() == frame_nr { - EventResult::RepaintAt(when) + if self.winit_app.frame_nr() == *frame_nr { + EventResult::RepaintAt(*when) } else { EventResult::Wait // old request - we've already repainted } @@ -308,7 +354,7 @@ fn run_and_exit(event_loop: EventLoop, mut winit_app: impl WinitApp + .. }) => EventResult::Wait, // We just woke up to check next_repaint_time - event => match winit_app.on_event(event_loop, &event) { + event => match self.winit_app.on_event(event_loop, &event) { Ok(event_result) => event_result, Err(err) => { panic!("eframe encountered a fatal error: {err}"); @@ -319,42 +365,88 @@ fn run_and_exit(event_loop: EventLoop, mut winit_app: impl WinitApp + match event_result { EventResult::Wait => {} EventResult::RepaintNow => { - if cfg!(target_os = "windows") { + if cfg!(windows) { // Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280 - next_repaint_time = extremely_far_future(); - winit_app.run_ui_and_paint(); + self.next_repaint_time = extremely_far_future(); + self.winit_app.run_ui_and_paint(); } else { // Fix for https://github.com/emilk/egui/issues/2425 - next_repaint_time = Instant::now(); + self.next_repaint_time = Instant::now(); } } EventResult::RepaintNext => { - next_repaint_time = Instant::now(); + self.next_repaint_time = Instant::now(); } EventResult::RepaintAt(repaint_time) => { - next_repaint_time = next_repaint_time.min(repaint_time); + self.next_repaint_time = self.next_repaint_time.min(repaint_time); } EventResult::Exit => { log::debug!("Quitting - saving app state…"); - winit_app.save_and_destroy(); - #[allow(clippy::exit)] - std::process::exit(0); + self.winit_app.save_and_destroy(); + return Ok(DetachedResult::Exit); } } - - *control_flow = if next_repaint_time <= Instant::now() { - if let Some(window) = winit_app.window() { + if self.next_repaint_time <= Instant::now() { + if let Some(window) = self.winit_app.window() { window.request_redraw(); } - next_repaint_time = extremely_far_future(); - ControlFlow::Poll + self.next_repaint_time = extremely_far_future(); + Ok(DetachedResult::UpdateNext) } else { // WaitUntil seems to not work on iOS #[cfg(target_os = "ios")] - if let Some(window) = winit_app.window() { + if let Some(window) = runner.winit_app.window() { window.request_redraw(); } - ControlFlow::WaitUntil(next_repaint_time) + Ok(DetachedResult::UpdateAt(self.next_repaint_time)) + } + } +} + +impl Detached for DetachedRunner { + fn window(&self) -> Option<&winit::window::Window> { + self.winit_app.window() + } + + fn on_event( + &mut self, + event: &Event<'_, UserEvent>, + event_loop: &EventLoopWindowTarget, + ) -> Result { + match &event { + Event::WindowEvent { + window_id, + event: _event, + } => { + // Ignore window events for other windows + if let Some(window) = self.winit_app.window() { + if *window_id == window.id() { + return self.on_event_internal(event, event_loop); + } + } + Ok(DetachedResult::UpdateAt(self.next_repaint_time)) + } + _ => self.on_event_internal(event, event_loop), + } + } +} + +fn run_and_exit(event_loop: EventLoop, winit_app: impl WinitApp + 'static) -> ! { + log::debug!("Entering the winit event loop (run)…"); + + let mut runner = DetachedRunner::new(winit_app); + event_loop.run(move |event, event_loop, control_flow| { + crate::profile_scope!("winit_event", short_event_description(&event)); + + *control_flow = match runner.on_event_internal(&event, event_loop).unwrap() { + DetachedResult::UpdateNext => ControlFlow::Poll, + DetachedResult::UpdateAt(next_repaint_time) => { + ControlFlow::WaitUntil(next_repaint_time) + } + DetachedResult::Exit => { + #[allow(clippy::exit)] + std::process::exit(0); + } }; }) } @@ -1120,8 +1212,24 @@ mod glow_integration { run_and_exit(event_loop, glow_eframe); } } + + pub fn detached_glow( + app_name: &str, + event_loop: &EventLoop, + native_options: epi::NativeOptions, + app_creator: epi::AppCreator, + ) -> impl Detached { + DetachedRunner::new(GlowWinitApp::new( + event_loop, + app_name, + native_options, + app_creator, + )) + } } +#[cfg(feature = "glow")] +pub use glow_integration::detached_glow; #[cfg(feature = "glow")] pub use glow_integration::run_glow; // ---------------------------------------------------------------------------- @@ -1583,8 +1691,20 @@ mod wgpu_integration { run_and_exit(event_loop, wgpu_eframe); } } + + pub fn detached_wgpu( + app_name: &str, + event_loop: &EventLoop, + native_options: epi::NativeOptions, + app_creator: epi::AppCreator, + ) -> impl Detached { + let app = WgpuWinitApp::new(event_loop, app_name, native_options, app_creator); + DetachedRunner::new(app) + } } +#[cfg(feature = "wgpu")] +pub use wgpu_integration::detached_wgpu; #[cfg(feature = "wgpu")] pub use wgpu_integration::run_wgpu; @@ -1609,7 +1729,7 @@ fn extremely_far_future() -> std::time::Instant { // For the puffin profiler! #[allow(dead_code)] // Only used for profiling fn short_event_description(event: &winit::event::Event<'_, UserEvent>) -> &'static str { - use winit::event::{DeviceEvent, Event, StartCause, WindowEvent}; + use winit::event::{DeviceEvent, StartCause, WindowEvent}; match event { Event::Suspended => "Event::Suspended", From acd30d8b1e8c11588365e153ecfd60f7d3fc0036 Mon Sep 17 00:00:00 2001 From: rockisch Date: Tue, 19 Sep 2023 08:31:07 -0300 Subject: [PATCH 2/4] Add detached app example --- Cargo.lock | 9 +++ examples/detached_app/Cargo.toml | 16 +++++ examples/detached_app/README.md | 7 +++ examples/detached_app/screenshot.png | Bin 0 -> 8064 bytes examples/detached_app/src/main.rs | 91 +++++++++++++++++++++++++++ 5 files changed, 123 insertions(+) create mode 100644 examples/detached_app/Cargo.toml create mode 100644 examples/detached_app/README.md create mode 100644 examples/detached_app/screenshot.png create mode 100644 examples/detached_app/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 765f03ef041..be6d5d20e25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1002,6 +1002,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "detached_app" +version = "0.1.0" +dependencies = [ + "eframe", + "env_logger", + "winit", +] + [[package]] name = "digest" version = "0.10.7" diff --git a/examples/detached_app/Cargo.toml b/examples/detached_app/Cargo.toml new file mode 100644 index 00000000000..26475afe504 --- /dev/null +++ b/examples/detached_app/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "detached_app" +version = "0.1.0" +authors = ["rockisch "] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.70" +publish = false + + +[dependencies] +eframe = { path = "../../crates/eframe", features = [ + "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO +] } +winit = { version = "0.28" } +env_logger = "0.10" diff --git a/examples/detached_app/README.md b/examples/detached_app/README.md new file mode 100644 index 00000000000..08c58a7d229 --- /dev/null +++ b/examples/detached_app/README.md @@ -0,0 +1,7 @@ +Example showing some UI controls like `Label`, `TextEdit`, `Slider`, `Button`. + +```sh +cargo run -p hello_world +``` + +![](screenshot.png) diff --git a/examples/detached_app/screenshot.png b/examples/detached_app/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..450f6eead24dc1ed8cae0c6f85037b525642fb5a GIT binary patch literal 8064 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 SE#p7Zx{{o_EJoTq_}>6-9v_MT literal 0 HcmV?d00001 diff --git a/examples/detached_app/src/main.rs b/examples/detached_app/src/main.rs new file mode 100644 index 00000000000..e7f5cc2158b --- /dev/null +++ b/examples/detached_app/src/main.rs @@ -0,0 +1,91 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release + +use std::time::Instant; + +use eframe::{egui, DetachedResult}; +use winit::{ + event::{ElementState, Event, WindowEvent}, + event_loop::ControlFlow, +}; + +fn main() { + env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). + let options = eframe::NativeOptions { + initial_window_size: Some(egui::vec2(320.0, 240.0)), + ..Default::default() + }; + let event_loop = eframe::EventLoopBuilder::::with_user_event().build(); + + // A detached window managed by eframe + let mut detached_app = eframe::run_detached_native( + "My egui App", + &event_loop, + options, + Box::new(|_cc| Box::::default()), + ); + // Winit window managed by the application + let winit_window = winit::window::WindowBuilder::new() + .build(&event_loop) + .unwrap(); + + let mut next_paint = Instant::now(); + event_loop.run(move |event, event_loop, control_flow| { + *control_flow = match event { + // Check first for events on the managed window + Event::WindowEvent { window_id, event } if window_id == winit_window.id() => { + match event { + WindowEvent::CloseRequested => ControlFlow::Exit, + WindowEvent::MouseInput { + state: ElementState::Pressed, + .. + } => { + winit_window.set_maximized(!winit_window.is_maximized()); + ControlFlow::WaitUntil(next_paint) + } + _ => ControlFlow::WaitUntil(next_paint), + } + } + // Otherwise, let eframe process the event + _ => match detached_app.on_event(&event, event_loop).unwrap() { + DetachedResult::Exit => ControlFlow::Exit, + DetachedResult::UpdateNext => ControlFlow::Poll, + DetachedResult::UpdateAt(_next_paint) => { + next_paint = _next_paint; + ControlFlow::WaitUntil(next_paint) + } + }, + } + }); +} + +struct MyApp { + name: String, + age: u32, +} + +impl Default for MyApp { + fn default() -> Self { + Self { + name: "Arthur".to_owned(), + age: 42, + } + } +} + +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 egui Application"); + ui.horizontal(|ui| { + let name_label = ui.label("Your name: "); + ui.text_edit_singleline(&mut self.name) + .labelled_by(name_label.id); + }); + ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age")); + if ui.button("Click each year").clicked() { + self.age += 1; + } + ui.label(format!("Hello '{}', age {}", self.name, self.age)); + }); + } +} From 2385348ad33a09763d8d5575fc57092d96b22cac Mon Sep 17 00:00:00 2001 From: rockisch Date: Tue, 19 Sep 2023 08:42:05 -0300 Subject: [PATCH 3/4] Fix detached app CI issues --- crates/eframe/src/lib.rs | 12 +++++++----- crates/eframe/src/native/run.rs | 18 +++++++++++------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index e60e64fff34..49d470ede95 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -302,16 +302,18 @@ pub fn run_simple_native( /// Execute an app in a detached state. /// /// This allows you to control the event loop itself, which is necessary in a few -/// ocasions, like when you need a screen not managed by `egui`. +/// occasions, like when you need a screen not managed by `egui`. /// /// See [`Detached`] for more info on how to run detached apps. /// /// # Example /// ``` no_run +/// use eframe::DetachedResult; +/// use winit::event_loop::ControlFlow; +/// /// fn main() { /// let native_options = eframe::NativeOptions::default(); -/// let event_loop: winit::event_loop::EventLoop = -/// EventLoopBuilder::with_user_event().build(); +/// let event_loop = eframe::EventLoopBuilder::::with_user_event().build(); /// let mut detached = eframe::run_detached_native( /// "MyApp", /// &event_loop, @@ -320,8 +322,8 @@ pub fn run_simple_native( /// ); /// event_loop.run(move |event, event_loop, control_flow| { /// *control_flow = match detached.on_event(&event, event_loop).unwrap() { -/// DetachedResult::RepaintNext => ControlFlow::Poll, -/// DetachedResult::RepaintAt(next_repaint) => ControlFlow::WaitUntil(next_repaint), +/// DetachedResult::UpdateNext => ControlFlow::Poll, +/// DetachedResult::UpdateAt(next_repaint) => ControlFlow::WaitUntil(next_repaint), /// DetachedResult::Exit => ControlFlow::Exit, /// } /// }) diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index bf97cb8a854..fe31776f529 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -284,9 +284,13 @@ fn run_and_return( pub trait Detached { /// Returns the managed window object fn window(&self) -> Option<&winit::window::Window>; + /// Should be called with event loop events. /// /// Events targeted at windows not managed by the app will be ignored. + /// + /// # Errors + /// This function can fail if we fail to set up a graphics context. fn on_event( &mut self, event: &Event<'_, UserEvent>, @@ -299,9 +303,11 @@ pub enum DetachedResult { /// Indicates the next call to [`Detached::on_event`] will result in either a state or paint update. /// Ideally, the event loop control should be set to [`winit::event_loop::ControlFlow::Poll`]. UpdateNext, + /// Indicates a call to [`Detached::on_event`] will only be required after the specified time. /// Ideally, the event loop control should be set to [`winit::event_loop::ControlFlow::WaitUntil`]. UpdateAt(Instant), + /// Indicates the app received a close request. Exit, } @@ -354,12 +360,7 @@ impl DetachedRunner { .. }) => EventResult::Wait, // We just woke up to check next_repaint_time - event => match self.winit_app.on_event(event_loop, &event) { - Ok(event_result) => event_result, - Err(err) => { - panic!("eframe encountered a fatal error: {err}"); - } - }, + event => self.winit_app.on_event(event_loop, event)?, }; match event_result { @@ -438,7 +439,10 @@ fn run_and_exit(event_loop: EventLoop, winit_app: impl WinitApp + 'st event_loop.run(move |event, event_loop, control_flow| { crate::profile_scope!("winit_event", short_event_description(&event)); - *control_flow = match runner.on_event_internal(&event, event_loop).unwrap() { + let result = runner + .on_event_internal(&event, event_loop) + .expect("eframe encountered a fatal error"); + *control_flow = match result { DetachedResult::UpdateNext => ControlFlow::Poll, DetachedResult::UpdateAt(next_repaint_time) => { ControlFlow::WaitUntil(next_repaint_time) From d92e226003023f3675d2d218d3370b50845bdd03 Mon Sep 17 00:00:00 2001 From: rockisch Date: Tue, 19 Sep 2023 09:04:21 -0300 Subject: [PATCH 4/4] Fix 'detached_app' example README.md --- examples/detached_app/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/detached_app/README.md b/examples/detached_app/README.md index 08c58a7d229..7f3f4022745 100644 --- a/examples/detached_app/README.md +++ b/examples/detached_app/README.md @@ -1,7 +1,7 @@ Example showing some UI controls like `Label`, `TextEdit`, `Slider`, `Button`. ```sh -cargo run -p hello_world +cargo run -p detached_app ``` ![](screenshot.png)