From 960b01b67a10a9dc39c6234826732b86403d3e12 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 19 Nov 2023 12:12:43 +0100 Subject: [PATCH] Refactor: move code around in `eframe` (#3575) `run.rs` was getting way too long and complex. --- crates/eframe/src/epi.rs | 2 +- crates/eframe/src/native/epi_integration.rs | 2 + crates/eframe/src/native/glow_integration.rs | 1372 +++++++++ crates/eframe/src/native/mod.rs | 8 + crates/eframe/src/native/run.rs | 2564 +---------------- crates/eframe/src/native/wgpu_integration.rs | 1012 +++++++ crates/eframe/src/native/winit_integration.rs | 102 + 7 files changed, 2533 insertions(+), 2529 deletions(-) create mode 100644 crates/eframe/src/native/glow_integration.rs create mode 100644 crates/eframe/src/native/wgpu_integration.rs create mode 100644 crates/eframe/src/native/winit_integration.rs diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index f728b20c..d5ef2a5a 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -11,7 +11,7 @@ use std::any::Any; #[cfg(not(target_arch = "wasm32"))] #[cfg(any(feature = "glow", feature = "wgpu"))] -pub use crate::native::run::UserEvent; +pub use crate::native::winit_integration::UserEvent; #[cfg(not(target_arch = "wasm32"))] use raw_window_handle::{ diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index bace3f30..b6b52291 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -1,3 +1,5 @@ +//! Common tools used by [`super::glow_integration`] and [`super::wgpu_integration`]. + use std::time::Instant; use winit::event_loop::EventLoopWindowTarget; diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs new file mode 100644 index 00000000..90af6284 --- /dev/null +++ b/crates/eframe/src/native/glow_integration.rs @@ -0,0 +1,1372 @@ +use std::{cell::RefCell, rc::Rc, sync::Arc, time::Instant}; + +use glutin::{ + display::GetGlDisplay, + prelude::{GlDisplay, NotCurrentGlContextSurfaceAccessor, PossiblyCurrentGlContext}, + surface::GlSurface, +}; +use raw_window_handle::{HasRawDisplayHandle as _, HasRawWindowHandle as _}; +use winit::{ + event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}, + window::{Window, WindowId}, +}; + +use egui::{ + epaint::ahash::HashMap, DeferredViewportUiCallback, ImmediateViewport, NumExt as _, + ViewportBuilder, ViewportClass, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, + ViewportInfo, ViewportOutput, +}; +#[cfg(feature = "accesskit")] +use egui_winit::accesskit_winit; +use egui_winit::{ + apply_viewport_builder_to_new_window, create_winit_window_builder, process_viewport_commands, + EventResponse, +}; + +use crate::{ + native::epi_integration::EpiIntegration, App, AppCreator, CreationContext, NativeOptions, + Result, Storage, +}; + +use super::{ + winit_integration::{EventResult, UserEvent, WinitApp}, + *, +}; + +// 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 + +// ---------------------------------------------------------------------------- +// Types: + +pub struct GlowWinitApp { + repaint_proxy: Arc>>, + app_name: String, + native_options: 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, +} + +/// 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 { + integration: EpiIntegration, + app: Box, + + // These needs to be shared with the immediate viewport renderer, hence the Rc/Arc/RefCells: + glutin: Rc>, + painter: Rc>, +} + +/// This struct will contain both persistent and temporary glutin state. +/// +/// Platform Quirks: +/// * Microsoft Windows: requires that we create a window before opengl context. +/// * Android: window and surface should be destroyed when we receive a suspend event. recreate on resume event. +/// +/// winit guarantees that we will get a Resumed event on startup on all platforms. +/// * Before Resumed event: `gl_config`, `gl_context` can be created at any time. on windows, a window must be created to get `gl_context`. +/// * Resumed: `gl_surface` will be created here. `window` will be re-created here for android. +/// * Suspended: on android, we drop window + surface. on other platforms, we don't get Suspended event. +/// +/// The setup is divided between the `new` fn and `on_resume` fn. we can just assume that `on_resume` is a continuation of +/// `new` fn on all platforms. only on android, do we get multiple resumed events because app can be suspended. +struct GlutinWindowContext { + swap_interval: glutin::surface::SwapInterval, + gl_config: glutin::config::Config, + + max_texture_side: Option, + + current_gl_context: Option, + not_current_gl_context: Option, + + viewports: ViewportIdMap, + viewport_from_window: HashMap, + window_from_viewport: ViewportIdMap, + + focused_viewport: Option, +} + +struct Viewport { + ids: ViewportIdPair, + class: ViewportClass, + builder: ViewportBuilder, + info: ViewportInfo, + screenshot_requested: bool, + + /// The user-callback that shows the ui. + /// None for immediate viewports. + viewport_ui_cb: Option>, + + gl_surface: Option>, + window: Option>, + egui_winit: Option, +} + +// ---------------------------------------------------------------------------- + +impl GlowWinitApp { + pub fn new( + event_loop: &EventLoop, + app_name: &str, + native_options: NativeOptions, + app_creator: AppCreator, + ) -> Self { + crate::profile_function!(); + 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), + } + } + + #[allow(unsafe_code)] + fn create_glutin_windowed_context( + event_loop: &EventLoopWindowTarget, + storage: Option<&dyn Storage>, + native_options: &mut NativeOptions, + ) -> Result<(GlutinWindowContext, egui_glow::Painter)> { + crate::profile_function!(); + + let window_settings = epi_integration::load_window_settings(storage); + + let winit_window_builder = + epi_integration::viewport_builder(event_loop, native_options, window_settings); + + let mut glutin_window_context = + unsafe { GlutinWindowContext::new(winit_window_builder, native_options, event_loop)? }; + + // Creates the window - must come before we create our glow context + glutin_window_context.on_resume(event_loop)?; + + if let Some(viewport) = glutin_window_context.viewports.get(&ViewportId::ROOT) { + if let Some(window) = &viewport.window { + epi_integration::apply_window_settings(window, window_settings); + } + } + + let gl = unsafe { + crate::profile_scope!("glow::Context::from_loader_function"); + Arc::new(glow::Context::from_loader_function(|s| { + let s = std::ffi::CString::new(s) + .expect("failed to construct C string from string for gl proc address"); + + glutin_window_context.get_proc_address(&s) + })) + }; + + let painter = egui_glow::Painter::new(gl, "", native_options.shader_version)?; + + Ok((glutin_window_context, painter)) + } + + fn init_run_state( + &mut self, + event_loop: &EventLoopWindowTarget, + ) -> Result<&mut GlowWinitRunning> { + crate::profile_function!(); + + let storage = epi_integration::create_storage( + self.native_options + .viewport + .app_id + .as_ref() + .unwrap_or(&self.app_name), + ); + + let (mut glutin, painter) = Self::create_glutin_windowed_context( + event_loop, + storage.as_deref(), + &mut self.native_options, + )?; + let gl = painter.gl().clone(); + + let max_texture_side = painter.max_texture_side(); + glutin.max_texture_side = Some(max_texture_side); + for viewport in glutin.viewports.values_mut() { + if let Some(egui_winit) = viewport.egui_winit.as_mut() { + egui_winit.set_max_texture_side(max_texture_side); + } + } + + let system_theme = + winit_integration::system_theme(&glutin.window(ViewportId::ROOT), &self.native_options); + + let mut integration = EpiIntegration::new( + &glutin.window(ViewportId::ROOT), + system_theme, + &self.app_name, + &self.native_options, + storage, + winit_integration::IS_DESKTOP, + Some(gl.clone()), + #[cfg(feature = "wgpu")] + None, + ); + + { + let event_loop_proxy = self.repaint_proxy.clone(); + integration + .egui_ctx + .set_request_repaint_callback(move |info| { + log::trace!("request_repaint_callback: {info:?}"); + let when = Instant::now() + info.delay; + let frame_nr = info.current_frame_nr; + event_loop_proxy + .lock() + .send_event(UserEvent::RequestRepaint { + viewport_id: info.viewport_id, + when, + frame_nr, + }) + .ok(); + }); + } + + #[cfg(feature = "accesskit")] + { + let event_loop_proxy = self.repaint_proxy.lock().clone(); + let viewport = glutin.viewports.get_mut(&ViewportId::ROOT).unwrap(); + if let Viewport { + window: Some(window), + egui_winit: Some(egui_winit), + .. + } = viewport + { + integration.init_accesskit(egui_winit, window, event_loop_proxy); + } + } + + let theme = system_theme.unwrap_or(self.native_options.default_theme); + integration.egui_ctx.set_visuals(theme.egui_visuals()); + + if self + .native_options + .viewport + .mouse_passthrough + .unwrap_or(false) + { + if let Err(err) = glutin.window(ViewportId::ROOT).set_cursor_hittest(false) { + log::warn!("set_cursor_hittest(false) failed: {err}"); + } + } + + let app_creator = std::mem::take(&mut self.app_creator) + .expect("Single-use AppCreator has unexpectedly already been taken"); + + let app = { + let window = glutin.window(ViewportId::ROOT); + let cc = CreationContext { + egui_ctx: integration.egui_ctx.clone(), + integration_info: integration.frame.info().clone(), + storage: integration.frame.storage(), + gl: Some(gl), + #[cfg(feature = "wgpu")] + wgpu_render_state: None, + raw_display_handle: window.raw_display_handle(), + raw_window_handle: window.raw_window_handle(), + }; + crate::profile_scope!("app_creator"); + app_creator(&cc) + }; + + let glutin = Rc::new(RefCell::new(glutin)); + let painter = Rc::new(RefCell::new(painter)); + + { + // Create weak pointers so that we don't keep + // state alive for too long. + let glutin = Rc::downgrade(&glutin); + let painter = Rc::downgrade(&painter); + let beginning = integration.beginning; + + let event_loop: *const EventLoopWindowTarget = event_loop; + + egui::Context::set_immediate_viewport_renderer(move |egui_ctx, immediate_viewport| { + if let (Some(glutin), Some(painter)) = (glutin.upgrade(), painter.upgrade()) { + // SAFETY: the event loop lives longer than + // the Rc:s we just upgraded above. + #[allow(unsafe_code)] + let event_loop = unsafe { event_loop.as_ref().unwrap() }; + + render_immediate_viewport( + event_loop, + egui_ctx, + &glutin, + &painter, + beginning, + immediate_viewport, + ); + } else { + log::warn!("render_sync_callback called after window closed"); + } + }); + } + + Ok(self.running.insert(GlowWinitRunning { + glutin, + painter, + integration, + app, + })) + } +} + +impl WinitApp for GlowWinitApp { + fn frame_nr(&self, viewport_id: ViewportId) -> u64 { + self.running + .as_ref() + .map_or(0, |r| r.integration.egui_ctx.frame_nr_for(viewport_id)) + } + + fn is_focused(&self, window_id: WindowId) -> bool { + if let Some(running) = &self.running { + let glutin = running.glutin.borrow(); + if let Some(window_id) = glutin.viewport_from_window.get(&window_id) { + return glutin.focused_viewport == Some(*window_id); + } + } + + false + } + + fn integration(&self) -> Option<&EpiIntegration> { + self.running.as_ref().map(|r| &r.integration) + } + + fn window(&self, window_id: WindowId) -> Option> { + let running = self.running.as_ref()?; + let glutin = running.glutin.borrow(); + let viewport_id = *glutin.viewport_from_window.get(&window_id)?; + if let Some(viewport) = glutin.viewports.get(&viewport_id) { + viewport.window.clone() + } else { + None + } + } + + fn window_id_from_viewport_id(&self, id: ViewportId) -> Option { + self.running + .as_ref() + .and_then(|r| r.glutin.borrow().window_from_viewport.get(&id).copied()) + } + + fn save_and_destroy(&mut self) { + if let Some(mut running) = self.running.take() { + crate::profile_function!(); + + running.integration.save( + running.app.as_mut(), + Some(&running.glutin.borrow().window(ViewportId::ROOT)), + ); + running.app.on_exit(Some(running.painter.borrow().gl())); + running.painter.borrow_mut().destroy(); + } + } + + fn run_ui_and_paint(&mut self, window_id: WindowId) -> EventResult { + if let Some(running) = &mut self.running { + running.run_ui_and_paint(window_id) + } else { + EventResult::Wait + } + } + + fn on_event( + &mut self, + event_loop: &EventLoopWindowTarget, + event: &winit::event::Event<'_, UserEvent>, + ) -> Result { + crate::profile_function!(); + + Ok(match event { + winit::event::Event::Resumed => { + let running = if let Some(running) = &mut self.running { + // not the first resume event. create whatever you need. + running.glutin.borrow_mut().on_resume(event_loop)?; + running + } else { + // first resume event. + // we can actually move this outside of event loop. + // and just run the on_resume fn of gl_window + self.init_run_state(event_loop)? + }; + let window_id = running + .glutin + .borrow() + .window_from_viewport + .get(&ViewportId::ROOT) + .copied(); + EventResult::RepaintNow(window_id.unwrap()) + } + + winit::event::Event::Suspended => { + if let Some(running) = &mut self.running { + running.glutin.borrow_mut().on_suspend()?; + } + EventResult::Wait + } + + winit::event::Event::MainEventsCleared => { + if let Some(running) = &self.running { + if let Err(err) = running.glutin.borrow_mut().on_resume(event_loop) { + log::warn!("on_resume failed {err}"); + } + } + EventResult::Wait + } + + winit::event::Event::WindowEvent { event, window_id } => { + if let Some(running) = &mut self.running { + running.on_window_event(*window_id, event) + } else { + EventResult::Wait + } + } + + #[cfg(feature = "accesskit")] + winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest( + accesskit_winit::ActionRequestEvent { request, window_id }, + )) => { + if let Some(running) = &self.running { + let mut glutin = running.glutin.borrow_mut(); + if let Some(viewport_id) = glutin.viewport_from_window.get(window_id).copied() { + if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) { + if let Some(egui_winit) = &mut viewport.egui_winit { + crate::profile_scope!("on_accesskit_action_request"); + egui_winit.on_accesskit_action_request(request.clone()); + } + } + } + // As a form of user input, accessibility actions should + // lead to a repaint. + EventResult::RepaintNext(*window_id) + } else { + EventResult::Wait + } + } + _ => EventResult::Wait, + }) + } +} + +impl GlowWinitRunning { + fn run_ui_and_paint(&mut self, window_id: WindowId) -> EventResult { + let Some(viewport_id) = self + .glutin + .borrow() + .viewport_from_window + .get(&window_id) + .copied() + else { + return EventResult::Wait; + }; + + #[cfg(feature = "puffin")] + puffin::GlobalProfiler::lock().new_frame(); + crate::profile_scope!("frame"); + + { + let glutin = self.glutin.borrow(); + let viewport = &glutin.viewports[&viewport_id]; + let is_immediate = viewport.viewport_ui_cb.is_none(); + if is_immediate && viewport_id != ViewportId::ROOT { + // This will only happen if this is an immediate viewport. + // That means that the viewport cannot be rendered by itself and needs his parent to be rendered. + if let Some(parent_viewport) = glutin.viewports.get(&viewport.ids.parent) { + if let Some(window) = parent_viewport.window.as_ref() { + return EventResult::RepaintNext(window.id()); + } + } + return EventResult::Wait; + } + } + + let (raw_input, viewport_ui_cb) = { + let mut glutin = self.glutin.borrow_mut(); + let viewport = glutin.viewports.get_mut(&viewport_id).unwrap(); + viewport.update_viewport_info(); + let window = viewport.window.as_ref().unwrap(); + + let egui_winit = viewport.egui_winit.as_mut().unwrap(); + let mut raw_input = egui_winit.take_egui_input(window, viewport.ids); + let viewport_ui_cb = viewport.viewport_ui_cb.clone(); + + self.integration.pre_update(); + + raw_input.time = Some(self.integration.beginning.elapsed().as_secs_f64()); + raw_input.viewports = glutin + .viewports + .iter() + .map(|(id, viewport)| (*id, viewport.info.clone())) + .collect(); + + (raw_input, viewport_ui_cb) + }; + + // ------------------------------------------------------------ + // The update function, which could call immediate viewports, + // so make sure we don't hold any locks here required by the immediate viewports rendeer. + + let full_output = + self.integration + .update(self.app.as_mut(), viewport_ui_cb.as_deref(), raw_input); + + // ------------------------------------------------------------ + + let Self { + integration, + app, + glutin, + painter, + .. + } = self; + + let mut glutin = glutin.borrow_mut(); + let mut painter = painter.borrow_mut(); + + let egui::FullOutput { + platform_output, + textures_delta, + shapes, + pixels_per_point, + viewport_output, + } = full_output; + + let GlutinWindowContext { + viewports, + current_gl_context, + .. + } = &mut *glutin; + + let viewport = viewports.get_mut(&viewport_id).unwrap(); + let window = viewport.window.as_ref().unwrap(); + let gl_surface = viewport.gl_surface.as_ref().unwrap(); + let egui_winit = viewport.egui_winit.as_mut().unwrap(); + + integration.post_update(); + integration.handle_platform_output(window, viewport_id, platform_output, egui_winit); + + let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point); + + *current_gl_context = Some( + current_gl_context + .take() + .unwrap() + .make_not_current() + .unwrap() + .make_current(gl_surface) + .unwrap(), + ); + + let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); + + painter.clear( + screen_size_in_pixels, + app.clear_color(&integration.egui_ctx.style().visuals), + ); + + painter.paint_and_update_textures( + screen_size_in_pixels, + pixels_per_point, + &clipped_primitives, + &textures_delta, + ); + + { + let screenshot_requested = std::mem::take(&mut viewport.screenshot_requested); + if screenshot_requested { + let screenshot = painter.read_screen_rgba(screen_size_in_pixels); + egui_winit + .egui_input_mut() + .events + .push(egui::Event::Screenshot { + viewport_id, + image: screenshot.into(), + }); + } + integration.post_rendering(window); + } + + { + crate::profile_scope!("swap_buffers"); + if let Err(err) = gl_surface.swap_buffers( + current_gl_context + .as_ref() + .expect("failed to get current context to swap buffers"), + ) { + log::error!("swap_buffers failed: {err}"); + } + } + + // give it time to settle: + #[cfg(feature = "__screenshot")] + if integration.egui_ctx.frame_nr() == 2 { + if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") { + save_screeshot_and_exit(&path, &painter, screen_size_in_pixels); + } + } + + integration.maybe_autosave(app.as_mut(), Some(window)); + + if window.is_minimized() == Some(true) { + // On Mac, a minimized Window uses up all CPU: + // https://github.com/emilk/egui/issues/325 + crate::profile_scope!("minimized_sleep"); + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + glutin.handle_viewport_output(viewport_output); + + if integration.should_close() { + EventResult::Exit + } else { + EventResult::Wait + } + } + + fn on_window_event( + &mut self, + window_id: WindowId, + event: &winit::event::WindowEvent<'_>, + ) -> EventResult { + let viewport_id = self + .glutin + .borrow() + .viewport_from_window + .get(&window_id) + .copied(); + + // On Windows, if a window is resized by the user, it should repaint synchronously, inside the + // event handler. + // + // If this is not done, the compositor will assume that the window does not want to redraw, + // and continue ahead. + // + // In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver + // new frames to the compositor in time. + // + // The flickering is technically glutin or glow's fault, but we should be responding properly + // to resizes anyway, as doing so avoids dropping frames. + // + // See: https://github.com/emilk/egui/issues/903 + let mut repaint_asap = false; + + match event { + winit::event::WindowEvent::Focused(new_focused) => { + self.glutin.borrow_mut().focused_viewport = + new_focused.then(|| viewport_id).flatten(); + } + + 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 0 < physical_size.width && 0 < physical_size.height { + if let Some(viewport_id) = viewport_id { + repaint_asap = true; + self.glutin.borrow_mut().resize(viewport_id, *physical_size); + } + } + } + + winit::event::WindowEvent::ScaleFactorChanged { new_inner_size, .. } => { + if let Some(viewport_id) = viewport_id { + repaint_asap = true; + self.glutin + .borrow_mut() + .resize(viewport_id, **new_inner_size); + } + } + + winit::event::WindowEvent::CloseRequested => { + let is_root = viewport_id == Some(ViewportId::ROOT); + if is_root && self.integration.should_close() { + log::debug!("Received WindowEvent::CloseRequested"); + return EventResult::Exit; + } + } + + _ => {} + } + + let event_response = 'res: { + if let Some(viewport_id) = viewport_id { + let mut glutin = self.glutin.borrow_mut(); + if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) { + break 'res self.integration.on_event( + self.app.as_mut(), + event, + viewport.egui_winit.as_mut().unwrap(), + viewport.ids.this, + ); + } + } + + EventResponse { + consumed: false, + repaint: false, + } + }; + + if self.integration.should_close() { + EventResult::Exit + } else if event_response.repaint { + if repaint_asap { + EventResult::RepaintNow(window_id) + } else { + EventResult::RepaintNext(window_id) + } + } else { + EventResult::Wait + } + } +} + +impl GlutinWindowContext { + #[allow(unsafe_code)] + unsafe fn new( + viewport_builder: ViewportBuilder, + native_options: &NativeOptions, + event_loop: &EventLoopWindowTarget, + ) -> Result { + crate::profile_function!(); + + // There is a lot of complexity with opengl creation, + // so prefer extensive logging to get all the help we can to debug issues. + + use glutin::prelude::*; + // convert native options to glutin options + let hardware_acceleration = match native_options.hardware_acceleration { + crate::HardwareAcceleration::Required => Some(true), + crate::HardwareAcceleration::Preferred => None, + crate::HardwareAcceleration::Off => Some(false), + }; + let swap_interval = if native_options.vsync { + glutin::surface::SwapInterval::Wait(std::num::NonZeroU32::new(1).unwrap()) + } else { + glutin::surface::SwapInterval::DontWait + }; + /* opengl setup flow goes like this: + 1. we create a configuration for opengl "Display" / "Config" creation + 2. choose between special extensions like glx or egl or wgl and use them to create config/display + 3. opengl context configuration + 4. opengl context creation + */ + // start building config for gl display + let config_template_builder = glutin::config::ConfigTemplateBuilder::new() + .prefer_hardware_accelerated(hardware_acceleration) + .with_depth_size(native_options.depth_buffer) + .with_stencil_size(native_options.stencil_buffer) + .with_transparency(native_options.viewport.transparent.unwrap_or(false)); + // we don't know if multi sampling option is set. so, check if its more than 0. + let config_template_builder = if native_options.multisampling > 0 { + config_template_builder.with_multisampling( + native_options + .multisampling + .try_into() + .expect("failed to fit multisamples option of native_options into u8"), + ) + } else { + config_template_builder + }; + + log::debug!("trying to create glutin Display with config: {config_template_builder:?}"); + + // Create GL display. This may probably create a window too on most platforms. Definitely on `MS windows`. Never on Android. + let display_builder = glutin_winit::DisplayBuilder::new() + // we might want to expose this option to users in the future. maybe using an env var or using native_options. + .with_preference(glutin_winit::ApiPrefence::FallbackEgl) // https://github.com/emilk/egui/issues/2520#issuecomment-1367841150 + .with_window_builder(Some(create_winit_window_builder(viewport_builder.clone()))); + + let (window, gl_config) = { + crate::profile_scope!("DisplayBuilder::build"); + + display_builder + .build( + event_loop, + config_template_builder.clone(), + |mut config_iterator| { + let config = config_iterator.next().expect( + "failed to find a matching configuration for creating glutin config", + ); + log::debug!( + "using the first config from config picker closure. config: {config:?}" + ); + config + }, + ) + .map_err(|e| crate::Error::NoGlutinConfigs(config_template_builder.build(), e))? + }; + if let Some(window) = &window { + apply_viewport_builder_to_new_window(window, &viewport_builder); + } + + let gl_display = gl_config.display(); + log::debug!( + "successfully created GL Display with version: {} and supported features: {:?}", + gl_display.version_string(), + gl_display.supported_features() + ); + let raw_window_handle = window.as_ref().map(|w| w.raw_window_handle()); + log::debug!("creating gl context using raw window handle: {raw_window_handle:?}"); + + // create gl context. if core context cannot be created, try gl es context as fallback. + let context_attributes = + glutin::context::ContextAttributesBuilder::new().build(raw_window_handle); + let fallback_context_attributes = glutin::context::ContextAttributesBuilder::new() + .with_context_api(glutin::context::ContextApi::Gles(None)) + .build(raw_window_handle); + + let gl_context_result = { + crate::profile_scope!("create_context"); + gl_config + .display() + .create_context(&gl_config, &context_attributes) + }; + + let gl_context = match gl_context_result { + Ok(it) => it, + Err(err) => { + log::warn!("Failed to create context using default context attributes {context_attributes:?} due to error: {err}"); + log::debug!( + "Retrying with fallback context attributes: {fallback_context_attributes:?}" + ); + gl_config + .display() + .create_context(&gl_config, &fallback_context_attributes)? + } + }; + let not_current_gl_context = Some(gl_context); + + let mut viewport_from_window = HashMap::default(); + let mut window_from_viewport = ViewportIdMap::default(); + let mut info = ViewportInfo::default(); + if let Some(window) = &window { + viewport_from_window.insert(window.id(), ViewportId::ROOT); + window_from_viewport.insert(ViewportId::ROOT, window.id()); + info.minimized = window.is_minimized(); + info.maximized = Some(window.is_maximized()); + } + + let mut viewports = ViewportIdMap::default(); + viewports.insert( + ViewportId::ROOT, + Viewport { + ids: ViewportIdPair::ROOT, + class: ViewportClass::Root, + builder: viewport_builder, + info, + screenshot_requested: false, + viewport_ui_cb: None, + gl_surface: None, + window: window.map(Rc::new), + egui_winit: None, + }, + ); + + // the fun part with opengl gl is that we never know whether there is an error. the context creation might have failed, but + // it could keep working until we try to make surface current or swap buffers or something else. future glutin improvements might + // help us start from scratch again if we fail context creation and go back to preferEgl or try with different config etc.. + // https://github.com/emilk/egui/pull/2541#issuecomment-1370767582 + + let mut slf = GlutinWindowContext { + swap_interval, + gl_config, + current_gl_context: None, + not_current_gl_context, + viewports, + viewport_from_window, + max_texture_side: None, + window_from_viewport, + focused_viewport: Some(ViewportId::ROOT), + }; + + slf.on_resume(event_loop)?; + + Ok(slf) + } + + /// This will be run after `new`. on android, it might be called multiple times over the course of the app's lifetime. + /// roughly, + /// 1. check if window already exists. otherwise, create one now. + /// 2. create attributes for surface creation. + /// 3. create surface. + /// 4. make surface and context current. + /// + /// we presently assume that we will + fn on_resume(&mut self, event_loop: &EventLoopWindowTarget) -> Result<()> { + crate::profile_function!(); + + let viewports: Vec = self + .viewports + .iter() + .filter(|(_, viewport)| viewport.gl_surface.is_none()) + .map(|(id, _)| *id) + .collect(); + + for viewport_id in viewports { + self.init_viewport(viewport_id, event_loop)?; + } + Ok(()) + } + + #[allow(unsafe_code)] + pub(crate) fn init_viewport( + &mut self, + viewport_id: ViewportId, + event_loop: &EventLoopWindowTarget, + ) -> Result<()> { + crate::profile_function!(); + + let viewport = self + .viewports + .get_mut(&viewport_id) + .expect("viewport doesn't exist"); + + let window = if let Some(window) = &mut viewport.window { + window + } else { + log::trace!("Window doesn't exist yet. Creating one now with finalize_window"); + let window = glutin_winit::finalize_window( + event_loop, + create_winit_window_builder(viewport.builder.clone()), + &self.gl_config, + )?; + apply_viewport_builder_to_new_window(&window, &viewport.builder); + viewport.info.minimized = window.is_minimized(); + viewport.info.maximized = Some(window.is_maximized()); + viewport.window.insert(Rc::new(window)) + }; + + { + // surface attributes + let (width_px, height_px): (u32, u32) = window.inner_size().into(); + let width_px = std::num::NonZeroU32::new(width_px.at_least(1)).unwrap(); + let height_px = std::num::NonZeroU32::new(height_px.at_least(1)).unwrap(); + let surface_attributes = + glutin::surface::SurfaceAttributesBuilder::::new() + .build(window.raw_window_handle(), width_px, height_px); + + log::trace!("creating surface with attributes: {surface_attributes:?}"); + let gl_surface = unsafe { + self.gl_config + .display() + .create_window_surface(&self.gl_config, &surface_attributes)? + }; + + log::trace!("surface created successfully: {gl_surface:?}. making context current"); + + let not_current_gl_context = + if let Some(not_current_context) = self.not_current_gl_context.take() { + not_current_context + } else { + self.current_gl_context + .take() + .unwrap() + .make_not_current() + .unwrap() + }; + let current_gl_context = not_current_gl_context.make_current(&gl_surface)?; + + // try setting swap interval. but its not absolutely necessary, so don't panic on failure. + log::trace!("made context current. setting swap interval for surface"); + if let Err(err) = gl_surface.set_swap_interval(¤t_gl_context, self.swap_interval) + { + log::warn!("Failed to set swap interval due to error: {err}"); + } + + // we will reach this point only once in most platforms except android. + // create window/surface/make context current once and just use them forever. + + viewport.egui_winit.get_or_insert_with(|| { + egui_winit::State::new( + event_loop, + Some(window.scale_factor() as f32), + self.max_texture_side, + ) + }); + + viewport.gl_surface = Some(gl_surface); + self.current_gl_context = Some(current_gl_context); + self.viewport_from_window + .insert(window.id(), viewport.ids.this); + self.window_from_viewport + .insert(viewport.ids.this, window.id()); + } + + Ok(()) + } + + /// only applies for android. but we basically drop surface + window and make context not current + fn on_suspend(&mut self) -> Result<()> { + log::debug!("received suspend event. dropping window and surface"); + for viewport in self.viewports.values_mut() { + viewport.gl_surface = None; + viewport.window = None; + } + if let Some(current) = self.current_gl_context.take() { + log::debug!("context is current, so making it non-current"); + self.not_current_gl_context = Some(current.make_not_current()?); + } else { + log::debug!("context is already not current??? could be duplicate suspend event"); + } + Ok(()) + } + + fn viewport(&self, viewport_id: ViewportId) -> &Viewport { + self.viewports + .get(&viewport_id) + .expect("viewport doesn't exist") + } + + fn window(&self, viewport_id: ViewportId) -> Rc { + self.viewport(viewport_id) + .window + .clone() + .expect("winit window doesn't exist") + } + + fn resize(&mut self, viewport_id: ViewportId, physical_size: winit::dpi::PhysicalSize) { + let width_px = std::num::NonZeroU32::new(physical_size.width.at_least(1)).unwrap(); + let height_px = std::num::NonZeroU32::new(physical_size.height.at_least(1)).unwrap(); + + if let Some(viewport) = self.viewports.get(&viewport_id) { + if let Some(gl_surface) = &viewport.gl_surface { + self.current_gl_context = Some( + self.current_gl_context + .take() + .unwrap() + .make_not_current() + .unwrap() + .make_current(gl_surface) + .unwrap(), + ); + gl_surface.resize( + self.current_gl_context + .as_ref() + .expect("failed to get current context to resize surface"), + width_px, + height_px, + ); + } + } + } + + fn get_proc_address(&self, addr: &std::ffi::CStr) -> *const std::ffi::c_void { + self.gl_config.display().get_proc_address(addr) + } + + fn handle_viewport_output(&mut self, viewport_output: ViewportIdMap) { + crate::profile_function!(); + + let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect(); + + for ( + viewport_id, + ViewportOutput { + parent, + class, + builder, + viewport_ui_cb, + commands, + repaint_delay: _, // ignored - we listened to the repaint callback instead + }, + ) in viewport_output + { + let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent); + + let viewport = initialize_or_update_viewport( + &mut self.viewports, + ids, + class, + builder, + viewport_ui_cb, + self.focused_viewport, + ); + + if let Some(window) = &viewport.window { + let is_viewport_focused = self.focused_viewport == Some(viewport_id); + egui_winit::process_viewport_commands( + &mut viewport.info, + commands, + window, + is_viewport_focused, + &mut viewport.screenshot_requested, + ); + } + } + + // GC old viewports + self.viewports + .retain(|id, _| active_viewports_ids.contains(id)); + self.viewport_from_window + .retain(|_, id| active_viewports_ids.contains(id)); + self.window_from_viewport + .retain(|id, _| active_viewports_ids.contains(id)); + } +} + +impl Viewport { + /// Update the stored `ViewportInfo`. + fn update_viewport_info(&mut self) { + let Some(window) = &self.window else { + return; + }; + let Some(egui_winit) = &self.egui_winit else { + return; + }; + egui_winit.update_viewport_info(&mut self.info, window); + } +} + +fn initialize_or_update_viewport( + viewports: &mut ViewportIdMap, + ids: ViewportIdPair, + class: ViewportClass, + mut builder: ViewportBuilder, + viewport_ui_cb: Option>, + focused_viewport: Option, +) -> &mut Viewport { + crate::profile_function!(); + + if builder.icon.is_none() { + // Inherit icon from parent + builder.icon = viewports + .get_mut(&ids.parent) + .and_then(|vp| vp.builder.icon.clone()); + } + + match viewports.entry(ids.this) { + std::collections::hash_map::Entry::Vacant(entry) => { + // New viewport: + log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title); + entry.insert(Viewport { + ids, + class, + builder, + info: Default::default(), + screenshot_requested: false, + viewport_ui_cb, + window: None, + egui_winit: None, + gl_surface: None, + }) + } + + std::collections::hash_map::Entry::Occupied(mut entry) => { + // Patch an existing viewport: + let viewport = entry.get_mut(); + + viewport.ids.parent = ids.parent; + viewport.class = class; + viewport.viewport_ui_cb = viewport_ui_cb; + + let (delta_commands, recreate) = viewport.builder.patch(&builder); + + if recreate { + log::debug!( + "Recreating window for viewport {:?} ({:?})", + ids.this, + builder.title + ); + viewport.window = None; + viewport.egui_winit = None; + } else if let Some(window) = &viewport.window { + let is_viewport_focused = focused_viewport == Some(ids.this); + process_viewport_commands( + &mut viewport.info, + delta_commands, + window, + is_viewport_focused, + &mut viewport.screenshot_requested, + ); + } + + entry.into_mut() + } + } +} + +/// This is called (via a callback) by user code to render immediate viewports, +/// i.e. viewport that are directly nested inside a parent viewport. +fn render_immediate_viewport( + event_loop: &EventLoopWindowTarget, + egui_ctx: &egui::Context, + glutin: &RefCell, + painter: &RefCell, + beginning: Instant, + immediate_viewport: ImmediateViewport<'_>, +) { + crate::profile_function!(); + + let ImmediateViewport { + ids, + builder, + viewport_ui_cb, + } = immediate_viewport; + + { + let mut glutin = glutin.borrow_mut(); + + let viewport = initialize_or_update_viewport( + &mut glutin.viewports, + ids, + ViewportClass::Immediate, + builder, + None, + None, + ); + + if viewport.gl_surface.is_none() { + glutin + .init_viewport(ids.this, event_loop) + .expect("Failed to initialize window in egui::Context::show_viewport_immediate"); + } + } + + let input = { + let mut glutin = glutin.borrow_mut(); + + let Some(viewport) = glutin.viewports.get_mut(&ids.this) else { + return; + }; + viewport.update_viewport_info(); + let Some(winit_state) = &mut viewport.egui_winit else { + return; + }; + let Some(window) = &viewport.window else { + return; + }; + + let mut raw_input = winit_state.take_egui_input(window, ids); + raw_input.viewports = glutin + .viewports + .iter() + .map(|(id, viewport)| (*id, viewport.info.clone())) + .collect(); + raw_input.time = Some(beginning.elapsed().as_secs_f64()); + raw_input + }; + + // --------------------------------------------------- + // Call the user ui-code, which could re-entrantly call this function again! + // No locks may be hold while calling this function. + + let egui::FullOutput { + platform_output, + textures_delta, + shapes, + pixels_per_point, + viewport_output, + } = egui_ctx.run(input, |ctx| { + viewport_ui_cb(ctx); + }); + + // --------------------------------------------------- + + let mut glutin = glutin.borrow_mut(); + + let GlutinWindowContext { + current_gl_context, + viewports, + .. + } = &mut *glutin; + + let Some(viewport) = viewports.get_mut(&ids.this) else { + return; + }; + + let Some(winit_state) = &mut viewport.egui_winit else { + return; + }; + let (Some(window), Some(gl_surface)) = (&viewport.window, &viewport.gl_surface) else { + return; + }; + + let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); + + let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); + + let mut painter = painter.borrow_mut(); + + *current_gl_context = Some( + current_gl_context + .take() + .unwrap() + .make_not_current() + .unwrap() + .make_current(gl_surface) + .unwrap(), + ); + + let current_gl_context = current_gl_context.as_ref().unwrap(); + + if !gl_surface.is_current(current_gl_context) { + log::error!("egui::show_viewport_immediate: viewport {:?} ({:?}) is not created in main thread, try to use wgpu!", viewport.ids.this, viewport.builder.title); + } + + let gl = &painter.gl().clone(); + egui_glow::painter::clear(gl, screen_size_in_pixels, [0.0, 0.0, 0.0, 0.0]); + + painter.paint_and_update_textures( + screen_size_in_pixels, + pixels_per_point, + &clipped_primitives, + &textures_delta, + ); + + { + crate::profile_scope!("swap_buffers"); + if let Err(err) = gl_surface.swap_buffers(current_gl_context) { + log::error!("swap_buffers failed: {err}"); + } + } + + winit_state.handle_platform_output(window, ids.this, egui_ctx, platform_output); + + glutin.handle_viewport_output(viewport_output); +} + +#[cfg(feature = "__screenshot")] +fn save_screeshot_and_exit( + path: &str, + painter: &egui_glow::Painter, + screen_size_in_pixels: [u32; 2], +) { + assert!( + path.ends_with(".png"), + "Expected EFRAME_SCREENSHOT_TO to end with '.png', got {path:?}" + ); + let screenshot = painter.read_screen_rgba(screen_size_in_pixels); + image::save_buffer( + path, + screenshot.as_raw(), + screenshot.width() as u32, + screenshot.height() as u32, + image::ColorType::Rgba8, + ) + .unwrap_or_else(|err| { + panic!("Failed to save screenshot to {path:?}: {err}"); + }); + eprintln!("Screenshot saved to {path:?}."); + + #[allow(clippy::exit)] + std::process::exit(0); +} diff --git a/crates/eframe/src/native/mod.rs b/crates/eframe/src/native/mod.rs index 8b606155..5fadd561 100644 --- a/crates/eframe/src/native/mod.rs +++ b/crates/eframe/src/native/mod.rs @@ -5,3 +5,11 @@ pub mod run; /// File storage which can be used by native backends. #[cfg(feature = "persistence")] pub mod file_storage; + +pub(crate) mod winit_integration; + +#[cfg(feature = "glow")] +mod glow_integration; + +#[cfg(feature = "wgpu")] +mod wgpu_integration; diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index cad2d6dc..bd71fcd7 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -5,112 +5,18 @@ //! There is a bunch of improvements we could do, //! like removing a bunch of `unwraps`. -use std::{cell::RefCell, rc::Rc, sync::Arc, time::Instant}; +use std::{cell::RefCell, time::Instant}; -use raw_window_handle::{HasRawDisplayHandle as _, HasRawWindowHandle as _}; -use winit::{ - event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget}, - window::{Window, WindowId}, -}; +use winit::event_loop::{EventLoop, EventLoopBuilder}; -use egui::{epaint::ahash::HashMap, ViewportBuilder, ViewportId}; +use egui::epaint::ahash::HashMap; -#[cfg(feature = "accesskit")] -use egui_winit::accesskit_winit; -use egui_winit::winit; +use crate::{epi, native::winit_integration::EventResult, Result}; -use crate::{epi, Result}; - -use super::epi_integration::{self, EpiIntegration}; +use super::winit_integration::{UserEvent, WinitApp}; // ---------------------------------------------------------------------------- -pub const IS_DESKTOP: bool = cfg!(any( - target_os = "freebsd", - target_os = "linux", - target_os = "macos", - target_os = "openbsd", - target_os = "windows", -)); - -// ---------------------------------------------------------------------------- - -/// The custom even `eframe` uses with the [`winit`] event loop. -#[derive(Debug)] -pub enum UserEvent { - /// A repaint is requested. - RequestRepaint { - /// What to repaint. - viewport_id: ViewportId, - - /// When to repaint. - when: Instant, - - /// What the frame number was when the repaint was _requested_. - frame_nr: u64, - }, - - /// A request related to [`accesskit`](https://accesskit.dev/). - #[cfg(feature = "accesskit")] - AccessKitActionRequest(accesskit_winit::ActionRequestEvent), -} - -#[cfg(feature = "accesskit")] -impl From for UserEvent { - fn from(inner: accesskit_winit::ActionRequestEvent) -> Self { - Self::AccessKitActionRequest(inner) - } -} - -// ---------------------------------------------------------------------------- - -pub use epi::NativeOptions; - -#[derive(Debug)] -enum EventResult { - Wait, - - /// Causes a synchronous repaint inside the event handler. This should only - /// be used in special situations if the window must be repainted while - /// handling a specific event. This occurs on Windows when handling resizes. - /// - /// `RepaintNow` creates a new frame synchronously, and should therefore - /// only be used for extremely urgent repaints. - RepaintNow(WindowId), - - /// Queues a repaint for once the event loop handles its next redraw. Exists - /// so that multiple input events can be handled in one frame. Does not - /// cause any delay like `RepaintNow`. - RepaintNext(WindowId), - - RepaintAt(WindowId, Instant), - - Exit, -} - -trait WinitApp { - /// The current frame number, as reported by egui. - fn frame_nr(&self, viewport_id: ViewportId) -> u64; - - fn is_focused(&self, window_id: WindowId) -> bool; - - fn integration(&self) -> Option<&EpiIntegration>; - - fn window(&self, window_id: WindowId) -> Option>; - - fn window_id_from_viewport_id(&self, id: ViewportId) -> Option; - - fn save_and_destroy(&mut self); - - fn run_ui_and_paint(&mut self, window_id: WindowId) -> EventResult; - - fn on_event( - &mut self, - event_loop: &EventLoopWindowTarget, - event: &winit::event::Event<'_, UserEvent>, - ) -> Result; -} - fn create_event_loop_builder( native_options: &mut epi::NativeOptions, ) -> EventLoopBuilder { @@ -138,7 +44,7 @@ fn create_event_loop(native_options: &mut epi::NativeOptions) -> EventLoop( mut native_options: epi::NativeOptions, - f: impl FnOnce(&mut EventLoop, NativeOptions) -> R, + f: impl FnOnce(&mut EventLoop, epi::NativeOptions) -> R, ) -> R { thread_local!(static EVENT_LOOP: RefCell>> = RefCell::new(None)); @@ -157,7 +63,7 @@ fn run_and_return( event_loop: &mut EventLoop, mut winit_app: impl WinitApp, ) -> Result<()> { - use winit::platform::run_return::EventLoopExtRunReturn as _; + use winit::{event_loop::ControlFlow, platform::run_return::EventLoopExtRunReturn as _}; log::debug!("Entering the winit event loop (run_return)…"); @@ -447,2451 +353,53 @@ fn run_and_exit(event_loop: EventLoop, mut winit_app: impl WinitApp + } // ---------------------------------------------------------------------------- -/// Run an egui app + #[cfg(feature = "glow")] -mod glow_integration { - use glutin::{ - display::GetGlDisplay, - prelude::{GlDisplay, NotCurrentGlContextSurfaceAccessor, PossiblyCurrentGlContext}, - surface::GlSurface, - }; +pub fn run_glow( + app_name: &str, + mut native_options: epi::NativeOptions, + app_creator: epi::AppCreator, +) -> Result<()> { + use super::glow_integration::GlowWinitApp; - use egui::{ - epaint::ahash::HashMap, DeferredViewportUiCallback, ImmediateViewport, NumExt as _, - ViewportClass, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, ViewportOutput, - }; - use egui_winit::{ - apply_viewport_builder_to_new_window, create_winit_window_builder, - process_viewport_commands, EventResponse, - }; - - use crate::native::epi_integration::EpiIntegration; - - use super::*; - - // 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 { - integration: EpiIntegration, - app: Box, - - // These needs to be shared with the immediate viewport renderer, hence the Rc/Arc/RefCells: - glutin: Rc>, - painter: Rc>, - } - - impl GlowWinitRunning { - fn run_ui_and_paint( - &mut self, - window_id: WindowId, - focused_viewport: Option, - ) -> EventResult { - let Some(viewport_id) = self - .glutin - .borrow() - .viewport_from_window - .get(&window_id) - .copied() - else { - return EventResult::Wait; - }; - - #[cfg(feature = "puffin")] - puffin::GlobalProfiler::lock().new_frame(); - crate::profile_scope!("frame"); - - { - let glutin = self.glutin.borrow(); - let viewport = &glutin.viewports[&viewport_id]; - let is_immediate = viewport.viewport_ui_cb.is_none(); - if is_immediate && viewport_id != ViewportId::ROOT { - // This will only happen if this is an immediate viewport. - // That means that the viewport cannot be rendered by itself and needs his parent to be rendered. - if let Some(parent_viewport) = glutin.viewports.get(&viewport.ids.parent) { - if let Some(window) = parent_viewport.window.as_ref() { - return EventResult::RepaintNext(window.id()); - } - } - return EventResult::Wait; - } - } - - let (raw_input, viewport_ui_cb) = { - let mut glutin = self.glutin.borrow_mut(); - let viewport = glutin.viewports.get_mut(&viewport_id).unwrap(); - viewport.update_viewport_info(); - let window = viewport.window.as_ref().unwrap(); - - let egui_winit = viewport.egui_winit.as_mut().unwrap(); - let mut raw_input = egui_winit.take_egui_input(window, viewport.ids); - let viewport_ui_cb = viewport.viewport_ui_cb.clone(); - - self.integration.pre_update(); - - raw_input.time = Some(self.integration.beginning.elapsed().as_secs_f64()); - raw_input.viewports = glutin - .viewports - .iter() - .map(|(id, viewport)| (*id, viewport.info.clone())) - .collect(); - - (raw_input, viewport_ui_cb) - }; - - // ------------------------------------------------------------ - // The update function, which could call immediate viewports, - // so make sure we don't hold any locks here required by the immediate viewports rendeer. - - let full_output = - self.integration - .update(self.app.as_mut(), viewport_ui_cb.as_deref(), raw_input); - - // ------------------------------------------------------------ - - let Self { - integration, - app, - glutin, - painter, - .. - } = self; - - let mut glutin = glutin.borrow_mut(); - let mut painter = painter.borrow_mut(); - - let egui::FullOutput { - platform_output, - textures_delta, - shapes, - pixels_per_point, - viewport_output, - } = full_output; - - let GlutinWindowContext { - viewports, - current_gl_context, - .. - } = &mut *glutin; - - let viewport = viewports.get_mut(&viewport_id).unwrap(); - let window = viewport.window.as_ref().unwrap(); - let gl_surface = viewport.gl_surface.as_ref().unwrap(); - let egui_winit = viewport.egui_winit.as_mut().unwrap(); - - integration.post_update(); - integration.handle_platform_output(window, viewport_id, platform_output, egui_winit); - - let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point); - - *current_gl_context = Some( - current_gl_context - .take() - .unwrap() - .make_not_current() - .unwrap() - .make_current(gl_surface) - .unwrap(), - ); - - let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); - - painter.clear( - screen_size_in_pixels, - app.clear_color(&integration.egui_ctx.style().visuals), - ); - - painter.paint_and_update_textures( - screen_size_in_pixels, - pixels_per_point, - &clipped_primitives, - &textures_delta, - ); - - { - let screenshot_requested = std::mem::take(&mut viewport.screenshot_requested); - if screenshot_requested { - let screenshot = painter.read_screen_rgba(screen_size_in_pixels); - egui_winit - .egui_input_mut() - .events - .push(egui::Event::Screenshot { - viewport_id, - image: screenshot.into(), - }); - } - integration.post_rendering(window); - } - - { - crate::profile_scope!("swap_buffers"); - if let Err(err) = gl_surface.swap_buffers( - current_gl_context - .as_ref() - .expect("failed to get current context to swap buffers"), - ) { - log::error!("swap_buffers failed: {err}"); - } - } - - // give it time to settle: - #[cfg(feature = "__screenshot")] - if integration.egui_ctx.frame_nr() == 2 { - if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") { - save_screeshot_and_exit(&path, &painter, screen_size_in_pixels); - } - } - - integration.maybe_autosave(app.as_mut(), Some(window)); - - if window.is_minimized() == Some(true) { - // On Mac, a minimized Window uses up all CPU: - // https://github.com/emilk/egui/issues/325 - crate::profile_scope!("minimized_sleep"); - std::thread::sleep(std::time::Duration::from_millis(10)); - } - - glutin.handle_viewport_output(viewport_output, focused_viewport); - - if integration.should_close() { - EventResult::Exit - } else { - EventResult::Wait - } - } - - fn on_window_event( - &mut self, - window_id: WindowId, - event: &winit::event::WindowEvent<'_>, - focused_viewport: &mut Option, - ) -> EventResult { - let viewport_id = self - .glutin - .borrow() - .viewport_from_window - .get(&window_id) - .copied(); - - // On Windows, if a window is resized by the user, it should repaint synchronously, inside the - // event handler. - // - // If this is not done, the compositor will assume that the window does not want to redraw, - // and continue ahead. - // - // In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver - // new frames to the compositor in time. - // - // The flickering is technically glutin or glow's fault, but we should be responding properly - // to resizes anyway, as doing so avoids dropping frames. - // - // See: https://github.com/emilk/egui/issues/903 - let mut repaint_asap = false; - - match event { - winit::event::WindowEvent::Focused(new_focused) => { - *focused_viewport = new_focused.then(|| viewport_id).flatten(); - } - - 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 0 < physical_size.width && 0 < physical_size.height { - if let Some(viewport_id) = viewport_id { - repaint_asap = true; - self.glutin.borrow_mut().resize(viewport_id, *physical_size); - } - } - } - - winit::event::WindowEvent::ScaleFactorChanged { new_inner_size, .. } => { - if let Some(viewport_id) = viewport_id { - repaint_asap = true; - self.glutin - .borrow_mut() - .resize(viewport_id, **new_inner_size); - } - } - - winit::event::WindowEvent::CloseRequested => { - let is_root = viewport_id == Some(ViewportId::ROOT); - if is_root && self.integration.should_close() { - log::debug!("Received WindowEvent::CloseRequested"); - return EventResult::Exit; - } - } - - _ => {} - } - - let event_response = 'res: { - if let Some(viewport_id) = viewport_id { - let mut glutin = self.glutin.borrow_mut(); - if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) { - break 'res self.integration.on_event( - self.app.as_mut(), - event, - viewport.egui_winit.as_mut().unwrap(), - viewport.ids.this, - ); - } - } - - EventResponse { - consumed: false, - repaint: false, - } - }; - - if self.integration.should_close() { - EventResult::Exit - } else if event_response.repaint { - if repaint_asap { - EventResult::RepaintNow(window_id) - } else { - EventResult::RepaintNext(window_id) - } - } else { - EventResult::Wait - } - } - } - - #[cfg(feature = "__screenshot")] - fn save_screeshot_and_exit( - path: &str, - painter: &egui_glow::Painter, - screen_size_in_pixels: [u32; 2], - ) { - assert!( - path.ends_with(".png"), - "Expected EFRAME_SCREENSHOT_TO to end with '.png', got {path:?}" - ); - let screenshot = painter.read_screen_rgba(screen_size_in_pixels); - image::save_buffer( - path, - screenshot.as_raw(), - screenshot.width() as u32, - screenshot.height() as u32, - image::ColorType::Rgba8, - ) - .unwrap_or_else(|err| { - panic!("Failed to save screenshot to {path:?}: {err}"); + #[cfg(not(target_os = "ios"))] + if native_options.run_and_return { + return 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) }); - eprintln!("Screenshot saved to {path:?}."); - - #[allow(clippy::exit)] - std::process::exit(0); } - struct Viewport { - ids: ViewportIdPair, - class: ViewportClass, - builder: ViewportBuilder, - info: ViewportInfo, - screenshot_requested: bool, - - /// The user-callback that shows the ui. - /// None for immediate viewports. - viewport_ui_cb: Option>, - - gl_surface: Option>, - window: Option>, - egui_winit: Option, - } - - impl Viewport { - /// Update the stored `ViewportInfo`. - pub fn update_viewport_info(&mut self) { - let Some(window) = &self.window else { - return; - }; - let Some(egui_winit) = &self.egui_winit else { - return; - }; - egui_winit.update_viewport_info(&mut self.info, window); - } - } - - /// This struct will contain both persistent and temporary glutin state. - /// - /// Platform Quirks: - /// * Microsoft Windows: requires that we create a window before opengl context. - /// * Android: window and surface should be destroyed when we receive a suspend event. recreate on resume event. - /// - /// winit guarantees that we will get a Resumed event on startup on all platforms. - /// * Before Resumed event: `gl_config`, `gl_context` can be created at any time. on windows, a window must be created to get `gl_context`. - /// * Resumed: `gl_surface` will be created here. `window` will be re-created here for android. - /// * Suspended: on android, we drop window + surface. on other platforms, we don't get Suspended event. - /// - /// The setup is divided between the `new` fn and `on_resume` fn. we can just assume that `on_resume` is a continuation of - /// `new` fn on all platforms. only on android, do we get multiple resumed events because app can be suspended. - struct GlutinWindowContext { - swap_interval: glutin::surface::SwapInterval, - gl_config: glutin::config::Config, - - max_texture_side: Option, - - current_gl_context: Option, - not_current_gl_context: Option, - - viewports: ViewportIdMap, - viewport_from_window: HashMap, - window_from_viewport: ViewportIdMap, - } - - impl GlutinWindowContext { - #[allow(unsafe_code)] - unsafe fn new( - viewport_builder: ViewportBuilder, - native_options: &epi::NativeOptions, - event_loop: &EventLoopWindowTarget, - ) -> Result { - crate::profile_function!(); - - // There is a lot of complexity with opengl creation, - // so prefer extensive logging to get all the help we can to debug issues. - - use glutin::prelude::*; - // convert native options to glutin options - let hardware_acceleration = match native_options.hardware_acceleration { - crate::HardwareAcceleration::Required => Some(true), - crate::HardwareAcceleration::Preferred => None, - crate::HardwareAcceleration::Off => Some(false), - }; - let swap_interval = if native_options.vsync { - glutin::surface::SwapInterval::Wait(std::num::NonZeroU32::new(1).unwrap()) - } else { - glutin::surface::SwapInterval::DontWait - }; - /* opengl setup flow goes like this: - 1. we create a configuration for opengl "Display" / "Config" creation - 2. choose between special extensions like glx or egl or wgl and use them to create config/display - 3. opengl context configuration - 4. opengl context creation - */ - // start building config for gl display - let config_template_builder = glutin::config::ConfigTemplateBuilder::new() - .prefer_hardware_accelerated(hardware_acceleration) - .with_depth_size(native_options.depth_buffer) - .with_stencil_size(native_options.stencil_buffer) - .with_transparency(native_options.viewport.transparent.unwrap_or(false)); - // we don't know if multi sampling option is set. so, check if its more than 0. - let config_template_builder = if native_options.multisampling > 0 { - config_template_builder.with_multisampling( - native_options - .multisampling - .try_into() - .expect("failed to fit multisamples option of native_options into u8"), - ) - } else { - config_template_builder - }; - - log::debug!("trying to create glutin Display with config: {config_template_builder:?}"); - - // Create GL display. This may probably create a window too on most platforms. Definitely on `MS windows`. Never on Android. - let display_builder = glutin_winit::DisplayBuilder::new() - // we might want to expose this option to users in the future. maybe using an env var or using native_options. - .with_preference(glutin_winit::ApiPrefence::FallbackEgl) // https://github.com/emilk/egui/issues/2520#issuecomment-1367841150 - .with_window_builder(Some(create_winit_window_builder(viewport_builder.clone()))); - - let (window, gl_config) = { - crate::profile_scope!("DisplayBuilder::build"); - - display_builder - .build( - event_loop, - config_template_builder.clone(), - |mut config_iterator| { - let config = config_iterator.next().expect( - "failed to find a matching configuration for creating glutin config", - ); - log::debug!( - "using the first config from config picker closure. config: {config:?}" - ); - config - }, - ) - .map_err(|e| { - crate::Error::NoGlutinConfigs(config_template_builder.build(), e) - })? - }; - if let Some(window) = &window { - apply_viewport_builder_to_new_window(window, &viewport_builder); - } - - let gl_display = gl_config.display(); - log::debug!( - "successfully created GL Display with version: {} and supported features: {:?}", - gl_display.version_string(), - gl_display.supported_features() - ); - let raw_window_handle = window.as_ref().map(|w| w.raw_window_handle()); - log::debug!("creating gl context using raw window handle: {raw_window_handle:?}"); - - // create gl context. if core context cannot be created, try gl es context as fallback. - let context_attributes = - glutin::context::ContextAttributesBuilder::new().build(raw_window_handle); - let fallback_context_attributes = glutin::context::ContextAttributesBuilder::new() - .with_context_api(glutin::context::ContextApi::Gles(None)) - .build(raw_window_handle); - - let gl_context_result = { - crate::profile_scope!("create_context"); - gl_config - .display() - .create_context(&gl_config, &context_attributes) - }; - - let gl_context = match gl_context_result { - Ok(it) => it, - Err(err) => { - log::warn!("Failed to create context using default context attributes {context_attributes:?} due to error: {err}"); - log::debug!("Retrying with fallback context attributes: {fallback_context_attributes:?}"); - gl_config - .display() - .create_context(&gl_config, &fallback_context_attributes)? - } - }; - let not_current_gl_context = Some(gl_context); - - let mut viewport_from_window = HashMap::default(); - let mut window_from_viewport = ViewportIdMap::default(); - let mut info = ViewportInfo::default(); - if let Some(window) = &window { - viewport_from_window.insert(window.id(), ViewportId::ROOT); - window_from_viewport.insert(ViewportId::ROOT, window.id()); - info.minimized = window.is_minimized(); - info.maximized = Some(window.is_maximized()); - } - - let mut viewports = ViewportIdMap::default(); - viewports.insert( - ViewportId::ROOT, - Viewport { - ids: ViewportIdPair::ROOT, - class: ViewportClass::Root, - builder: viewport_builder, - info, - screenshot_requested: false, - viewport_ui_cb: None, - gl_surface: None, - window: window.map(Rc::new), - egui_winit: None, - }, - ); - - // the fun part with opengl gl is that we never know whether there is an error. the context creation might have failed, but - // it could keep working until we try to make surface current or swap buffers or something else. future glutin improvements might - // help us start from scratch again if we fail context creation and go back to preferEgl or try with different config etc.. - // https://github.com/emilk/egui/pull/2541#issuecomment-1370767582 - - let mut slf = GlutinWindowContext { - swap_interval, - gl_config, - current_gl_context: None, - not_current_gl_context, - viewports, - viewport_from_window, - max_texture_side: None, - window_from_viewport, - }; - - slf.on_resume(event_loop)?; - - Ok(slf) - } - - /// This will be run after `new`. on android, it might be called multiple times over the course of the app's lifetime. - /// roughly, - /// 1. check if window already exists. otherwise, create one now. - /// 2. create attributes for surface creation. - /// 3. create surface. - /// 4. make surface and context current. - /// - /// we presently assume that we will - fn on_resume(&mut self, event_loop: &EventLoopWindowTarget) -> Result<()> { - crate::profile_function!(); - - let viewports: Vec = self - .viewports - .iter() - .filter(|(_, viewport)| viewport.gl_surface.is_none()) - .map(|(id, _)| *id) - .collect(); - - for viewport_id in viewports { - self.init_viewport(viewport_id, event_loop)?; - } - Ok(()) - } - - #[allow(unsafe_code)] - pub(crate) fn init_viewport( - &mut self, - viewport_id: ViewportId, - event_loop: &EventLoopWindowTarget, - ) -> Result<()> { - crate::profile_function!(); - - let viewport = self - .viewports - .get_mut(&viewport_id) - .expect("viewport doesn't exist"); - - let window = if let Some(window) = &mut viewport.window { - window - } else { - log::trace!("Window doesn't exist yet. Creating one now with finalize_window"); - let window = glutin_winit::finalize_window( - event_loop, - create_winit_window_builder(viewport.builder.clone()), - &self.gl_config, - )?; - apply_viewport_builder_to_new_window(&window, &viewport.builder); - viewport.info.minimized = window.is_minimized(); - viewport.info.maximized = Some(window.is_maximized()); - viewport.window.insert(Rc::new(window)) - }; - - { - // surface attributes - let (width_px, height_px): (u32, u32) = window.inner_size().into(); - let width_px = std::num::NonZeroU32::new(width_px.at_least(1)).unwrap(); - let height_px = std::num::NonZeroU32::new(height_px.at_least(1)).unwrap(); - let surface_attributes = glutin::surface::SurfaceAttributesBuilder::< - glutin::surface::WindowSurface, - >::new() - .build(window.raw_window_handle(), width_px, height_px); - - log::trace!("creating surface with attributes: {surface_attributes:?}"); - let gl_surface = unsafe { - self.gl_config - .display() - .create_window_surface(&self.gl_config, &surface_attributes)? - }; - - log::trace!("surface created successfully: {gl_surface:?}. making context current"); - - let not_current_gl_context = - if let Some(not_current_context) = self.not_current_gl_context.take() { - not_current_context - } else { - self.current_gl_context - .take() - .unwrap() - .make_not_current() - .unwrap() - }; - let current_gl_context = not_current_gl_context.make_current(&gl_surface)?; - - // try setting swap interval. but its not absolutely necessary, so don't panic on failure. - log::trace!("made context current. setting swap interval for surface"); - if let Err(err) = - gl_surface.set_swap_interval(¤t_gl_context, self.swap_interval) - { - log::warn!("Failed to set swap interval due to error: {err}"); - } - - // we will reach this point only once in most platforms except android. - // create window/surface/make context current once and just use them forever. - - viewport.egui_winit.get_or_insert_with(|| { - egui_winit::State::new( - event_loop, - Some(window.scale_factor() as f32), - self.max_texture_side, - ) - }); - - viewport.gl_surface = Some(gl_surface); - self.current_gl_context = Some(current_gl_context); - self.viewport_from_window - .insert(window.id(), viewport.ids.this); - self.window_from_viewport - .insert(viewport.ids.this, window.id()); - } - - Ok(()) - } - - /// only applies for android. but we basically drop surface + window and make context not current - fn on_suspend(&mut self) -> Result<()> { - log::debug!("received suspend event. dropping window and surface"); - for viewport in self.viewports.values_mut() { - viewport.gl_surface = None; - viewport.window = None; - } - if let Some(current) = self.current_gl_context.take() { - log::debug!("context is current, so making it non-current"); - self.not_current_gl_context = Some(current.make_not_current()?); - } else { - log::debug!("context is already not current??? could be duplicate suspend event"); - } - Ok(()) - } - - fn viewport(&self, viewport_id: ViewportId) -> &Viewport { - self.viewports - .get(&viewport_id) - .expect("viewport doesn't exist") - } - - fn window(&self, viewport_id: ViewportId) -> Rc { - self.viewport(viewport_id) - .window - .clone() - .expect("winit window doesn't exist") - } - - fn resize( - &mut self, - viewport_id: ViewportId, - physical_size: winit::dpi::PhysicalSize, - ) { - let width_px = std::num::NonZeroU32::new(physical_size.width.at_least(1)).unwrap(); - let height_px = std::num::NonZeroU32::new(physical_size.height.at_least(1)).unwrap(); - - if let Some(viewport) = self.viewports.get(&viewport_id) { - if let Some(gl_surface) = &viewport.gl_surface { - self.current_gl_context = Some( - self.current_gl_context - .take() - .unwrap() - .make_not_current() - .unwrap() - .make_current(gl_surface) - .unwrap(), - ); - gl_surface.resize( - self.current_gl_context - .as_ref() - .expect("failed to get current context to resize surface"), - width_px, - height_px, - ); - } - } - } - - fn get_proc_address(&self, addr: &std::ffi::CStr) -> *const std::ffi::c_void { - self.gl_config.display().get_proc_address(addr) - } - - fn handle_viewport_output( - &mut self, - viewport_output: ViewportIdMap, - focused_viewport: Option, - ) { - crate::profile_function!(); - - let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect(); - - for ( - viewport_id, - ViewportOutput { - parent, - class, - builder, - viewport_ui_cb, - commands, - repaint_delay: _, // ignored - we listened to the repaint callback instead - }, - ) in viewport_output - { - let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent); - - let viewport = initialize_or_update_viewport( - &mut self.viewports, - ids, - class, - builder, - viewport_ui_cb, - focused_viewport, - ); - - if let Some(window) = &viewport.window { - let is_viewport_focused = focused_viewport == Some(viewport_id); - egui_winit::process_viewport_commands( - &mut viewport.info, - commands, - window, - is_viewport_focused, - &mut viewport.screenshot_requested, - ); - } - } - - // GC old viewports - self.viewports - .retain(|id, _| active_viewports_ids.contains(id)); - self.viewport_from_window - .retain(|_, id| active_viewports_ids.contains(id)); - self.window_from_viewport - .retain(|id, _| active_viewports_ids.contains(id)); - } - } - - fn initialize_or_update_viewport( - viewports: &mut ViewportIdMap, - ids: ViewportIdPair, - class: ViewportClass, - mut builder: ViewportBuilder, - viewport_ui_cb: Option>, - focused_viewport: Option, - ) -> &mut Viewport { - crate::profile_function!(); - - if builder.icon.is_none() { - // Inherit icon from parent - builder.icon = viewports - .get_mut(&ids.parent) - .and_then(|vp| vp.builder.icon.clone()); - } - - match viewports.entry(ids.this) { - std::collections::hash_map::Entry::Vacant(entry) => { - // New viewport: - log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title); - entry.insert(Viewport { - ids, - class, - builder, - info: Default::default(), - screenshot_requested: false, - viewport_ui_cb, - window: None, - egui_winit: None, - gl_surface: None, - }) - } - - std::collections::hash_map::Entry::Occupied(mut entry) => { - // Patch an existing viewport: - let viewport = entry.get_mut(); - - viewport.ids.parent = ids.parent; - viewport.class = class; - viewport.viewport_ui_cb = viewport_ui_cb; - - let (delta_commands, recreate) = viewport.builder.patch(&builder); - - if recreate { - log::debug!( - "Recreating window for viewport {:?} ({:?})", - ids.this, - builder.title - ); - viewport.window = None; - viewport.egui_winit = None; - } else if let Some(window) = &viewport.window { - let is_viewport_focused = focused_viewport == Some(ids.this); - process_viewport_commands( - &mut viewport.info, - delta_commands, - window, - is_viewport_focused, - &mut viewport.screenshot_requested, - ); - } - - entry.into_mut() - } - } - } - - 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, - - focused_viewport: Option, - } - - impl GlowWinitApp { - fn new( - event_loop: &EventLoop, - app_name: &str, - native_options: epi::NativeOptions, - app_creator: epi::AppCreator, - ) -> Self { - crate::profile_function!(); - 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), - focused_viewport: Some(ViewportId::ROOT), - } - } - - #[allow(unsafe_code)] - fn create_glutin_windowed_context( - event_loop: &EventLoopWindowTarget, - storage: Option<&dyn epi::Storage>, - native_options: &mut NativeOptions, - ) -> Result<(GlutinWindowContext, egui_glow::Painter)> { - crate::profile_function!(); - - let window_settings = epi_integration::load_window_settings(storage); - - let winit_window_builder = - epi_integration::viewport_builder(event_loop, native_options, window_settings); - - let mut glutin_window_context = unsafe { - GlutinWindowContext::new(winit_window_builder, native_options, event_loop)? - }; - - // Creates the window - must come before we create our glow context - glutin_window_context.on_resume(event_loop)?; - - if let Some(viewport) = glutin_window_context.viewports.get(&ViewportId::ROOT) { - if let Some(window) = &viewport.window { - epi_integration::apply_window_settings(window, window_settings); - } - } - - let gl = unsafe { - crate::profile_scope!("glow::Context::from_loader_function"); - Arc::new(glow::Context::from_loader_function(|s| { - let s = std::ffi::CString::new(s) - .expect("failed to construct C string from string for gl proc address"); - - glutin_window_context.get_proc_address(&s) - })) - }; - - let painter = egui_glow::Painter::new(gl, "", native_options.shader_version)?; - - Ok((glutin_window_context, painter)) - } - - fn init_run_state( - &mut self, - event_loop: &EventLoopWindowTarget, - ) -> Result<&mut GlowWinitRunning> { - crate::profile_function!(); - - let storage = epi_integration::create_storage( - self.native_options - .viewport - .app_id - .as_ref() - .unwrap_or(&self.app_name), - ); - - let (mut glutin, painter) = Self::create_glutin_windowed_context( - event_loop, - storage.as_deref(), - &mut self.native_options, - )?; - let gl = painter.gl().clone(); - - let max_texture_side = painter.max_texture_side(); - glutin.max_texture_side = Some(max_texture_side); - for viewport in glutin.viewports.values_mut() { - if let Some(egui_winit) = viewport.egui_winit.as_mut() { - egui_winit.set_max_texture_side(max_texture_side); - } - } - - let system_theme = system_theme(&glutin.window(ViewportId::ROOT), &self.native_options); - - let mut integration = EpiIntegration::new( - &glutin.window(ViewportId::ROOT), - system_theme, - &self.app_name, - &self.native_options, - storage, - IS_DESKTOP, - Some(gl.clone()), - #[cfg(feature = "wgpu")] - None, - ); - - { - let event_loop_proxy = self.repaint_proxy.clone(); - integration - .egui_ctx - .set_request_repaint_callback(move |info| { - log::trace!("request_repaint_callback: {info:?}"); - let when = Instant::now() + info.delay; - let frame_nr = info.current_frame_nr; - event_loop_proxy - .lock() - .send_event(UserEvent::RequestRepaint { - viewport_id: info.viewport_id, - when, - frame_nr, - }) - .ok(); - }); - } - - #[cfg(feature = "accesskit")] - { - let event_loop_proxy = self.repaint_proxy.lock().clone(); - let viewport = glutin.viewports.get_mut(&ViewportId::ROOT).unwrap(); - if let Viewport { - window: Some(window), - egui_winit: Some(egui_winit), - .. - } = viewport - { - integration.init_accesskit(egui_winit, window, event_loop_proxy); - } - } - - let theme = system_theme.unwrap_or(self.native_options.default_theme); - integration.egui_ctx.set_visuals(theme.egui_visuals()); - - if self - .native_options - .viewport - .mouse_passthrough - .unwrap_or(false) - { - if let Err(err) = glutin.window(ViewportId::ROOT).set_cursor_hittest(false) { - log::warn!("set_cursor_hittest(false) failed: {err}"); - } - } - - let app_creator = std::mem::take(&mut self.app_creator) - .expect("Single-use AppCreator has unexpectedly already been taken"); - - let app = { - let window = glutin.window(ViewportId::ROOT); - let cc = epi::CreationContext { - egui_ctx: integration.egui_ctx.clone(), - integration_info: integration.frame.info().clone(), - storage: integration.frame.storage(), - gl: Some(gl), - #[cfg(feature = "wgpu")] - wgpu_render_state: None, - raw_display_handle: window.raw_display_handle(), - raw_window_handle: window.raw_window_handle(), - }; - crate::profile_scope!("app_creator"); - app_creator(&cc) - }; - - let glutin = Rc::new(RefCell::new(glutin)); - let painter = Rc::new(RefCell::new(painter)); - - { - // Create weak pointers so that we don't keep - // state alive for too long. - let glutin = Rc::downgrade(&glutin); - let painter = Rc::downgrade(&painter); - let beginning = integration.beginning; - - let event_loop: *const EventLoopWindowTarget = event_loop; - - egui::Context::set_immediate_viewport_renderer( - move |egui_ctx, immediate_viewport| { - if let (Some(glutin), Some(painter)) = (glutin.upgrade(), painter.upgrade()) - { - // SAFETY: the event loop lives longer than - // the Rc:s we just upgraded above. - #[allow(unsafe_code)] - let event_loop = unsafe { event_loop.as_ref().unwrap() }; - - render_immediate_viewport( - event_loop, - egui_ctx, - &glutin, - &painter, - beginning, - immediate_viewport, - ); - } else { - log::warn!("render_sync_callback called after window closed"); - } - }, - ); - } - - Ok(self.running.insert(GlowWinitRunning { - glutin, - painter, - integration, - app, - })) - } - } - - /// This is called (via a callback) by user code to render immediate viewports, - /// i.e. viewport that are directly nested inside a parent viewport. - fn render_immediate_viewport( - event_loop: &EventLoopWindowTarget, - egui_ctx: &egui::Context, - glutin: &RefCell, - painter: &RefCell, - beginning: Instant, - immediate_viewport: ImmediateViewport<'_>, - ) { - crate::profile_function!(); - - let ImmediateViewport { - ids, - builder, - viewport_ui_cb, - } = immediate_viewport; - - { - let mut glutin = glutin.borrow_mut(); - - let viewport = initialize_or_update_viewport( - &mut glutin.viewports, - ids, - ViewportClass::Immediate, - builder, - None, - None, - ); - - if viewport.gl_surface.is_none() { - glutin.init_viewport(ids.this, event_loop).expect( - "Failed to initialize window in egui::Context::show_viewport_immediate", - ); - } - } - - let input = { - let mut glutin = glutin.borrow_mut(); - - let Some(viewport) = glutin.viewports.get_mut(&ids.this) else { - return; - }; - viewport.update_viewport_info(); - let Some(winit_state) = &mut viewport.egui_winit else { - return; - }; - let Some(window) = &viewport.window else { - return; - }; - - let mut raw_input = winit_state.take_egui_input(window, ids); - raw_input.viewports = glutin - .viewports - .iter() - .map(|(id, viewport)| (*id, viewport.info.clone())) - .collect(); - raw_input.time = Some(beginning.elapsed().as_secs_f64()); - raw_input - }; - - // --------------------------------------------------- - // Call the user ui-code, which could re-entrantly call this function again! - // No locks may be hold while calling this function. - - let egui::FullOutput { - platform_output, - textures_delta, - shapes, - pixels_per_point, - viewport_output, - } = egui_ctx.run(input, |ctx| { - viewport_ui_cb(ctx); - }); - - // --------------------------------------------------- - - let mut glutin = glutin.borrow_mut(); - - let GlutinWindowContext { - current_gl_context, - viewports, - .. - } = &mut *glutin; - - let Some(viewport) = viewports.get_mut(&ids.this) else { - return; - }; - - let Some(winit_state) = &mut viewport.egui_winit else { - return; - }; - let (Some(window), Some(gl_surface)) = (&viewport.window, &viewport.gl_surface) else { - return; - }; - - let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); - - let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); - - let mut painter = painter.borrow_mut(); - - *current_gl_context = Some( - current_gl_context - .take() - .unwrap() - .make_not_current() - .unwrap() - .make_current(gl_surface) - .unwrap(), - ); - - let current_gl_context = current_gl_context.as_ref().unwrap(); - - if !gl_surface.is_current(current_gl_context) { - log::error!("egui::show_viewport_immediate: viewport {:?} ({:?}) is not created in main thread, try to use wgpu!", viewport.ids.this, viewport.builder.title); - } - - let gl = &painter.gl().clone(); - egui_glow::painter::clear(gl, screen_size_in_pixels, [0.0, 0.0, 0.0, 0.0]); - - painter.paint_and_update_textures( - screen_size_in_pixels, - pixels_per_point, - &clipped_primitives, - &textures_delta, - ); - - { - crate::profile_scope!("swap_buffers"); - if let Err(err) = gl_surface.swap_buffers(current_gl_context) { - log::error!("swap_buffers failed: {err}"); - } - } - - winit_state.handle_platform_output(window, ids.this, egui_ctx, platform_output); - - let focused_viewport = None; // TODO - glutin.handle_viewport_output(viewport_output, focused_viewport); - } - - impl WinitApp for GlowWinitApp { - fn frame_nr(&self, viewport_id: ViewportId) -> u64 { - self.running - .as_ref() - .map_or(0, |r| r.integration.egui_ctx.frame_nr_for(viewport_id)) - } - - fn is_focused(&self, window_id: WindowId) -> bool { - if let Some(focused_viewport) = self.focused_viewport { - if let Some(running) = &self.running { - if let Some(window_id) = - running.glutin.borrow().viewport_from_window.get(&window_id) - { - return focused_viewport == *window_id; - } - } - } - false - } - - fn integration(&self) -> Option<&EpiIntegration> { - self.running.as_ref().map(|r| &r.integration) - } - - fn window(&self, window_id: WindowId) -> Option> { - let running = self.running.as_ref()?; - let glutin = running.glutin.borrow(); - let viewport_id = *glutin.viewport_from_window.get(&window_id)?; - if let Some(viewport) = glutin.viewports.get(&viewport_id) { - viewport.window.clone() - } else { - None - } - } - - fn window_id_from_viewport_id(&self, id: ViewportId) -> Option { - self.running - .as_ref() - .and_then(|r| r.glutin.borrow().window_from_viewport.get(&id).copied()) - } - - fn save_and_destroy(&mut self) { - if let Some(mut running) = self.running.take() { - crate::profile_function!(); - - running.integration.save( - running.app.as_mut(), - Some(&running.glutin.borrow().window(ViewportId::ROOT)), - ); - running.app.on_exit(Some(running.painter.borrow().gl())); - running.painter.borrow_mut().destroy(); - } - } - - fn run_ui_and_paint(&mut self, window_id: WindowId) -> EventResult { - if let Some(running) = &mut self.running { - running.run_ui_and_paint(window_id, self.focused_viewport) - } else { - EventResult::Wait - } - } - - fn on_event( - &mut self, - event_loop: &EventLoopWindowTarget, - event: &winit::event::Event<'_, UserEvent>, - ) -> Result { - crate::profile_function!(); - - Ok(match event { - winit::event::Event::Resumed => { - let running = if let Some(running) = &mut self.running { - // not the first resume event. create whatever you need. - running.glutin.borrow_mut().on_resume(event_loop)?; - running - } else { - // first resume event. - // we can actually move this outside of event loop. - // and just run the on_resume fn of gl_window - self.init_run_state(event_loop)? - }; - let window_id = running - .glutin - .borrow() - .window_from_viewport - .get(&ViewportId::ROOT) - .copied(); - EventResult::RepaintNow(window_id.unwrap()) - } - - winit::event::Event::Suspended => { - if let Some(running) = &mut self.running { - running.glutin.borrow_mut().on_suspend()?; - } - EventResult::Wait - } - - winit::event::Event::MainEventsCleared => { - if let Some(running) = &self.running { - if let Err(err) = running.glutin.borrow_mut().on_resume(event_loop) { - log::warn!("on_resume failed {err}"); - } - } - EventResult::Wait - } - - winit::event::Event::WindowEvent { event, window_id } => { - if let Some(running) = &mut self.running { - running.on_window_event(*window_id, event, &mut self.focused_viewport) - } else { - EventResult::Wait - } - } - - #[cfg(feature = "accesskit")] - winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest( - accesskit_winit::ActionRequestEvent { request, window_id }, - )) => { - if let Some(running) = &self.running { - let mut glutin = running.glutin.borrow_mut(); - if let Some(viewport_id) = - glutin.viewport_from_window.get(window_id).copied() - { - if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) { - if let Some(egui_winit) = &mut viewport.egui_winit { - crate::profile_scope!("on_accesskit_action_request"); - egui_winit.on_accesskit_action_request(request.clone()); - } - } - } - // As a form of user input, accessibility actions should - // lead to a repaint. - EventResult::RepaintNext(*window_id) - } else { - EventResult::Wait - } - } - _ => EventResult::Wait, - }) - } - } - - pub fn run_glow( - app_name: &str, - mut native_options: epi::NativeOptions, - app_creator: epi::AppCreator, - ) -> Result<()> { - #[cfg(not(target_os = "ios"))] - if native_options.run_and_return { - return 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) - }); - } - - let event_loop = create_event_loop(&mut native_options); - let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator); - run_and_exit(event_loop, glow_eframe); - } + let event_loop = create_event_loop(&mut native_options); + let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator); + run_and_exit(event_loop, glow_eframe); } -#[cfg(feature = "glow")] -pub use glow_integration::run_glow; // ---------------------------------------------------------------------------- #[cfg(feature = "wgpu")] -mod wgpu_integration { - use parking_lot::Mutex; +pub fn run_wgpu( + app_name: &str, + mut native_options: epi::NativeOptions, + app_creator: epi::AppCreator, +) -> Result<()> { + use super::wgpu_integration::WgpuWinitApp; - use egui::{ - DeferredViewportUiCallback, FullOutput, ImmediateViewport, ViewportClass, ViewportIdMap, - ViewportIdPair, ViewportIdSet, ViewportInfo, ViewportOutput, - }; - use egui_winit::{ - apply_viewport_builder_to_new_window, create_winit_window_builder, - process_viewport_commands, - }; - - use crate::native::epi_integration::EpiIntegration; - - use super::*; - - pub struct Viewport { - ids: ViewportIdPair, - class: ViewportClass, - builder: ViewportBuilder, - info: ViewportInfo, - screenshot_requested: bool, - - /// `None` for sync viewports. - viewport_ui_cb: 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>, - - /// `window` and `egui_winit` are initialized together. - egui_winit: Option, - } - - impl Viewport { - fn init_window( - &mut self, - windows_id: &mut HashMap, - painter: &mut egui_wgpu::winit::Painter, - event_loop: &EventLoopWindowTarget, - ) { - crate::profile_function!(); - - let viewport_id = self.ids.this; - - match create_winit_window_builder(self.builder.clone()).build(event_loop) { - Ok(window) => { - apply_viewport_builder_to_new_window(&window, &self.builder); - - windows_id.insert(window.id(), viewport_id); - - if let Err(err) = - pollster::block_on(painter.set_window(viewport_id, Some(&window))) - { - log::error!("on set_window: viewport_id {viewport_id:?} {err}"); - } - - self.egui_winit = Some(egui_winit::State::new( - event_loop, - Some(window.scale_factor() as f32), - painter.max_texture_side(), - )); - - self.info.minimized = window.is_minimized(); - self.info.maximized = Some(window.is_maximized()); - - self.window = Some(Rc::new(window)); - } - Err(err) => { - log::error!("Failed to create window: {err}"); - } - } - } - - /// Update the stored `ViewportInfo`. - pub fn update_viewport_info(&mut self) { - let Some(window) = &self.window else { - return; - }; - let Some(egui_winit) = &self.egui_winit else { - return; - }; - egui_winit.update_viewport_info(&mut self.info, window); - } - } - - pub type Viewports = ViewportIdMap; - - /// Everything needed by the immediate viewport renderer. - /// - /// Wrapped in an `Rc>` so it can be re-entrantly shared via a weak-pointer. - pub struct SharedState { - viewports: Viewports, - painter: egui_wgpu::winit::Painter, - viewport_from_window: HashMap, - } - - /// 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 { - integration: EpiIntegration, - - /// The users application. - app: Box, - - /// Wrapped in an `Rc>` so it can be re-entrantly shared via a weak-pointer. - shared: Rc>, - } - - struct WgpuWinitApp { - repaint_proxy: Arc>>, - app_name: String, - native_options: epi::NativeOptions, - - /// Set at initialization, then taken and set to `None` in `init_run_state`. - app_creator: Option, - - /// Set when we are actually up and running. - running: Option, - - focused_viewport: Option, - } - - impl WgpuWinitApp { - fn new( - event_loop: &EventLoop, - app_name: &str, - native_options: epi::NativeOptions, - app_creator: epi::AppCreator, - ) -> Self { - crate::profile_function!(); - - #[cfg(feature = "__screenshot")] - assert!( - std::env::var("EFRAME_SCREENSHOT_TO").is_err(), - "EFRAME_SCREENSHOT_TO not yet implemented for wgpu backend" - ); - - Self { - repaint_proxy: Arc::new(Mutex::new(event_loop.create_proxy())), - app_name: app_name.to_owned(), - native_options, - running: None, - app_creator: Some(app_creator), - focused_viewport: Some(ViewportId::ROOT), - } - } - - fn build_windows(&mut self, event_loop: &EventLoopWindowTarget) { - let Some(running) = &mut self.running else { - return; - }; - let mut shared = running.shared.borrow_mut(); - let SharedState { - viewports, - painter, - viewport_from_window, - } = &mut *shared; - - for viewport in viewports.values_mut() { - if viewport.window.is_none() { - viewport.init_window(viewport_from_window, painter, event_loop); - } - } - } - - #[cfg(target_os = "android")] - fn drop_window(&mut self) -> Result<(), egui_wgpu::WgpuError> { - if let Some(running) = &mut self.running { - let mut shared = running.shared.borrow_mut(); - shared.viewports.remove(&ViewportId::ROOT); - pollster::block_on(shared.painter.set_window(ViewportId::ROOT, None))?; - } - Ok(()) - } - - fn init_run_state( - &mut self, - event_loop: &EventLoopWindowTarget, - storage: Option>, - window: Window, - builder: ViewportBuilder, - ) -> Result<&mut WgpuWinitRunning, egui_wgpu::WgpuError> { - crate::profile_function!(); - - #[allow(unsafe_code, unused_mut, unused_unsafe)] - let mut painter = egui_wgpu::winit::Painter::new( - self.native_options.wgpu_options.clone(), - self.native_options.multisampling.max(1) as _, - egui_wgpu::depth_format_from_bits( - self.native_options.depth_buffer, - self.native_options.stencil_buffer, - ), - self.native_options.viewport.transparent.unwrap_or(false), - ); - pollster::block_on(painter.set_window(ViewportId::ROOT, Some(&window)))?; - - let wgpu_render_state = painter.render_state(); - - let system_theme = system_theme(&window, &self.native_options); - let mut integration = EpiIntegration::new( - &window, - system_theme, - &self.app_name, - &self.native_options, - storage, - IS_DESKTOP, - #[cfg(feature = "glow")] - None, - wgpu_render_state.clone(), - ); - - { - let event_loop_proxy = self.repaint_proxy.clone(); - - integration - .egui_ctx - .set_request_repaint_callback(move |info| { - log::trace!("request_repaint_callback: {info:?}"); - let when = Instant::now() + info.delay; - let frame_nr = info.current_frame_nr; - - event_loop_proxy - .lock() - .send_event(UserEvent::RequestRepaint { - when, - frame_nr, - viewport_id: info.viewport_id, - }) - .ok(); - }); - } - - let mut egui_winit = egui_winit::State::new( - event_loop, - Some(window.scale_factor() as f32), - painter.max_texture_side(), - ); - - #[cfg(feature = "accesskit")] - { - let event_loop_proxy = self.repaint_proxy.lock().clone(); - integration.init_accesskit(&mut egui_winit, &window, event_loop_proxy); - } - let theme = system_theme.unwrap_or(self.native_options.default_theme); - integration.egui_ctx.set_visuals(theme.egui_visuals()); - - let app_creator = std::mem::take(&mut self.app_creator) - .expect("Single-use AppCreator has unexpectedly already been taken"); - let cc = epi::CreationContext { - egui_ctx: integration.egui_ctx.clone(), - integration_info: integration.frame.info().clone(), - storage: integration.frame.storage(), - #[cfg(feature = "glow")] - gl: None, - wgpu_render_state, - raw_display_handle: window.raw_display_handle(), - raw_window_handle: window.raw_window_handle(), - }; - let app = { - crate::profile_scope!("user_app_creator"); - app_creator(&cc) - }; - - let mut viewport_from_window = HashMap::default(); - viewport_from_window.insert(window.id(), ViewportId::ROOT); - - let mut viewports = Viewports::default(); - viewports.insert( - ViewportId::ROOT, - Viewport { - ids: ViewportIdPair::ROOT, - class: ViewportClass::Root, - builder, - info: ViewportInfo { - minimized: window.is_minimized(), - maximized: Some(window.is_maximized()), - ..Default::default() - }, - screenshot_requested: false, - viewport_ui_cb: None, - window: Some(Rc::new(window)), - egui_winit: Some(egui_winit), - }, - ); - - let shared = Rc::new(RefCell::new(SharedState { - viewport_from_window, - viewports, - painter, - })); - - { - // Create a weak pointer so that we don't keep state alive for too long. - let shared = Rc::downgrade(&shared); - let beginning = integration.beginning; - - let event_loop: *const EventLoopWindowTarget = event_loop; - - egui::Context::set_immediate_viewport_renderer( - move |egui_ctx, immediate_viewport| { - if let Some(shared) = shared.upgrade() { - // SAFETY: the event loop lives longer than - // the Rc:s we just upgraded above. - #[allow(unsafe_code)] - let event_loop = unsafe { event_loop.as_ref().unwrap() }; - - render_immediate_viewport( - event_loop, - egui_ctx, - beginning, - &shared, - immediate_viewport, - ); - } else { - log::warn!("render_sync_callback called after window closed"); - } - }, - ); - } - - Ok(self.running.insert(WgpuWinitRunning { - integration, - app, - shared, - })) - } - } - - fn create_window( - event_loop: &EventLoopWindowTarget, - storage: Option<&dyn epi::Storage>, - native_options: &mut NativeOptions, - ) -> Result<(Window, ViewportBuilder), winit::error::OsError> { - crate::profile_function!(); - - let window_settings = epi_integration::load_window_settings(storage); - let viewport_builder = - epi_integration::viewport_builder(event_loop, native_options, window_settings); - let window = { - crate::profile_scope!("WindowBuilder::build"); - create_winit_window_builder(viewport_builder.clone()).build(event_loop)? - }; - apply_viewport_builder_to_new_window(&window, &viewport_builder); - epi_integration::apply_window_settings(&window, window_settings); - Ok((window, viewport_builder)) - } - - fn render_immediate_viewport( - event_loop: &EventLoopWindowTarget, - egui_ctx: &egui::Context, - beginning: Instant, - shared: &RefCell, - immediate_viewport: ImmediateViewport<'_>, - ) { - crate::profile_function!(); - - let ImmediateViewport { - ids, - builder, - viewport_ui_cb, - } = immediate_viewport; - - let input = { - let SharedState { - viewports, - painter, - viewport_from_window, - } = &mut *shared.borrow_mut(); - - let viewport = initialize_or_update_viewport( - viewports, - ids, - ViewportClass::Immediate, - builder, - None, - None, - ); - if viewport.window.is_none() { - viewport.init_window(viewport_from_window, painter, event_loop); - } - viewport.update_viewport_info(); - - let (Some(window), Some(winit_state)) = (&viewport.window, &mut viewport.egui_winit) - else { - return; - }; - - let mut input = winit_state.take_egui_input(window, ids); - input.viewports = viewports - .iter() - .map(|(id, viewport)| (*id, viewport.info.clone())) - .collect(); - input.time = Some(beginning.elapsed().as_secs_f64()); - input - }; - - // ------------------------------------------ - - // Run the user code, which could re-entrantly call this function again (!). - // Make sure no locks are held during this call. - let egui::FullOutput { - platform_output, - textures_delta, - shapes, - pixels_per_point, - viewport_output, - } = egui_ctx.run(input, |ctx| { - viewport_ui_cb(ctx); + #[cfg(not(target_os = "ios"))] + if native_options.run_and_return { + return 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) }); - - // ------------------------------------------ - - let mut shared = shared.borrow_mut(); - let SharedState { - viewports, painter, .. - } = &mut *shared; - - let Some(viewport) = viewports.get_mut(&ids.this) else { - return; - }; - let Some(winit_state) = &mut viewport.egui_winit else { - return; - }; - let Some(window) = &viewport.window else { - return; - }; - - if let Err(err) = pollster::block_on(painter.set_window(ids.this, Some(window))) { - log::error!( - "when rendering viewport_id={:?}, set_window Error {err}", - ids.this - ); - } - - let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); - painter.paint_and_update_textures( - ids.this, - pixels_per_point, - [0.0, 0.0, 0.0, 0.0], - &clipped_primitives, - &textures_delta, - false, - ); - - winit_state.handle_platform_output(window, ids.this, egui_ctx, platform_output); - - let focused_viewport = None; // TODO - handle_viewport_output(viewport_output, viewports, focused_viewport); } - impl WinitApp for WgpuWinitApp { - fn frame_nr(&self, viewport_id: ViewportId) -> u64 { - self.running - .as_ref() - .map_or(0, |r| r.integration.egui_ctx.frame_nr_for(viewport_id)) - } - - fn is_focused(&self, window_id: WindowId) -> bool { - let viewport_id = self.running.as_ref().and_then(|r| { - r.shared - .borrow() - .viewport_from_window - .get(&window_id) - .copied() - }); - - self.focused_viewport.is_some() && self.focused_viewport == viewport_id - } - - fn integration(&self) -> Option<&EpiIntegration> { - self.running.as_ref().map(|r| &r.integration) - } - - fn window(&self, window_id: WindowId) -> Option> { - self.running - .as_ref() - .and_then(|r| { - let shared = r.shared.borrow(); - shared - .viewport_from_window - .get(&window_id) - .and_then(|id| shared.viewports.get(id).map(|v| v.window.clone())) - }) - .flatten() - } - - fn window_id_from_viewport_id(&self, id: ViewportId) -> Option { - Some( - self.running - .as_ref()? - .shared - .borrow() - .viewports - .get(&id)? - .window - .as_ref()? - .id(), - ) - } - - fn save_and_destroy(&mut self) { - if let Some(mut running) = self.running.take() { - running.save_and_destroy(); - } - } - - fn run_ui_and_paint(&mut self, window_id: WindowId) -> EventResult { - if let Some(running) = &mut self.running { - running.run_ui_and_paint(window_id, self.focused_viewport) - } else { - EventResult::Wait - } - } - - fn on_event( - &mut self, - event_loop: &EventLoopWindowTarget, - event: &winit::event::Event<'_, UserEvent>, - ) -> Result { - crate::profile_function!(); - - self.build_windows(event_loop); - - Ok(match event { - winit::event::Event::Resumed => { - let running = if let Some(running) = &self.running { - running - } else { - let storage = epi_integration::create_storage( - self.native_options - .viewport - .app_id - .as_ref() - .unwrap_or(&self.app_name), - ); - let (window, builder) = create_window( - event_loop, - storage.as_deref(), - &mut self.native_options, - )?; - self.init_run_state(event_loop, storage, window, builder)? - }; - - EventResult::RepaintNow( - running.shared.borrow().viewports[&ViewportId::ROOT] - .window - .as_ref() - .unwrap() - .id(), - ) - } - - winit::event::Event::Suspended => { - #[cfg(target_os = "android")] - self.drop_window()?; - EventResult::Wait - } - - winit::event::Event::WindowEvent { event, window_id } => { - if let Some(running) = &mut self.running { - running.on_window_event(*window_id, event, &mut self.focused_viewport) - } else { - EventResult::Wait - } - } - - #[cfg(feature = "accesskit")] - winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest( - accesskit_winit::ActionRequestEvent { request, window_id }, - )) => { - if let Some(running) = &mut self.running { - let mut shared_lock = running.shared.borrow_mut(); - let SharedState { - viewport_from_window, - viewports, - .. - } = &mut *shared_lock; - if let Some(viewport) = viewport_from_window - .get(window_id) - .and_then(|id| viewports.get_mut(id)) - { - if let Some(egui_winit) = &mut viewport.egui_winit { - egui_winit.on_accesskit_action_request(request.clone()); - } - } - // As a form of user input, accessibility actions should - // lead to a repaint. - EventResult::RepaintNext(*window_id) - } else { - EventResult::Wait - } - } - _ => EventResult::Wait, - }) - } - } - - impl WgpuWinitRunning { - fn save_and_destroy(&mut self) { - crate::profile_function!(); - - let mut shared = self.shared.borrow_mut(); - if let Some(Viewport { window, .. }) = shared.viewports.get(&ViewportId::ROOT) { - self.integration.save(self.app.as_mut(), window.as_deref()); - } - - #[cfg(feature = "glow")] - self.app.on_exit(None); - - #[cfg(not(feature = "glow"))] - self.app.on_exit(); - - shared.painter.destroy(); - } - - /// This is called both for the root viewport, and all deferred viewports - fn run_ui_and_paint( - &mut self, - window_id: WindowId, - focused_viewport: Option, - ) -> EventResult { - let Some(viewport_id) = self - .shared - .borrow() - .viewport_from_window - .get(&window_id) - .copied() - else { - return EventResult::Wait; - }; - - #[cfg(feature = "puffin")] - puffin::GlobalProfiler::lock().new_frame(); - - crate::profile_scope!("frame"); - - let WgpuWinitRunning { - app, - integration, - shared, - } = self; - - let (viewport_ui_cb, raw_input) = { - let mut shared_lock = shared.borrow_mut(); - - let SharedState { - viewports, painter, .. - } = &mut *shared_lock; - - let Some(viewport) = viewports.get(&viewport_id) else { - return EventResult::Wait; - }; - - if viewport_id != ViewportId::ROOT && viewport.viewport_ui_cb.is_none() { - // This will only happen if this is an immediate viewport. - // That means that the viewport cannot be rendered by itself and needs his parent to be rendered. - if let Some(viewport) = viewports.get(&viewport.ids.parent) { - if let Some(window) = viewport.window.as_ref() { - return EventResult::RepaintNext(window.id()); - } - } - return EventResult::Wait; - } - - let Some(viewport) = viewports.get_mut(&viewport_id) else { - return EventResult::Wait; - }; - viewport.update_viewport_info(); - - let Viewport { - ids, - viewport_ui_cb, - window, - egui_winit, - .. - } = viewport; - let viewport_ui_cb = viewport_ui_cb.clone(); - - let Some(window) = window else { - return EventResult::Wait; - }; - - if let Err(err) = pollster::block_on(painter.set_window(viewport_id, Some(window))) - { - log::warn!("Failed to set window: {err}"); - } - - let mut raw_input = egui_winit.as_mut().unwrap().take_egui_input( - window, - ViewportIdPair::from_self_and_parent(viewport_id, ids.parent), - ); - - integration.pre_update(); - - raw_input.time = Some(integration.beginning.elapsed().as_secs_f64()); - raw_input.viewports = viewports - .iter() - .map(|(id, viewport)| (*id, viewport.info.clone())) - .collect(); - - (viewport_ui_cb, raw_input) - }; - - // ------------------------------------------------------------ - - // Runs the update, which could call immediate viewports, - // so make sure we hold no locks here! - let full_output = - integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input); - - // ------------------------------------------------------------ - - let mut shared = shared.borrow_mut(); - - let SharedState { - viewports, - painter, - viewport_from_window, - } = &mut *shared; - - let Some(viewport) = viewports.get_mut(&viewport_id) else { - return EventResult::Wait; - }; - - let Viewport { - window: Some(window), - egui_winit: Some(egui_winit), - .. - } = viewport - else { - return EventResult::Wait; - }; - - integration.post_update(); - - let FullOutput { - platform_output, - textures_delta, - shapes, - pixels_per_point, - viewport_output, - } = full_output; - - integration.handle_platform_output(window, viewport_id, platform_output, egui_winit); - - { - let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point); - - let screenshot_requested = std::mem::take(&mut viewport.screenshot_requested); - let screenshot = painter.paint_and_update_textures( - viewport_id, - pixels_per_point, - app.clear_color(&integration.egui_ctx.style().visuals), - &clipped_primitives, - &textures_delta, - screenshot_requested, - ); - if let Some(screenshot) = screenshot { - egui_winit - .egui_input_mut() - .events - .push(egui::Event::Screenshot { - viewport_id, - image: screenshot.into(), - }); - } - } - - integration.post_rendering(window); - - let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect(); - - handle_viewport_output(viewport_output, viewports, focused_viewport); - - // Prune dead viewports: - viewports.retain(|id, _| active_viewports_ids.contains(id)); - viewport_from_window.retain(|_, id| active_viewports_ids.contains(id)); - painter.gc_viewports(&active_viewports_ids); - - let window = viewport_from_window - .get(&window_id) - .and_then(|id| viewports.get(id)) - .and_then(|vp| vp.window.as_ref()); - - integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref())); - - if let Some(window) = window { - if window.is_minimized() == Some(true) { - // On Mac, a minimized Window uses up all CPU: - // https://github.com/emilk/egui/issues/325 - crate::profile_scope!("minimized_sleep"); - std::thread::sleep(std::time::Duration::from_millis(10)); - } - } - - if integration.should_close() { - EventResult::Exit - } else { - EventResult::Wait - } - } - - fn on_window_event( - &mut self, - window_id: WindowId, - event: &winit::event::WindowEvent<'_>, - focused_viewport: &mut Option, - ) -> EventResult { - let Self { - integration, - app, - shared, - } = self; - let mut shared = shared.borrow_mut(); - - let viewport_id = shared.viewport_from_window.get(&window_id).copied(); - - // On Windows, if a window is resized by the user, it should repaint synchronously, inside the - // event handler. - // - // If this is not done, the compositor will assume that the window does not want to redraw, - // and continue ahead. - // - // In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver - // new frames to the compositor in time. - // - // The flickering is technically glutin or glow's fault, but we should be responding properly - // to resizes anyway, as doing so avoids dropping frames. - // - // See: https://github.com/emilk/egui/issues/903 - let mut repaint_asap = false; - - match event { - winit::event::WindowEvent::Focused(new_focused) => { - *focused_viewport = new_focused.then(|| viewport_id).flatten(); - } - - 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 let Some(viewport_id) = viewport_id { - use std::num::NonZeroU32; - if let (Some(width), Some(height)) = ( - NonZeroU32::new(physical_size.width), - NonZeroU32::new(physical_size.height), - ) { - repaint_asap = true; - shared.painter.on_window_resized(viewport_id, width, height); - } - } - } - - winit::event::WindowEvent::ScaleFactorChanged { new_inner_size, .. } => { - use std::num::NonZeroU32; - if let (Some(width), Some(height), Some(viewport_id)) = ( - NonZeroU32::new(new_inner_size.width), - NonZeroU32::new(new_inner_size.height), - shared.viewport_from_window.get(&window_id).copied(), - ) { - repaint_asap = true; - shared.painter.on_window_resized(viewport_id, width, height); - } - } - - winit::event::WindowEvent::CloseRequested if integration.should_close() => { - log::debug!("Received WindowEvent::CloseRequested"); - return EventResult::Exit; - } - - _ => {} - }; - - let event_response = viewport_id.and_then(|viewport_id| { - shared.viewports.get_mut(&viewport_id).and_then(|viewport| { - viewport.egui_winit.as_mut().map(|egui_winit| { - integration.on_event(app.as_mut(), event, egui_winit, viewport_id) - }) - }) - }); - - if integration.should_close() { - EventResult::Exit - } else if let Some(event_response) = event_response { - if event_response.repaint { - if repaint_asap { - EventResult::RepaintNow(window_id) - } else { - EventResult::RepaintNext(window_id) - } - } else { - EventResult::Wait - } - } else { - EventResult::Wait - } - } - } - - /// Add new viewports, and update existing ones: - fn handle_viewport_output( - viewport_output: ViewportIdMap, - viewports: &mut ViewportIdMap, - focused_viewport: Option, - ) { - for ( - viewport_id, - ViewportOutput { - parent, - class, - builder, - viewport_ui_cb, - commands, - repaint_delay: _, // ignored - we listened to the repaint callback instead - }, - ) in viewport_output - { - let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent); - - let viewport = initialize_or_update_viewport( - viewports, - ids, - class, - builder, - viewport_ui_cb, - focused_viewport, - ); - - if let Some(window) = viewport.window.as_ref() { - let is_viewport_focused = focused_viewport == Some(viewport_id); - egui_winit::process_viewport_commands( - &mut viewport.info, - commands, - window, - is_viewport_focused, - &mut viewport.screenshot_requested, - ); - } - } - } - - fn initialize_or_update_viewport( - viewports: &mut Viewports, - ids: ViewportIdPair, - class: ViewportClass, - mut builder: ViewportBuilder, - viewport_ui_cb: Option>, - focused_viewport: Option, - ) -> &mut Viewport { - if builder.icon.is_none() { - // Inherit icon from parent - builder.icon = viewports - .get_mut(&ids.parent) - .and_then(|vp| vp.builder.icon.clone()); - } - - match viewports.entry(ids.this) { - std::collections::hash_map::Entry::Vacant(entry) => { - // New viewport: - log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title); - entry.insert(Viewport { - ids, - class, - builder, - info: Default::default(), - screenshot_requested: false, - viewport_ui_cb, - window: None, - egui_winit: None, - }) - } - - std::collections::hash_map::Entry::Occupied(mut entry) => { - // Patch an existing viewport: - let viewport = entry.get_mut(); - - viewport.class = class; - viewport.ids.parent = ids.parent; - viewport.viewport_ui_cb = viewport_ui_cb; - - let (delta_commands, recreate) = viewport.builder.patch(&builder); - - if recreate { - log::debug!( - "Recreating window for viewport {:?} ({:?})", - ids.this, - builder.title - ); - viewport.window = None; - viewport.egui_winit = None; - } else if let Some(window) = &viewport.window { - let is_viewport_focused = focused_viewport == Some(ids.this); - process_viewport_commands( - &mut viewport.info, - delta_commands, - window, - is_viewport_focused, - &mut viewport.screenshot_requested, - ); - } - - entry.into_mut() - } - } - } - - pub fn run_wgpu( - app_name: &str, - mut native_options: epi::NativeOptions, - app_creator: epi::AppCreator, - ) -> Result<()> { - #[cfg(not(target_os = "ios"))] - if native_options.run_and_return { - return 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) - }); - } - - let event_loop = create_event_loop(&mut native_options); - let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator); - run_and_exit(event_loop, wgpu_eframe); - } + let event_loop = create_event_loop(&mut native_options); + let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator); + run_and_exit(event_loop, wgpu_eframe); } -#[cfg(feature = "wgpu")] -pub use wgpu_integration::run_wgpu; - // ---------------------------------------------------------------------------- -fn system_theme(window: &Window, options: &NativeOptions) -> Option { - if options.follow_system_theme { - window - .theme() - .map(super::epi_integration::theme_from_winit_theme) - } else { - None - } -} - // For the puffin profiler! #[allow(dead_code)] // Only used for profiling fn short_event_description(event: &winit::event::Event<'_, UserEvent>) -> &'static str { diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs new file mode 100644 index 00000000..825fdcdc --- /dev/null +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -0,0 +1,1012 @@ +use std::{cell::RefCell, rc::Rc, sync::Arc, time::Instant}; + +use parking_lot::Mutex; +use raw_window_handle::{HasRawDisplayHandle as _, HasRawWindowHandle as _}; +use winit::{ + event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}, + window::{Window, WindowId}, +}; + +use egui::{ + ahash::HashMap, DeferredViewportUiCallback, FullOutput, ImmediateViewport, ViewportBuilder, + ViewportClass, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, + ViewportOutput, +}; +#[cfg(feature = "accesskit")] +use egui_winit::accesskit_winit; +use egui_winit::{ + apply_viewport_builder_to_new_window, create_winit_window_builder, process_viewport_commands, +}; + +use crate::{ + native::{epi_integration::EpiIntegration, winit_integration::EventResult}, + App, AppCreator, CreationContext, NativeOptions, Result, Storage, UserEvent, +}; + +use super::{winit_integration::WinitApp, *}; + +// ---------------------------------------------------------------------------- +// Types: + +pub struct WgpuWinitApp { + repaint_proxy: Arc>>, + app_name: String, + native_options: NativeOptions, + + /// Set at initialization, then taken and set to `None` in `init_run_state`. + app_creator: Option, + + /// Set when we are actually up and running. + running: Option, +} + +/// 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 { + integration: EpiIntegration, + + /// The users application. + app: Box, + + /// Wrapped in an `Rc>` so it can be re-entrantly shared via a weak-pointer. + shared: Rc>, +} + +/// Everything needed by the immediate viewport renderer. +/// +/// Wrapped in an `Rc>` so it can be re-entrantly shared via a weak-pointer. +pub struct SharedState { + viewports: Viewports, + painter: egui_wgpu::winit::Painter, + viewport_from_window: HashMap, + focused_viewport: Option, +} + +pub type Viewports = ViewportIdMap; + +pub struct Viewport { + ids: ViewportIdPair, + class: ViewportClass, + builder: ViewportBuilder, + info: ViewportInfo, + screenshot_requested: bool, + + /// `None` for sync viewports. + viewport_ui_cb: 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>, + + /// `window` and `egui_winit` are initialized together. + egui_winit: Option, +} + +// ---------------------------------------------------------------------------- + +impl WgpuWinitApp { + pub fn new( + event_loop: &EventLoop, + app_name: &str, + native_options: NativeOptions, + app_creator: AppCreator, + ) -> Self { + crate::profile_function!(); + + #[cfg(feature = "__screenshot")] + assert!( + std::env::var("EFRAME_SCREENSHOT_TO").is_err(), + "EFRAME_SCREENSHOT_TO not yet implemented for wgpu backend" + ); + + Self { + repaint_proxy: Arc::new(Mutex::new(event_loop.create_proxy())), + app_name: app_name.to_owned(), + native_options, + running: None, + app_creator: Some(app_creator), + } + } + + fn build_windows(&mut self, event_loop: &EventLoopWindowTarget) { + let Some(running) = &mut self.running else { + return; + }; + let mut shared = running.shared.borrow_mut(); + let SharedState { + viewports, + painter, + viewport_from_window, + .. + } = &mut *shared; + + for viewport in viewports.values_mut() { + if viewport.window.is_none() { + viewport.init_window(viewport_from_window, painter, event_loop); + } + } + } + + #[cfg(target_os = "android")] + fn drop_window(&mut self) -> Result<(), egui_wgpu::WgpuError> { + if let Some(running) = &mut self.running { + let mut shared = running.shared.borrow_mut(); + shared.viewports.remove(&ViewportId::ROOT); + pollster::block_on(shared.painter.set_window(ViewportId::ROOT, None))?; + } + Ok(()) + } + + fn init_run_state( + &mut self, + event_loop: &EventLoopWindowTarget, + storage: Option>, + window: Window, + builder: ViewportBuilder, + ) -> Result<&mut WgpuWinitRunning, egui_wgpu::WgpuError> { + crate::profile_function!(); + + #[allow(unsafe_code, unused_mut, unused_unsafe)] + let mut painter = egui_wgpu::winit::Painter::new( + self.native_options.wgpu_options.clone(), + self.native_options.multisampling.max(1) as _, + egui_wgpu::depth_format_from_bits( + self.native_options.depth_buffer, + self.native_options.stencil_buffer, + ), + self.native_options.viewport.transparent.unwrap_or(false), + ); + pollster::block_on(painter.set_window(ViewportId::ROOT, Some(&window)))?; + + let wgpu_render_state = painter.render_state(); + + let system_theme = winit_integration::system_theme(&window, &self.native_options); + let mut integration = EpiIntegration::new( + &window, + system_theme, + &self.app_name, + &self.native_options, + storage, + winit_integration::IS_DESKTOP, + #[cfg(feature = "glow")] + None, + wgpu_render_state.clone(), + ); + + { + let event_loop_proxy = self.repaint_proxy.clone(); + + integration + .egui_ctx + .set_request_repaint_callback(move |info| { + log::trace!("request_repaint_callback: {info:?}"); + let when = Instant::now() + info.delay; + let frame_nr = info.current_frame_nr; + + event_loop_proxy + .lock() + .send_event(UserEvent::RequestRepaint { + when, + frame_nr, + viewport_id: info.viewport_id, + }) + .ok(); + }); + } + + let mut egui_winit = egui_winit::State::new( + event_loop, + Some(window.scale_factor() as f32), + painter.max_texture_side(), + ); + + #[cfg(feature = "accesskit")] + { + let event_loop_proxy = self.repaint_proxy.lock().clone(); + integration.init_accesskit(&mut egui_winit, &window, event_loop_proxy); + } + let theme = system_theme.unwrap_or(self.native_options.default_theme); + integration.egui_ctx.set_visuals(theme.egui_visuals()); + + let app_creator = std::mem::take(&mut self.app_creator) + .expect("Single-use AppCreator has unexpectedly already been taken"); + let cc = CreationContext { + egui_ctx: integration.egui_ctx.clone(), + integration_info: integration.frame.info().clone(), + storage: integration.frame.storage(), + #[cfg(feature = "glow")] + gl: None, + wgpu_render_state, + raw_display_handle: window.raw_display_handle(), + raw_window_handle: window.raw_window_handle(), + }; + let app = { + crate::profile_scope!("user_app_creator"); + app_creator(&cc) + }; + + let mut viewport_from_window = HashMap::default(); + viewport_from_window.insert(window.id(), ViewportId::ROOT); + + let mut viewports = Viewports::default(); + viewports.insert( + ViewportId::ROOT, + Viewport { + ids: ViewportIdPair::ROOT, + class: ViewportClass::Root, + builder, + info: ViewportInfo { + minimized: window.is_minimized(), + maximized: Some(window.is_maximized()), + ..Default::default() + }, + screenshot_requested: false, + viewport_ui_cb: None, + window: Some(Rc::new(window)), + egui_winit: Some(egui_winit), + }, + ); + + let shared = Rc::new(RefCell::new(SharedState { + viewport_from_window, + viewports, + painter, + focused_viewport: Some(ViewportId::ROOT), + })); + + { + // Create a weak pointer so that we don't keep state alive for too long. + let shared = Rc::downgrade(&shared); + let beginning = integration.beginning; + + let event_loop: *const EventLoopWindowTarget = event_loop; + + egui::Context::set_immediate_viewport_renderer(move |egui_ctx, immediate_viewport| { + if let Some(shared) = shared.upgrade() { + // SAFETY: the event loop lives longer than + // the Rc:s we just upgraded above. + #[allow(unsafe_code)] + let event_loop = unsafe { event_loop.as_ref().unwrap() }; + + render_immediate_viewport( + event_loop, + egui_ctx, + beginning, + &shared, + immediate_viewport, + ); + } else { + log::warn!("render_sync_callback called after window closed"); + } + }); + } + + Ok(self.running.insert(WgpuWinitRunning { + integration, + app, + shared, + })) + } +} + +impl WinitApp for WgpuWinitApp { + fn frame_nr(&self, viewport_id: ViewportId) -> u64 { + self.running + .as_ref() + .map_or(0, |r| r.integration.egui_ctx.frame_nr_for(viewport_id)) + } + + fn is_focused(&self, window_id: WindowId) -> bool { + if let Some(running) = &self.running { + let shared = running.shared.borrow(); + let viewport_id = shared.viewport_from_window.get(&window_id).copied(); + shared.focused_viewport.is_some() && shared.focused_viewport == viewport_id + } else { + false + } + } + + fn integration(&self) -> Option<&EpiIntegration> { + self.running.as_ref().map(|r| &r.integration) + } + + fn window(&self, window_id: WindowId) -> Option> { + self.running + .as_ref() + .and_then(|r| { + let shared = r.shared.borrow(); + shared + .viewport_from_window + .get(&window_id) + .and_then(|id| shared.viewports.get(id).map(|v| v.window.clone())) + }) + .flatten() + } + + fn window_id_from_viewport_id(&self, id: ViewportId) -> Option { + Some( + self.running + .as_ref()? + .shared + .borrow() + .viewports + .get(&id)? + .window + .as_ref()? + .id(), + ) + } + + fn save_and_destroy(&mut self) { + if let Some(mut running) = self.running.take() { + running.save_and_destroy(); + } + } + + fn run_ui_and_paint(&mut self, window_id: WindowId) -> EventResult { + if let Some(running) = &mut self.running { + running.run_ui_and_paint(window_id) + } else { + EventResult::Wait + } + } + + fn on_event( + &mut self, + event_loop: &EventLoopWindowTarget, + event: &winit::event::Event<'_, UserEvent>, + ) -> Result { + crate::profile_function!(); + + self.build_windows(event_loop); + + Ok(match event { + winit::event::Event::Resumed => { + let running = if let Some(running) = &self.running { + running + } else { + let storage = epi_integration::create_storage( + self.native_options + .viewport + .app_id + .as_ref() + .unwrap_or(&self.app_name), + ); + let (window, builder) = + create_window(event_loop, storage.as_deref(), &mut self.native_options)?; + self.init_run_state(event_loop, storage, window, builder)? + }; + + EventResult::RepaintNow( + running.shared.borrow().viewports[&ViewportId::ROOT] + .window + .as_ref() + .unwrap() + .id(), + ) + } + + winit::event::Event::Suspended => { + #[cfg(target_os = "android")] + self.drop_window()?; + EventResult::Wait + } + + winit::event::Event::WindowEvent { event, window_id } => { + if let Some(running) = &mut self.running { + running.on_window_event(*window_id, event) + } else { + EventResult::Wait + } + } + + #[cfg(feature = "accesskit")] + winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest( + accesskit_winit::ActionRequestEvent { request, window_id }, + )) => { + if let Some(running) = &mut self.running { + let mut shared_lock = running.shared.borrow_mut(); + let SharedState { + viewport_from_window, + viewports, + .. + } = &mut *shared_lock; + if let Some(viewport) = viewport_from_window + .get(window_id) + .and_then(|id| viewports.get_mut(id)) + { + if let Some(egui_winit) = &mut viewport.egui_winit { + egui_winit.on_accesskit_action_request(request.clone()); + } + } + // As a form of user input, accessibility actions should + // lead to a repaint. + EventResult::RepaintNext(*window_id) + } else { + EventResult::Wait + } + } + _ => EventResult::Wait, + }) + } +} + +impl WgpuWinitRunning { + fn save_and_destroy(&mut self) { + crate::profile_function!(); + + let mut shared = self.shared.borrow_mut(); + if let Some(Viewport { window, .. }) = shared.viewports.get(&ViewportId::ROOT) { + self.integration.save(self.app.as_mut(), window.as_deref()); + } + + #[cfg(feature = "glow")] + self.app.on_exit(None); + + #[cfg(not(feature = "glow"))] + self.app.on_exit(); + + shared.painter.destroy(); + } + + /// This is called both for the root viewport, and all deferred viewports + fn run_ui_and_paint(&mut self, window_id: WindowId) -> EventResult { + let Some(viewport_id) = self + .shared + .borrow() + .viewport_from_window + .get(&window_id) + .copied() + else { + return EventResult::Wait; + }; + + #[cfg(feature = "puffin")] + puffin::GlobalProfiler::lock().new_frame(); + + crate::profile_scope!("frame"); + + let WgpuWinitRunning { + app, + integration, + shared, + } = self; + + let (viewport_ui_cb, raw_input) = { + let mut shared_lock = shared.borrow_mut(); + + let SharedState { + viewports, painter, .. + } = &mut *shared_lock; + + let Some(viewport) = viewports.get(&viewport_id) else { + return EventResult::Wait; + }; + + if viewport_id != ViewportId::ROOT && viewport.viewport_ui_cb.is_none() { + // This will only happen if this is an immediate viewport. + // That means that the viewport cannot be rendered by itself and needs his parent to be rendered. + if let Some(viewport) = viewports.get(&viewport.ids.parent) { + if let Some(window) = viewport.window.as_ref() { + return EventResult::RepaintNext(window.id()); + } + } + return EventResult::Wait; + } + + let Some(viewport) = viewports.get_mut(&viewport_id) else { + return EventResult::Wait; + }; + viewport.update_viewport_info(); + + let Viewport { + ids, + viewport_ui_cb, + window, + egui_winit, + .. + } = viewport; + let viewport_ui_cb = viewport_ui_cb.clone(); + + let Some(window) = window else { + return EventResult::Wait; + }; + + if let Err(err) = pollster::block_on(painter.set_window(viewport_id, Some(window))) { + log::warn!("Failed to set window: {err}"); + } + + let mut raw_input = egui_winit.as_mut().unwrap().take_egui_input( + window, + ViewportIdPair::from_self_and_parent(viewport_id, ids.parent), + ); + + integration.pre_update(); + + raw_input.time = Some(integration.beginning.elapsed().as_secs_f64()); + raw_input.viewports = viewports + .iter() + .map(|(id, viewport)| (*id, viewport.info.clone())) + .collect(); + + (viewport_ui_cb, raw_input) + }; + + // ------------------------------------------------------------ + + // Runs the update, which could call immediate viewports, + // so make sure we hold no locks here! + let full_output = integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input); + + // ------------------------------------------------------------ + + let mut shared = shared.borrow_mut(); + + let SharedState { + viewports, + painter, + viewport_from_window, + focused_viewport, + } = &mut *shared; + + let Some(viewport) = viewports.get_mut(&viewport_id) else { + return EventResult::Wait; + }; + + let Viewport { + window: Some(window), + egui_winit: Some(egui_winit), + .. + } = viewport + else { + return EventResult::Wait; + }; + + integration.post_update(); + + let FullOutput { + platform_output, + textures_delta, + shapes, + pixels_per_point, + viewport_output, + } = full_output; + + integration.handle_platform_output(window, viewport_id, platform_output, egui_winit); + + { + let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point); + + let screenshot_requested = std::mem::take(&mut viewport.screenshot_requested); + let screenshot = painter.paint_and_update_textures( + viewport_id, + pixels_per_point, + app.clear_color(&integration.egui_ctx.style().visuals), + &clipped_primitives, + &textures_delta, + screenshot_requested, + ); + if let Some(screenshot) = screenshot { + egui_winit + .egui_input_mut() + .events + .push(egui::Event::Screenshot { + viewport_id, + image: screenshot.into(), + }); + } + } + + integration.post_rendering(window); + + let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect(); + + handle_viewport_output(viewport_output, viewports, *focused_viewport); + + // Prune dead viewports: + viewports.retain(|id, _| active_viewports_ids.contains(id)); + viewport_from_window.retain(|_, id| active_viewports_ids.contains(id)); + painter.gc_viewports(&active_viewports_ids); + + let window = viewport_from_window + .get(&window_id) + .and_then(|id| viewports.get(id)) + .and_then(|vp| vp.window.as_ref()); + + integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref())); + + if let Some(window) = window { + if window.is_minimized() == Some(true) { + // On Mac, a minimized Window uses up all CPU: + // https://github.com/emilk/egui/issues/325 + crate::profile_scope!("minimized_sleep"); + std::thread::sleep(std::time::Duration::from_millis(10)); + } + } + + if integration.should_close() { + EventResult::Exit + } else { + EventResult::Wait + } + } + + fn on_window_event( + &mut self, + window_id: WindowId, + event: &winit::event::WindowEvent<'_>, + ) -> EventResult { + let Self { + integration, + app, + shared, + } = self; + let mut shared = shared.borrow_mut(); + + let viewport_id = shared.viewport_from_window.get(&window_id).copied(); + + // On Windows, if a window is resized by the user, it should repaint synchronously, inside the + // event handler. + // + // If this is not done, the compositor will assume that the window does not want to redraw, + // and continue ahead. + // + // In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver + // new frames to the compositor in time. + // + // The flickering is technically glutin or glow's fault, but we should be responding properly + // to resizes anyway, as doing so avoids dropping frames. + // + // See: https://github.com/emilk/egui/issues/903 + let mut repaint_asap = false; + + match event { + winit::event::WindowEvent::Focused(new_focused) => { + shared.focused_viewport = new_focused.then(|| viewport_id).flatten(); + } + + 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 let Some(viewport_id) = viewport_id { + use std::num::NonZeroU32; + if let (Some(width), Some(height)) = ( + NonZeroU32::new(physical_size.width), + NonZeroU32::new(physical_size.height), + ) { + repaint_asap = true; + shared.painter.on_window_resized(viewport_id, width, height); + } + } + } + + winit::event::WindowEvent::ScaleFactorChanged { new_inner_size, .. } => { + use std::num::NonZeroU32; + if let (Some(width), Some(height), Some(viewport_id)) = ( + NonZeroU32::new(new_inner_size.width), + NonZeroU32::new(new_inner_size.height), + shared.viewport_from_window.get(&window_id).copied(), + ) { + repaint_asap = true; + shared.painter.on_window_resized(viewport_id, width, height); + } + } + + winit::event::WindowEvent::CloseRequested if integration.should_close() => { + log::debug!("Received WindowEvent::CloseRequested"); + return EventResult::Exit; + } + + _ => {} + }; + + let event_response = viewport_id.and_then(|viewport_id| { + shared.viewports.get_mut(&viewport_id).and_then(|viewport| { + viewport.egui_winit.as_mut().map(|egui_winit| { + integration.on_event(app.as_mut(), event, egui_winit, viewport_id) + }) + }) + }); + + if integration.should_close() { + EventResult::Exit + } else if let Some(event_response) = event_response { + if event_response.repaint { + if repaint_asap { + EventResult::RepaintNow(window_id) + } else { + EventResult::RepaintNext(window_id) + } + } else { + EventResult::Wait + } + } else { + EventResult::Wait + } + } +} + +impl Viewport { + fn init_window( + &mut self, + windows_id: &mut HashMap, + painter: &mut egui_wgpu::winit::Painter, + event_loop: &EventLoopWindowTarget, + ) { + crate::profile_function!(); + + let viewport_id = self.ids.this; + + match create_winit_window_builder(self.builder.clone()).build(event_loop) { + Ok(window) => { + apply_viewport_builder_to_new_window(&window, &self.builder); + + windows_id.insert(window.id(), viewport_id); + + if let Err(err) = pollster::block_on(painter.set_window(viewport_id, Some(&window))) + { + log::error!("on set_window: viewport_id {viewport_id:?} {err}"); + } + + self.egui_winit = Some(egui_winit::State::new( + event_loop, + Some(window.scale_factor() as f32), + painter.max_texture_side(), + )); + + self.info.minimized = window.is_minimized(); + self.info.maximized = Some(window.is_maximized()); + + self.window = Some(Rc::new(window)); + } + Err(err) => { + log::error!("Failed to create window: {err}"); + } + } + } + + /// Update the stored `ViewportInfo`. + pub fn update_viewport_info(&mut self) { + let Some(window) = &self.window else { + return; + }; + let Some(egui_winit) = &self.egui_winit else { + return; + }; + egui_winit.update_viewport_info(&mut self.info, window); + } +} + +fn create_window( + event_loop: &EventLoopWindowTarget, + storage: Option<&dyn Storage>, + native_options: &mut NativeOptions, +) -> Result<(Window, ViewportBuilder), winit::error::OsError> { + crate::profile_function!(); + + let window_settings = epi_integration::load_window_settings(storage); + let viewport_builder = + epi_integration::viewport_builder(event_loop, native_options, window_settings); + let window = { + crate::profile_scope!("WindowBuilder::build"); + create_winit_window_builder(viewport_builder.clone()).build(event_loop)? + }; + apply_viewport_builder_to_new_window(&window, &viewport_builder); + epi_integration::apply_window_settings(&window, window_settings); + Ok((window, viewport_builder)) +} + +fn render_immediate_viewport( + event_loop: &EventLoopWindowTarget, + egui_ctx: &egui::Context, + beginning: Instant, + shared: &RefCell, + immediate_viewport: ImmediateViewport<'_>, +) { + crate::profile_function!(); + + let ImmediateViewport { + ids, + builder, + viewport_ui_cb, + } = immediate_viewport; + + let input = { + let SharedState { + viewports, + painter, + viewport_from_window, + .. + } = &mut *shared.borrow_mut(); + + let viewport = initialize_or_update_viewport( + viewports, + ids, + ViewportClass::Immediate, + builder, + None, + None, + ); + if viewport.window.is_none() { + viewport.init_window(viewport_from_window, painter, event_loop); + } + viewport.update_viewport_info(); + + let (Some(window), Some(winit_state)) = (&viewport.window, &mut viewport.egui_winit) + else { + return; + }; + + let mut input = winit_state.take_egui_input(window, ids); + input.viewports = viewports + .iter() + .map(|(id, viewport)| (*id, viewport.info.clone())) + .collect(); + input.time = Some(beginning.elapsed().as_secs_f64()); + input + }; + + // ------------------------------------------ + + // Run the user code, which could re-entrantly call this function again (!). + // Make sure no locks are held during this call. + let egui::FullOutput { + platform_output, + textures_delta, + shapes, + pixels_per_point, + viewport_output, + } = egui_ctx.run(input, |ctx| { + viewport_ui_cb(ctx); + }); + + // ------------------------------------------ + + let mut shared = shared.borrow_mut(); + let SharedState { + viewports, + painter, + focused_viewport, + .. + } = &mut *shared; + + let Some(viewport) = viewports.get_mut(&ids.this) else { + return; + }; + let Some(winit_state) = &mut viewport.egui_winit else { + return; + }; + let Some(window) = &viewport.window else { + return; + }; + + if let Err(err) = pollster::block_on(painter.set_window(ids.this, Some(window))) { + log::error!( + "when rendering viewport_id={:?}, set_window Error {err}", + ids.this + ); + } + + let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); + painter.paint_and_update_textures( + ids.this, + pixels_per_point, + [0.0, 0.0, 0.0, 0.0], + &clipped_primitives, + &textures_delta, + false, + ); + + winit_state.handle_platform_output(window, ids.this, egui_ctx, platform_output); + + handle_viewport_output(viewport_output, viewports, *focused_viewport); +} + +/// Add new viewports, and update existing ones: +fn handle_viewport_output( + viewport_output: ViewportIdMap, + viewports: &mut ViewportIdMap, + focused_viewport: Option, +) { + for ( + viewport_id, + ViewportOutput { + parent, + class, + builder, + viewport_ui_cb, + commands, + repaint_delay: _, // ignored - we listened to the repaint callback instead + }, + ) in viewport_output + { + let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent); + + let viewport = initialize_or_update_viewport( + viewports, + ids, + class, + builder, + viewport_ui_cb, + focused_viewport, + ); + + if let Some(window) = viewport.window.as_ref() { + let is_viewport_focused = focused_viewport == Some(viewport_id); + egui_winit::process_viewport_commands( + &mut viewport.info, + commands, + window, + is_viewport_focused, + &mut viewport.screenshot_requested, + ); + } + } +} + +fn initialize_or_update_viewport( + viewports: &mut Viewports, + ids: ViewportIdPair, + class: ViewportClass, + mut builder: ViewportBuilder, + viewport_ui_cb: Option>, + focused_viewport: Option, +) -> &mut Viewport { + if builder.icon.is_none() { + // Inherit icon from parent + builder.icon = viewports + .get_mut(&ids.parent) + .and_then(|vp| vp.builder.icon.clone()); + } + + match viewports.entry(ids.this) { + std::collections::hash_map::Entry::Vacant(entry) => { + // New viewport: + log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title); + entry.insert(Viewport { + ids, + class, + builder, + info: Default::default(), + screenshot_requested: false, + viewport_ui_cb, + window: None, + egui_winit: None, + }) + } + + std::collections::hash_map::Entry::Occupied(mut entry) => { + // Patch an existing viewport: + let viewport = entry.get_mut(); + + viewport.class = class; + viewport.ids.parent = ids.parent; + viewport.viewport_ui_cb = viewport_ui_cb; + + let (delta_commands, recreate) = viewport.builder.patch(&builder); + + if recreate { + log::debug!( + "Recreating window for viewport {:?} ({:?})", + ids.this, + builder.title + ); + viewport.window = None; + viewport.egui_winit = None; + } else if let Some(window) = &viewport.window { + let is_viewport_focused = focused_viewport == Some(ids.this); + process_viewport_commands( + &mut viewport.info, + delta_commands, + window, + is_viewport_focused, + &mut viewport.screenshot_requested, + ); + } + + entry.into_mut() + } + } +} diff --git a/crates/eframe/src/native/winit_integration.rs b/crates/eframe/src/native/winit_integration.rs new file mode 100644 index 00000000..96a98f70 --- /dev/null +++ b/crates/eframe/src/native/winit_integration.rs @@ -0,0 +1,102 @@ +use std::{rc::Rc, time::Instant}; + +use winit::{ + event_loop::EventLoopWindowTarget, + window::{Window, WindowId}, +}; + +use egui::ViewportId; +#[cfg(feature = "accesskit")] +use egui_winit::accesskit_winit; + +use super::epi_integration::EpiIntegration; + +pub const IS_DESKTOP: bool = cfg!(any( + target_os = "freebsd", + target_os = "linux", + target_os = "macos", + target_os = "openbsd", + target_os = "windows", +)); + +/// The custom even `eframe` uses with the [`winit`] event loop. +#[derive(Debug)] +pub enum UserEvent { + /// A repaint is requested. + RequestRepaint { + /// What to repaint. + viewport_id: ViewportId, + + /// When to repaint. + when: Instant, + + /// What the frame number was when the repaint was _requested_. + frame_nr: u64, + }, + + /// A request related to [`accesskit`](https://accesskit.dev/). + #[cfg(feature = "accesskit")] + AccessKitActionRequest(accesskit_winit::ActionRequestEvent), +} + +#[cfg(feature = "accesskit")] +impl From for UserEvent { + fn from(inner: accesskit_winit::ActionRequestEvent) -> Self { + Self::AccessKitActionRequest(inner) + } +} + +pub trait WinitApp { + /// The current frame number, as reported by egui. + fn frame_nr(&self, viewport_id: ViewportId) -> u64; + + fn is_focused(&self, window_id: WindowId) -> bool; + + fn integration(&self) -> Option<&EpiIntegration>; + + fn window(&self, window_id: WindowId) -> Option>; + + fn window_id_from_viewport_id(&self, id: ViewportId) -> Option; + + fn save_and_destroy(&mut self); + + fn run_ui_and_paint(&mut self, window_id: WindowId) -> EventResult; + + fn on_event( + &mut self, + event_loop: &EventLoopWindowTarget, + event: &winit::event::Event<'_, UserEvent>, + ) -> crate::Result; +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum EventResult { + Wait, + + /// Causes a synchronous repaint inside the event handler. This should only + /// be used in special situations if the window must be repainted while + /// handling a specific event. This occurs on Windows when handling resizes. + /// + /// `RepaintNow` creates a new frame synchronously, and should therefore + /// only be used for extremely urgent repaints. + RepaintNow(WindowId), + + /// Queues a repaint for once the event loop handles its next redraw. Exists + /// so that multiple input events can be handled in one frame. Does not + /// cause any delay like `RepaintNow`. + RepaintNext(WindowId), + + RepaintAt(WindowId, Instant), + + Exit, +} + +pub fn system_theme(window: &Window, options: &crate::NativeOptions) -> Option { + if options.follow_system_theme { + window + .theme() + .map(super::epi_integration::theme_from_winit_theme) + } else { + None + } +}