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/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 4ceae307a9f..49d470ede95 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,94 @@ 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 +/// 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 = eframe::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::UpdateNext => ControlFlow::Poll, +/// DetachedResult::UpdateAt(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..fe31776f529 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,65 @@ 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>; - let mut next_repaint_time = Instant::now(); + /// 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>, + event_loop: &EventLoopWindowTarget, + ) -> Result; +} - event_loop.run(move |event, event_loop, control_flow| { - crate::profile_scope!("winit_event", short_event_description(&event)); +/// 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, +} + +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 +339,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,53 +360,97 @@ 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) { - 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 { 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)); + + 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) + } + DetachedResult::Exit => { + #[allow(clippy::exit)] + std::process::exit(0); + } }; }) } @@ -1120,8 +1216,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 +1695,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 +1733,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", 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..7f3f4022745 --- /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 detached_app +``` + +![](screenshot.png) diff --git a/examples/detached_app/screenshot.png b/examples/detached_app/screenshot.png new file mode 100644 index 00000000000..450f6eead24 Binary files /dev/null and b/examples/detached_app/screenshot.png differ 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)); + }); + } +}