diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index a861d26a7bd..f71046c36f1 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -5,7 +5,8 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C ## Unreleased - +* Added `NativeOptions::event_loop_builder` hook for apps to change platform specific event loop options ([#1952](https://github.com/emilk/egui/pull/1952)). +* Enabled deferred render state initialization to support Android ([#1952](https://github.com/emilk/egui/pull/1952)). ## 0.19.0 - 2022-08-20 * MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)). diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index 9fa339bdb6f..ee42b06c7a6 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -6,6 +6,18 @@ #![warn(missing_docs)] // Let's keep `epi` well-documented. +#[cfg(not(target_arch = "wasm32"))] +pub use crate::native::run::RequestRepaintEvent; +#[cfg(not(target_arch = "wasm32"))] +pub use winit::event_loop::EventLoopBuilder; + +/// Hook into the building of an event loop before it is run +/// +/// You can configure any platform specific details required on top of the default configuration +/// done by `EFrame`. +#[cfg(not(target_arch = "wasm32"))] +pub type EventLoopBuilderHook = Box)>; + /// This is how your app is created. /// /// You can use the [`CreationContext`] to setup egui, restore state, setup OpenGL things, etc. @@ -177,7 +189,6 @@ pub enum HardwareAcceleration { /// /// Only a single native window is currently supported. #[cfg(not(target_arch = "wasm32"))] -#[derive(Clone)] pub struct NativeOptions { /// Sets whether or not the window will always be on top of other windows. pub always_on_top: bool, @@ -292,6 +303,25 @@ pub struct NativeOptions { /// When `true`, [`winit::platform::run_return::EventLoopExtRunReturn::run_return`] is used. /// When `false`, [`winit::event_loop::EventLoop::run`] is used. pub run_and_return: bool, + + /// Hook into the building of an event loop before it is run. + /// + /// Specify a callback here in case you need to make platform specific changes to the + /// event loop before it is run. + /// + /// Note: A [`NativeOptions`] clone will not include any `event_loop_builder` hook. + pub event_loop_builder: Option, +} + +#[cfg(not(target_arch = "wasm32"))] +impl Clone for NativeOptions { + fn clone(&self) -> Self { + Self { + icon_data: self.icon_data.clone(), + event_loop_builder: None, // Skip any builder callbacks if cloning + ..*self + } + } } #[cfg(not(target_arch = "wasm32"))] @@ -319,6 +349,7 @@ impl Default for NativeOptions { follow_system_theme: cfg!(target_os = "macos") || cfg!(target_os = "windows"), default_theme: Theme::Dark, run_and_return: true, + event_loop_builder: None, } } } diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 42ddc943362..088382cb5d5 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -169,13 +169,13 @@ pub fn run_native(app_name: &str, native_options: NativeOptions, app_creator: Ap #[cfg(feature = "glow")] Renderer::Glow => { tracing::debug!("Using the glow renderer"); - native::run::run_glow(app_name, &native_options, app_creator); + native::run::run_glow(app_name, native_options, app_creator); } #[cfg(feature = "wgpu")] Renderer::Wgpu => { tracing::debug!("Using the wgpu renderer"); - native::run::run_wgpu(app_name, &native_options, app_creator); + native::run::run_wgpu(app_name, native_options, app_creator); } } } diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 8188e7049db..26dc17cca9f 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -5,52 +5,15 @@ use std::time::Duration; use std::time::Instant; use egui_winit::winit; -use winit::event_loop::{ControlFlow, EventLoop}; +use winit::event_loop::{ + ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget, +}; use super::epi_integration::{self, EpiIntegration}; use crate::epi; #[derive(Debug)] -struct RequestRepaintEvent; - -#[cfg(feature = "glow")] -#[allow(unsafe_code)] -fn create_display( - native_options: &NativeOptions, - window_builder: winit::window::WindowBuilder, - event_loop: &EventLoop, -) -> ( - glutin::WindowedContext, - glow::Context, -) { - crate::profile_function!(); - - use crate::HardwareAcceleration; - - let hardware_acceleration = match native_options.hardware_acceleration { - HardwareAcceleration::Required => Some(true), - HardwareAcceleration::Preferred => None, - HardwareAcceleration::Off => Some(false), - }; - - let gl_window = unsafe { - glutin::ContextBuilder::new() - .with_hardware_acceleration(hardware_acceleration) - .with_depth_buffer(native_options.depth_buffer) - .with_multisampling(native_options.multisampling) - .with_srgb(true) - .with_stencil_buffer(native_options.stencil_buffer) - .with_vsync(native_options.vsync) - .build_windowed(window_builder, event_loop) - .unwrap() - .make_current() - .unwrap() - }; - - let gl = unsafe { glow::Context::from_loader_function(|s| gl_window.get_proc_address(s)) }; - - (gl_window, gl) -} +pub struct RequestRepaintEvent; // ---------------------------------------------------------------------------- @@ -65,23 +28,48 @@ enum EventResult { trait WinitApp { fn is_focused(&self) -> bool; - fn integration(&self) -> &EpiIntegration; - fn window(&self) -> &winit::window::Window; + fn integration(&self) -> Option<&EpiIntegration>; + fn window(&self) -> Option<&winit::window::Window>; fn save_and_destroy(&mut self); fn paint(&mut self) -> EventResult; - fn on_event(&mut self, event: winit::event::Event<'_, RequestRepaintEvent>) -> EventResult; + fn on_event( + &mut self, + event_loop: &EventLoopWindowTarget, + event: winit::event::Event<'_, RequestRepaintEvent>, + ) -> EventResult; +} + +fn create_event_loop_builder( + native_options: &mut epi::NativeOptions, +) -> EventLoopBuilder { + let mut event_loop_builder = winit::event_loop::EventLoopBuilder::with_user_event(); + + if let Some(hook) = std::mem::take(&mut native_options.event_loop_builder) { + hook(&mut event_loop_builder); + } + + event_loop_builder } /// Access a thread-local event loop. /// /// We reuse the event-loop so we can support closing and opening an eframe window /// multiple times. This is just a limitation of winit. -fn with_event_loop(f: impl FnOnce(&mut EventLoop)) { +fn with_event_loop( + mut native_options: epi::NativeOptions, + f: impl FnOnce(&mut EventLoop, NativeOptions), +) { use std::cell::RefCell; - thread_local!(static EVENT_LOOP: RefCell> = RefCell::new(winit::event_loop::EventLoopBuilder::with_user_event().build())); + thread_local!(static EVENT_LOOP: RefCell>> = RefCell::new(None)); EVENT_LOOP.with(|event_loop| { - f(&mut *event_loop.borrow_mut()); + // Since we want to reference NativeOptions when creating the EventLoop we can't + // do that as part of the lazy thread local storage initialization and so we instead + // create the event loop lazily here + let mut event_loop = event_loop.borrow_mut(); + let event_loop = event_loop + .get_or_insert_with(|| create_event_loop_builder(&mut native_options).build()); + f(event_loop, native_options); }); } @@ -92,7 +80,7 @@ fn run_and_return(event_loop: &mut EventLoop, mut winit_app let mut next_repaint_time = Instant::now(); - event_loop.run_return(|event, _, control_flow| { + event_loop.run_return(|event, event_loop, control_flow| { let event_result = match event { winit::event::Event::LoopDestroyed => EventResult::Exit, @@ -114,14 +102,15 @@ fn run_and_return(event_loop: &mut EventLoop, mut winit_app }) => EventResult::RepaintAsap, winit::event::Event::WindowEvent { window_id, .. } - if window_id != winit_app.window().id() => + if winit_app.window().is_none() + || window_id != winit_app.window().unwrap().id() => { // This can happen if we close a window, and then reopen a new one, // or if we have multiple windows open. EventResult::Wait } - event => winit_app.on_event(event), + event => winit_app.on_event(event_loop, event), }; match event_result { @@ -140,7 +129,9 @@ fn run_and_return(event_loop: &mut EventLoop, mut winit_app *control_flow = match next_repaint_time.checked_duration_since(Instant::now()) { None => { - winit_app.window().request_redraw(); + if let Some(window) = winit_app.window() { + window.request_redraw(); + } ControlFlow::Poll } Some(time_until_next_repaint) => { @@ -169,7 +160,7 @@ fn run_and_exit( let mut next_repaint_time = Instant::now(); - event_loop.run(move |event, _, control_flow| { + event_loop.run(move |event, event_loop, control_flow| { let event_result = match event { winit::event::Event::LoopDestroyed => EventResult::Exit, @@ -190,7 +181,7 @@ fn run_and_exit( .. }) => EventResult::RepaintAsap, - event => winit_app.on_event(event), + event => winit_app.on_event(event_loop, event), }; match event_result { @@ -211,7 +202,9 @@ fn run_and_exit( *control_flow = match next_repaint_time.checked_duration_since(Instant::now()) { None => { - winit_app.window().request_redraw(); + if let Some(window) = winit_app.window() { + window.request_redraw(); + } ControlFlow::Poll } Some(time_until_next_repaint) => { @@ -222,7 +215,6 @@ fn run_and_exit( } // ---------------------------------------------------------------------------- - /// Run an egui app #[cfg(feature = "glow")] mod glow_integration { @@ -230,12 +222,43 @@ mod glow_integration { use super::*; - struct GlowWinitApp { - gl_window: glutin::WindowedContext, + // Note: that the current Glutin API design tightly couples the GL context with + // the Window which means it's not practically possible to just destroy the + // window and re-create a new window while continuing to use the same GL context. + // + // For now this means it's not possible to support Android as well as we can with + // wgpu because we're basically forced to destroy and recreate _everything_ when + // the application suspends and resumes. + // + // There is work in progress to improve the Glutin API so it has a separate Surface + // API that would allow us to just destroy a Window/Surface when suspending, see: + // https://github.com/rust-windowing/glutin/pull/1435 + // + + /// State that is initialized when the application is first starts running via + /// a Resumed event. On Android this ensures that any graphics state is only + /// initialized once the application has an associated `SurfaceView`. + struct GlowWinitRunning { gl: Arc, painter: egui_glow::Painter, integration: epi_integration::EpiIntegration, app: Box, + + // Conceptually this will be split out eventually so that the rest of the state + // can be persistent. + gl_window: glutin::WindowedContext, + } + + struct GlowWinitApp { + repaint_proxy: Arc>>, + app_name: String, + native_options: epi::NativeOptions, + running: Option, + + // Note that since this `AppCreator` is FnOnce we are currently unable to support + // re-initializing the `GlowWinitRunning` state on Android if the application + // suspends and resumes. + app_creator: Option, is_focused: bool, } @@ -243,21 +266,78 @@ mod glow_integration { fn new( event_loop: &EventLoop, app_name: &str, - native_options: &epi::NativeOptions, + native_options: epi::NativeOptions, app_creator: epi::AppCreator, ) -> Self { - let storage = epi_integration::create_storage(app_name); - let window_settings = epi_integration::load_window_settings(storage.as_deref()); + Self { + repaint_proxy: Arc::new(egui::mutex::Mutex::new(event_loop.create_proxy())), + app_name: app_name.to_owned(), + native_options, + running: None, + app_creator: Some(app_creator), + is_focused: true, + } + } + + #[allow(unsafe_code)] + fn create_glutin_windowed_context( + event_loop: &EventLoopWindowTarget, + storage: Option<&dyn epi::Storage>, + title: &String, + native_options: &NativeOptions, + ) -> ( + glutin::WindowedContext, + glow::Context, + ) { + crate::profile_function!(); + + use crate::HardwareAcceleration; + + let hardware_acceleration = match native_options.hardware_acceleration { + HardwareAcceleration::Required => Some(true), + HardwareAcceleration::Preferred => None, + HardwareAcceleration::Off => Some(false), + }; + let window_settings = epi_integration::load_window_settings(storage); + + let window_builder = + epi_integration::window_builder(native_options, &window_settings).with_title(title); + + let gl_window = unsafe { + glutin::ContextBuilder::new() + .with_hardware_acceleration(hardware_acceleration) + .with_depth_buffer(native_options.depth_buffer) + .with_multisampling(native_options.multisampling) + .with_srgb(true) + .with_stencil_buffer(native_options.stencil_buffer) + .with_vsync(native_options.vsync) + .build_windowed(window_builder, event_loop) + .unwrap() + .make_current() + .unwrap() + }; - let window_builder = epi_integration::window_builder(native_options, &window_settings) - .with_title(app_name); - let (gl_window, gl) = create_display(native_options, window_builder, event_loop); + let gl = + unsafe { glow::Context::from_loader_function(|s| gl_window.get_proc_address(s)) }; + + (gl_window, gl) + } + + fn init_run_state(&mut self, event_loop: &EventLoopWindowTarget) { + let storage = epi_integration::create_storage(&self.app_name); + + let (gl_window, gl) = Self::create_glutin_windowed_context( + event_loop, + storage.as_deref(), + &self.app_name, + &self.native_options, + ); let gl = Arc::new(gl); let painter = egui_glow::Painter::new(gl.clone(), None, "") .unwrap_or_else(|error| panic!("some OpenGL error occurred {}\n", error)); - let system_theme = native_options.system_theme(); + let system_theme = self.native_options.system_theme(); let mut integration = epi_integration::EpiIntegration::new( event_loop, painter.max_texture_side(), @@ -268,16 +348,18 @@ mod glow_integration { #[cfg(feature = "wgpu")] None, ); - let theme = system_theme.unwrap_or(native_options.default_theme); + let theme = system_theme.unwrap_or(self.native_options.default_theme); integration.egui_ctx.set_visuals(theme.egui_visuals()); { - let event_loop_proxy = egui::mutex::Mutex::new(event_loop.create_proxy()); + let event_loop_proxy = self.repaint_proxy.clone(); integration.egui_ctx.set_request_repaint_callback(move || { event_loop_proxy.lock().send_event(RequestRepaintEvent).ok(); }); } + let app_creator = std::mem::take(&mut self.app_creator) + .expect("Single-use AppCreator has unexpectedly already been taken"); let mut app = app_creator(&epi::CreationContext { egui_ctx: integration.egui_ctx.clone(), integration_info: integration.frame.info(), @@ -291,14 +373,13 @@ mod glow_integration { integration.warm_up(app.as_mut(), gl_window.window()); } - Self { + self.running = Some(GlowWinitRunning { gl_window, gl, painter, integration, app, - is_focused: true, - } + }); } } @@ -307,139 +388,179 @@ mod glow_integration { self.is_focused } - fn integration(&self) -> &EpiIntegration { - &self.integration + fn integration(&self) -> Option<&EpiIntegration> { + self.running.as_ref().map(|r| &r.integration) } - fn window(&self) -> &winit::window::Window { - self.gl_window.window() + fn window(&self) -> Option<&winit::window::Window> { + self.running.as_ref().map(|r| r.gl_window.window()) } fn save_and_destroy(&mut self) { - self.integration - .save(&mut *self.app, self.gl_window.window()); - self.app.on_exit(Some(&self.gl)); - self.painter.destroy(); + if let Some(running) = &mut self.running { + running + .integration + .save(running.app.as_mut(), running.gl_window.window()); + running.app.on_exit(Some(&running.gl)); + running.painter.destroy(); + } } fn paint(&mut self) -> EventResult { - #[cfg(feature = "puffin")] - puffin::GlobalProfiler::lock().new_frame(); - crate::profile_scope!("frame"); - - let Self { - gl_window, - gl, - app, - integration, - painter, - .. - } = self; - let window = gl_window.window(); - - let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); - - egui_glow::painter::clear( - gl, - screen_size_in_pixels, - app.clear_color(&integration.egui_ctx.style().visuals), - ); - - let egui::FullOutput { - platform_output, - repaint_after, - textures_delta, - shapes, - } = integration.update(app.as_mut(), window); - - integration.handle_platform_output(window, platform_output); + if let Some(running) = &mut self.running { + #[cfg(feature = "puffin")] + puffin::GlobalProfiler::lock().new_frame(); + crate::profile_scope!("frame"); + + let GlowWinitRunning { + gl_window, + gl, + app, + integration, + painter, + } = running; + + let window = gl_window.window(); + + let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); + + egui_glow::painter::clear( + gl, + screen_size_in_pixels, + app.clear_color(&integration.egui_ctx.style().visuals), + ); - let clipped_primitives = { - crate::profile_scope!("tessellate"); - integration.egui_ctx.tessellate(shapes) - }; + let egui::FullOutput { + platform_output, + repaint_after, + textures_delta, + shapes, + } = integration.update(app.as_mut(), window); + + integration.handle_platform_output(window, platform_output); + + let clipped_primitives = { + crate::profile_scope!("tessellate"); + integration.egui_ctx.tessellate(shapes) + }; + + painter.paint_and_update_textures( + screen_size_in_pixels, + integration.egui_ctx.pixels_per_point(), + &clipped_primitives, + &textures_delta, + ); - painter.paint_and_update_textures( - screen_size_in_pixels, - integration.egui_ctx.pixels_per_point(), - &clipped_primitives, - &textures_delta, - ); + integration.post_rendering(app.as_mut(), window); - integration.post_rendering(app.as_mut(), window); + { + crate::profile_scope!("swap_buffers"); + gl_window.swap_buffers().unwrap(); + } - { - crate::profile_scope!("swap_buffers"); - gl_window.swap_buffers().unwrap(); - } + let control_flow = if integration.should_close() { + EventResult::Exit + } else if repaint_after.is_zero() { + EventResult::RepaintAsap + } else if let Some(repaint_after_instant) = + std::time::Instant::now().checked_add(repaint_after) + { + // if repaint_after is something huge and can't be added to Instant, + // we will use `ControlFlow::Wait` instead. + // technically, this might lead to some weird corner cases where the user *WANTS* + // winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own + // egui backend impl i guess. + EventResult::RepaintAt(repaint_after_instant) + } else { + EventResult::Wait + }; + + integration.maybe_autosave(app.as_mut(), window); + + if !self.is_focused { + // On Mac, a minimized Window uses up all CPU: https://github.com/emilk/egui/issues/325 + // We can't know if we are minimized: https://github.com/rust-windowing/winit/issues/208 + // But we know if we are focused (in foreground). When minimized, we are not focused. + // However, a user may want an egui with an animation in the background, + // so we still need to repaint quite fast. + crate::profile_scope!("bg_sleep"); + std::thread::sleep(std::time::Duration::from_millis(10)); + } - let control_flow = if integration.should_close() { - EventResult::Exit - } else if repaint_after.is_zero() { - EventResult::RepaintAsap - } else if let Some(repaint_after_instant) = - std::time::Instant::now().checked_add(repaint_after) - { - // if repaint_after is something huge and can't be added to Instant, - // we will use `ControlFlow::Wait` instead. - // technically, this might lead to some weird corner cases where the user *WANTS* - // winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own - // egui backend impl i guess. - EventResult::RepaintAt(repaint_after_instant) + control_flow } else { EventResult::Wait - }; - - integration.maybe_autosave(app.as_mut(), window); - - if !self.is_focused { - // On Mac, a minimized Window uses up all CPU: https://github.com/emilk/egui/issues/325 - // We can't know if we are minimized: https://github.com/rust-windowing/winit/issues/208 - // But we know if we are focused (in foreground). When minimized, we are not focused. - // However, a user may want an egui with an animation in the background, - // so we still need to repaint quite fast. - crate::profile_scope!("bg_sleep"); - std::thread::sleep(std::time::Duration::from_millis(10)); } - - control_flow } - fn on_event(&mut self, event: winit::event::Event<'_, RequestRepaintEvent>) -> EventResult { + fn on_event( + &mut self, + event_loop: &EventLoopWindowTarget, + event: winit::event::Event<'_, RequestRepaintEvent>, + ) -> EventResult { match event { + winit::event::Event::Resumed => { + if self.running.is_none() { + self.init_run_state(event_loop); + } + EventResult::RepaintAsap + } + winit::event::Event::Suspended => { + #[cfg(target_os = "android")] + { + tracing::error!("Suspended app can't destroy Window surface state with current Egui Glow backend (undefined behaviour)"); + // Instead of destroying everything which we _know_ we can't re-create + // we instead currently just try our luck with not destroying anything. + // + // When the application resumes then it will get a new `SurfaceView` but + // we have no practical way currently of creating a new EGL surface + // via the Glutin API while keeping the GL context and the rest of + // our app state. This will likely result in a black screen or + // frozen screen. + // + //self.running = None; + } + EventResult::Wait + } + winit::event::Event::WindowEvent { event, .. } => { - match &event { - winit::event::WindowEvent::Focused(new_focused) => { - self.is_focused = *new_focused; - } - winit::event::WindowEvent::Resized(physical_size) => { - // Resize with 0 width and height is used by winit to signal a minimize event on Windows. - // See: https://github.com/rust-windowing/winit/issues/208 - // This solves an issue where the app would panic when minimizing on Windows. - if physical_size.width > 0 && physical_size.height > 0 { - self.gl_window.resize(*physical_size); + if let Some(running) = &mut self.running { + match &event { + winit::event::WindowEvent::Focused(new_focused) => { + self.is_focused = *new_focused; } + winit::event::WindowEvent::Resized(physical_size) => { + // Resize with 0 width and height is used by winit to signal a minimize event on Windows. + // See: https://github.com/rust-windowing/winit/issues/208 + // This solves an issue where the app would panic when minimizing on Windows. + if physical_size.width > 0 && physical_size.height > 0 { + running.gl_window.resize(*physical_size); + } + } + winit::event::WindowEvent::ScaleFactorChanged { + new_inner_size, + .. + } => { + running.gl_window.resize(**new_inner_size); + } + winit::event::WindowEvent::CloseRequested + if running.integration.should_close() => + { + return EventResult::Exit + } + _ => {} } - winit::event::WindowEvent::ScaleFactorChanged { - new_inner_size, .. - } => { - self.gl_window.resize(**new_inner_size); - } - winit::event::WindowEvent::CloseRequested - if self.integration.should_close() => - { - return EventResult::Exit - } - _ => {} - } - self.integration.on_event(self.app.as_mut(), &event); + running.integration.on_event(running.app.as_mut(), &event); - if self.integration.should_close() { - EventResult::Exit + if running.integration.should_close() { + EventResult::Exit + } else { + // TODO(emilk): ask egui if the event warrants a repaint + EventResult::RepaintAsap + } } else { - // TODO(emilk): ask egui if the event warrants a repaint - EventResult::RepaintAsap + EventResult::Wait } } _ => EventResult::Wait, @@ -449,17 +570,17 @@ mod glow_integration { pub fn run_glow( app_name: &str, - native_options: &epi::NativeOptions, + mut native_options: epi::NativeOptions, app_creator: epi::AppCreator, ) { if native_options.run_and_return { - with_event_loop(|event_loop| { + with_event_loop(native_options, |event_loop, native_options| { let glow_eframe = GlowWinitApp::new(event_loop, app_name, native_options, app_creator); run_and_return(event_loop, glow_eframe); }); } else { - let event_loop = winit::event_loop::EventLoopBuilder::with_user_event().build(); + let event_loop = create_event_loop_builder(&mut native_options).build(); let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator); run_and_exit(event_loop, glow_eframe); } @@ -468,18 +589,33 @@ mod glow_integration { #[cfg(feature = "glow")] pub use glow_integration::run_glow; - // ---------------------------------------------------------------------------- #[cfg(feature = "wgpu")] mod wgpu_integration { + use std::sync::Arc; + use super::*; - struct WgpuWinitApp { - window: winit::window::Window, + /// State that is initialized when the application is first starts running via + /// a Resumed event. On Android this ensures that any graphics state is only + /// initialized once the application has an associated `SurfaceView`. + struct WgpuWinitRunning { painter: egui_wgpu::winit::Painter<'static>, integration: epi_integration::EpiIntegration, app: Box, + } + + struct WgpuWinitApp { + repaint_proxy: Arc>>, + app_name: String, + native_options: epi::NativeOptions, + app_creator: Option, + running: Option, + + /// Window surface state that's initialized when the app starts running via a Resumed event + /// and on Android will also be destroyed if the application is paused. + window: Option, is_focused: bool, } @@ -487,18 +623,60 @@ mod wgpu_integration { fn new( event_loop: &EventLoop, app_name: &str, - native_options: &epi::NativeOptions, + native_options: epi::NativeOptions, app_creator: epi::AppCreator, ) -> Self { - let storage = epi_integration::create_storage(app_name); - let window_settings = epi_integration::load_window_settings(storage.as_deref()); + Self { + repaint_proxy: Arc::new(std::sync::Mutex::new(event_loop.create_proxy())), + app_name: app_name.to_owned(), + native_options, + running: None, + window: None, + app_creator: Some(app_creator), + is_focused: true, + } + } - let window = epi_integration::window_builder(native_options, &window_settings) - .with_title(app_name) + fn create_window( + event_loop: &EventLoopWindowTarget, + storage: Option<&dyn epi::Storage>, + title: &String, + native_options: &NativeOptions, + ) -> winit::window::Window { + let window_settings = epi_integration::load_window_settings(storage); + epi_integration::window_builder(native_options, &window_settings) + .with_title(title) .build(event_loop) - .unwrap(); + .unwrap() + } + + #[allow(unsafe_code)] + fn set_window(&mut self, window: winit::window::Window) { + self.window = Some(window); + if let Some(running) = &mut self.running { + unsafe { + running.painter.set_window(self.window.as_ref()); + } + } + } - // SAFETY: `window` must outlive `painter`. + #[allow(unsafe_code)] + #[cfg(target_os = "android")] + fn drop_window(&mut self) { + self.window = None; + if let Some(running) = &mut self.running { + unsafe { + running.painter.set_window(None); + } + } + } + + fn init_run_state( + &mut self, + event_loop: &EventLoopWindowTarget, + storage: Option>, + window: winit::window::Window, + ) { #[allow(unsafe_code, unused_mut, unused_unsafe)] let painter = unsafe { let mut painter = egui_wgpu::winit::Painter::new( @@ -510,16 +688,15 @@ mod wgpu_integration { limits: wgpu::Limits::downlevel_webgl2_defaults(), }, wgpu::PresentMode::Fifo, - native_options.multisampling.max(1) as _, + self.native_options.multisampling.max(1) as _, ); - #[cfg(not(target_os = "android"))] painter.set_window(Some(&window)); painter }; let wgpu_render_state = painter.render_state(); - let system_theme = native_options.system_theme(); + let system_theme = self.native_options.system_theme(); let mut integration = epi_integration::EpiIntegration::new( event_loop, painter.max_texture_side().unwrap_or(2048), @@ -530,16 +707,22 @@ mod wgpu_integration { None, wgpu_render_state.clone(), ); - let theme = system_theme.unwrap_or(native_options.default_theme); + let theme = system_theme.unwrap_or(self.native_options.default_theme); integration.egui_ctx.set_visuals(theme.egui_visuals()); { - let event_loop_proxy = egui::mutex::Mutex::new(event_loop.create_proxy()); + let event_loop_proxy = self.repaint_proxy.clone(); integration.egui_ctx.set_request_repaint_callback(move || { - event_loop_proxy.lock().send_event(RequestRepaintEvent).ok(); + event_loop_proxy + .lock() + .unwrap() + .send_event(RequestRepaintEvent) + .ok(); }); } + let app_creator = std::mem::take(&mut self.app_creator) + .expect("Single-use AppCreator has unexpectedly already been taken"); let mut app = app_creator(&epi::CreationContext { egui_ctx: integration.egui_ctx.clone(), integration_info: integration.frame.info(), @@ -553,13 +736,12 @@ mod wgpu_integration { integration.warm_up(app.as_mut(), &window); } - Self { - window, + self.running = Some(WgpuWinitRunning { painter, integration, app, - is_focused: true, - } + }); + self.window = Some(window); } } @@ -568,141 +750,177 @@ mod wgpu_integration { self.is_focused } - fn integration(&self) -> &EpiIntegration { - &self.integration + fn integration(&self) -> Option<&EpiIntegration> { + self.running.as_ref().map(|r| &r.integration) } - fn window(&self) -> &winit::window::Window { - &self.window + fn window(&self) -> Option<&winit::window::Window> { + self.window.as_ref() } fn save_and_destroy(&mut self) { - self.integration.save(&mut *self.app, &self.window); + if let Some(running) = &mut self.running { + if let Some(window) = &self.window { + running.integration.save(running.app.as_mut(), window); + } - #[cfg(feature = "glow")] - self.app.on_exit(None); + #[cfg(feature = "glow")] + running.app.on_exit(None); - #[cfg(not(feature = "glow"))] - self.app.on_exit(); + #[cfg(not(feature = "glow"))] + running.app.on_exit(); - self.painter.destroy(); + running.painter.destroy(); + } } fn paint(&mut self) -> EventResult { - #[cfg(feature = "puffin")] - puffin::GlobalProfiler::lock().new_frame(); - crate::profile_scope!("frame"); - - let Self { - window, - app, - integration, - painter, - .. - } = self; - - let egui::FullOutput { - platform_output, - repaint_after, - textures_delta, - shapes, - } = integration.update(app.as_mut(), window); + if let (Some(running), Some(window)) = (&mut self.running, &self.window) { + #[cfg(feature = "puffin")] + puffin::GlobalProfiler::lock().new_frame(); + crate::profile_scope!("frame"); + + let WgpuWinitRunning { + app, + integration, + painter, + } = running; + + let egui::FullOutput { + platform_output, + repaint_after, + textures_delta, + shapes, + } = integration.update(app.as_mut(), window); + + integration.handle_platform_output(window, platform_output); + + let clipped_primitives = { + crate::profile_scope!("tessellate"); + integration.egui_ctx.tessellate(shapes) + }; + + painter.paint_and_update_textures( + integration.egui_ctx.pixels_per_point(), + app.clear_color(&integration.egui_ctx.style().visuals), + &clipped_primitives, + &textures_delta, + ); - integration.handle_platform_output(window, platform_output); + integration.post_rendering(app.as_mut(), window); - let clipped_primitives = { - crate::profile_scope!("tessellate"); - integration.egui_ctx.tessellate(shapes) - }; - - painter.paint_and_update_textures( - integration.egui_ctx.pixels_per_point(), - app.clear_color(&integration.egui_ctx.style().visuals), - &clipped_primitives, - &textures_delta, - ); - - integration.post_rendering(app.as_mut(), window); + let control_flow = if integration.should_close() { + EventResult::Exit + } else if repaint_after.is_zero() { + EventResult::RepaintAsap + } else if let Some(repaint_after_instant) = + std::time::Instant::now().checked_add(repaint_after) + { + // if repaint_after is something huge and can't be added to Instant, + // we will use `ControlFlow::Wait` instead. + // technically, this might lead to some weird corner cases where the user *WANTS* + // winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own + // egui backend impl i guess. + EventResult::RepaintAt(repaint_after_instant) + } else { + EventResult::Wait + }; + + integration.maybe_autosave(app.as_mut(), window); + + if !self.is_focused { + // On Mac, a minimized Window uses up all CPU: https://github.com/emilk/egui/issues/325 + // We can't know if we are minimized: https://github.com/rust-windowing/winit/issues/208 + // But we know if we are focused (in foreground). When minimized, we are not focused. + // However, a user may want an egui with an animation in the background, + // so we still need to repaint quite fast. + crate::profile_scope!("bg_sleep"); + std::thread::sleep(std::time::Duration::from_millis(10)); + } - let control_flow = if integration.should_close() { - EventResult::Exit - } else if repaint_after.is_zero() { - EventResult::RepaintAsap - } else if let Some(repaint_after_instant) = - std::time::Instant::now().checked_add(repaint_after) - { - // if repaint_after is something huge and can't be added to Instant, - // we will use `ControlFlow::Wait` instead. - // technically, this might lead to some weird corner cases where the user *WANTS* - // winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own - // egui backend impl i guess. - EventResult::RepaintAt(repaint_after_instant) + control_flow } else { EventResult::Wait - }; - - integration.maybe_autosave(app.as_mut(), window); - - if !self.is_focused { - // On Mac, a minimized Window uses up all CPU: https://github.com/emilk/egui/issues/325 - // We can't know if we are minimized: https://github.com/rust-windowing/winit/issues/208 - // But we know if we are focused (in foreground). When minimized, we are not focused. - // However, a user may want an egui with an animation in the background, - // so we still need to repaint quite fast. - crate::profile_scope!("bg_sleep"); - std::thread::sleep(std::time::Duration::from_millis(10)); } - - control_flow } - fn on_event(&mut self, event: winit::event::Event<'_, RequestRepaintEvent>) -> EventResult { + fn on_event( + &mut self, + event_loop: &EventLoopWindowTarget, + event: winit::event::Event<'_, RequestRepaintEvent>, + ) -> EventResult { match event { - #[cfg(target_os = "android")] - winit::event::Event::Resumed => unsafe { - self.painter.set_window(Some(&self.window)); + winit::event::Event::Resumed => { + if let Some(running) = &self.running { + if self.window.is_none() { + let window = Self::create_window( + event_loop, + running.integration.frame.storage(), + &self.app_name, + &self.native_options, + ); + self.set_window(window); + } + } else { + let storage = epi_integration::create_storage(&self.app_name); + let window = Self::create_window( + event_loop, + storage.as_deref(), + &self.app_name, + &self.native_options, + ); + self.init_run_state(event_loop, storage, window); + } EventResult::RepaintAsap - }, - #[cfg(target_os = "android")] - winit::event::Event::Suspended => unsafe { - self.painter.set_window(None); + } + winit::event::Event::Suspended => { + #[cfg(target_os = "android")] + self.drop_window(); EventResult::Wait - }, + } winit::event::Event::WindowEvent { event, .. } => { - match &event { - winit::event::WindowEvent::Focused(new_focused) => { - self.is_focused = *new_focused; - } - winit::event::WindowEvent::Resized(physical_size) => { - // Resize with 0 width and height is used by winit to signal a minimize event on Windows. - // See: https://github.com/rust-windowing/winit/issues/208 - // This solves an issue where the app would panic when minimizing on Windows. - if physical_size.width > 0 && physical_size.height > 0 { - self.painter - .on_window_resized(physical_size.width, physical_size.height); + if let Some(running) = &mut self.running { + match &event { + winit::event::WindowEvent::Focused(new_focused) => { + self.is_focused = *new_focused; } + winit::event::WindowEvent::Resized(physical_size) => { + // Resize with 0 width and height is used by winit to signal a minimize event on Windows. + // See: https://github.com/rust-windowing/winit/issues/208 + // This solves an issue where the app would panic when minimizing on Windows. + if physical_size.width > 0 && physical_size.height > 0 { + running.painter.on_window_resized( + physical_size.width, + physical_size.height, + ); + } + } + winit::event::WindowEvent::ScaleFactorChanged { + new_inner_size, + .. + } => { + running + .painter + .on_window_resized(new_inner_size.width, new_inner_size.height); + } + winit::event::WindowEvent::CloseRequested + if running.integration.should_close() => + { + return EventResult::Exit + } + _ => {} + }; + + running.integration.on_event(running.app.as_mut(), &event); + if running.integration.should_close() { + EventResult::Exit + } else { + // TODO(emilk): ask egui if the event warrants a repaint + EventResult::RepaintAsap } - winit::event::WindowEvent::ScaleFactorChanged { - new_inner_size, .. - } => { - self.painter - .on_window_resized(new_inner_size.width, new_inner_size.height); - } - winit::event::WindowEvent::CloseRequested - if self.integration.should_close() => - { - return EventResult::Exit - } - _ => {} - }; - - self.integration.on_event(self.app.as_mut(), &event); - if self.integration.should_close() { - EventResult::Exit } else { - // TODO(emilk): ask egui if the event warrants a repaint - EventResult::RepaintAsap + EventResult::Wait } } _ => EventResult::Wait, @@ -712,17 +930,17 @@ mod wgpu_integration { pub fn run_wgpu( app_name: &str, - native_options: &epi::NativeOptions, + mut native_options: epi::NativeOptions, app_creator: epi::AppCreator, ) { if native_options.run_and_return { - with_event_loop(|event_loop| { + with_event_loop(native_options, |event_loop, native_options| { let wgpu_eframe = WgpuWinitApp::new(event_loop, app_name, native_options, app_creator); run_and_return(event_loop, wgpu_eframe); }); } else { - let event_loop = winit::event_loop::EventLoopBuilder::with_user_event().build(); + let event_loop = create_event_loop_builder(&mut native_options).build(); let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator); run_and_exit(event_loop, wgpu_eframe); }