use std::time::Instant; use winit::event_loop::EventLoopWindowTarget; use raw_window_handle::{HasRawDisplayHandle as _, HasRawWindowHandle as _}; use egui::{ DeferredViewportUiCallback, NumExt as _, ViewportBuilder, ViewportId, ViewportIdPair, ViewportInfo, }; use egui_winit::{EventResponse, WindowSettings}; use crate::{epi, Theme}; pub fn window_builder( event_loop: &EventLoopWindowTarget, title: &str, native_options: &mut epi::NativeOptions, window_settings: Option, ) -> ViewportBuilder { let epi::NativeOptions { maximized, decorated, fullscreen, #[cfg(target_os = "macos")] fullsize_content, drag_and_drop_support, icon_data, initial_window_pos, initial_window_size, min_window_size, max_window_size, resizable, transparent, centered, active, .. } = native_options; let mut viewport_builder = egui::ViewportBuilder::default() .with_title(title) .with_decorations(*decorated) .with_fullscreen(*fullscreen) .with_maximized(*maximized) .with_resizable(*resizable) .with_transparent(*transparent) .with_active(*active) // Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279 // We must also keep the window hidden until AccessKit is initialized. .with_visible(false); if let Some(icon_data) = icon_data { viewport_builder = viewport_builder.with_window_icon(egui::ColorImage::from_rgba_premultiplied( [icon_data.width as usize, icon_data.height as usize], &icon_data.rgba, )); } #[cfg(target_os = "macos")] if *fullsize_content { viewport_builder = viewport_builder .with_title_hidden(true) .with_titlebar_transparent(true) .with_fullsize_content_view(true); } #[cfg(all(feature = "wayland", target_os = "linux"))] { viewport_builder = match &native_options.app_id { Some(app_id) => viewport_builder.with_name(app_id, ""), None => viewport_builder.with_name(title, ""), }; } if let Some(min_size) = *min_window_size { viewport_builder = viewport_builder.with_min_inner_size(min_size); } if let Some(max_size) = *max_window_size { viewport_builder = viewport_builder.with_max_inner_size(max_size); } viewport_builder = viewport_builder.with_drag_and_drop(*drag_and_drop_support); // Always use the default window size / position on iOS. Trying to restore the previous position // causes the window to be shown too small. #[cfg(not(target_os = "ios"))] let inner_size_points = if let Some(mut window_settings) = window_settings { // Restore pos/size from previous session window_settings.clamp_size_to_sane_values(largest_monitor_point_size(event_loop)); window_settings.clamp_position_to_monitors(event_loop); viewport_builder = window_settings.initialize_viewport_builder(viewport_builder); window_settings.inner_size_points() } else { if let Some(pos) = *initial_window_pos { viewport_builder = viewport_builder.with_position(pos); } if let Some(initial_window_size) = *initial_window_size { let initial_window_size = initial_window_size.at_most(largest_monitor_point_size(event_loop)); viewport_builder = viewport_builder.with_inner_size(initial_window_size); } *initial_window_size }; #[cfg(not(target_os = "ios"))] if *centered { if let Some(monitor) = event_loop.available_monitors().next() { let monitor_size = monitor.size().to_logical::(monitor.scale_factor()); let inner_size = inner_size_points.unwrap_or(egui::Vec2 { x: 800.0, y: 600.0 }); if monitor_size.width > 0.0 && monitor_size.height > 0.0 { let x = (monitor_size.width - inner_size.x) / 2.0; let y = (monitor_size.height - inner_size.y) / 2.0; viewport_builder = viewport_builder.with_position([x, y]); } } } match std::mem::take(&mut native_options.window_builder) { Some(hook) => hook(viewport_builder), None => viewport_builder, } } pub fn apply_native_options_to_window( window: &winit::window::Window, native_options: &crate::NativeOptions, window_settings: Option, ) { crate::profile_function!(); use winit::window::WindowLevel; window.set_window_level(if native_options.always_on_top { WindowLevel::AlwaysOnTop } else { WindowLevel::Normal }); if let Some(window_settings) = window_settings { window_settings.initialize_window(window); } } fn largest_monitor_point_size(event_loop: &EventLoopWindowTarget) -> egui::Vec2 { let mut max_size = egui::Vec2::ZERO; for monitor in event_loop.available_monitors() { let size = monitor.size().to_logical::(monitor.scale_factor()); let size = egui::vec2(size.width, size.height); max_size = max_size.max(size); } if max_size == egui::Vec2::ZERO { egui::Vec2::splat(16000.0) } else { max_size } } // ---------------------------------------------------------------------------- /// For loading/saving app state and/or egui memory to disk. pub fn create_storage(_app_name: &str) -> Option> { #[cfg(feature = "persistence")] if let Some(storage) = super::file_storage::FileStorage::from_app_id(_app_name) { return Some(Box::new(storage)); } None } // ---------------------------------------------------------------------------- /// Everything needed to make a winit-based integration for [`epi`]. /// /// Only one instance per app (not one per viewport). pub struct EpiIntegration { pub frame: epi::Frame, last_auto_save: Instant, pub beginning: Instant, is_first_frame: bool, pub frame_start: Instant, pub egui_ctx: egui::Context, pending_full_output: egui::FullOutput, /// When set, it is time to close the native window. close: bool, can_drag_window: bool, follow_system_theme: bool, #[cfg(feature = "persistence")] persist_window: bool, app_icon_setter: super::app_icon::AppTitleIconSetter, } impl EpiIntegration { #[allow(clippy::too_many_arguments)] pub fn new( window: &winit::window::Window, system_theme: Option, app_name: &str, native_options: &crate::NativeOptions, storage: Option>, is_desktop: bool, #[cfg(feature = "glow")] gl: Option>, #[cfg(feature = "wgpu")] wgpu_render_state: Option, ) -> Self { let egui_ctx = egui::Context::default(); egui_ctx.set_embed_viewports(!is_desktop); let memory = load_egui_memory(storage.as_deref()).unwrap_or_default(); egui_ctx.memory_mut(|mem| *mem = memory); let frame = epi::Frame { info: epi::IntegrationInfo { system_theme, cpu_usage: None, }, storage, #[cfg(feature = "glow")] gl, #[cfg(feature = "wgpu")] wgpu_render_state, raw_display_handle: window.raw_display_handle(), raw_window_handle: window.raw_window_handle(), }; let app_icon_setter = super::app_icon::AppTitleIconSetter::new( app_name.to_owned(), native_options.icon_data.clone(), ); Self { frame, last_auto_save: Instant::now(), egui_ctx, pending_full_output: Default::default(), close: false, can_drag_window: false, follow_system_theme: native_options.follow_system_theme, #[cfg(feature = "persistence")] persist_window: native_options.persist_window, app_icon_setter, beginning: Instant::now(), is_first_frame: true, frame_start: Instant::now(), } } #[cfg(feature = "accesskit")] pub fn init_accesskit + Send>( &mut self, egui_winit: &mut egui_winit::State, window: &winit::window::Window, event_loop_proxy: winit::event_loop::EventLoopProxy, ) { crate::profile_function!(); let egui_ctx = self.egui_ctx.clone(); egui_winit.init_accesskit(window, event_loop_proxy, move || { // This function is called when an accessibility client // (e.g. screen reader) makes its first request. If we got here, // we know that an accessibility tree is actually wanted. egui_ctx.enable_accesskit(); // Enqueue a repaint so we'll receive a full tree update soon. egui_ctx.request_repaint(); egui_ctx.accesskit_placeholder_tree_update() }); } pub fn warm_up( &mut self, app: &mut dyn epi::App, window: &winit::window::Window, egui_winit: &mut egui_winit::State, ) { crate::profile_function!(); let saved_memory: egui::Memory = self.egui_ctx.memory(|mem| mem.clone()); self.egui_ctx .memory_mut(|mem| mem.set_everything_is_visible(true)); let mut raw_input = egui_winit.take_egui_input(window, ViewportIdPair::ROOT); raw_input.viewports = std::iter::once((ViewportId::ROOT, ViewportInfo::default())).collect(); self.pre_update(); let full_output = self.update(app, None, raw_input); self.post_update(); self.pending_full_output.append(full_output); // Handle it next frame self.egui_ctx.memory_mut(|mem| *mem = saved_memory); // We don't want to remember that windows were huge. self.egui_ctx.clear_animations(); } /// If `true`, it is time to close the native window. pub fn should_close(&self) -> bool { self.close } pub fn on_event( &mut self, app: &mut dyn epi::App, event: &winit::event::WindowEvent<'_>, egui_winit: &mut egui_winit::State, viewport_id: ViewportId, ) -> EventResponse { crate::profile_function!(); use winit::event::{ElementState, MouseButton, WindowEvent}; match event { WindowEvent::CloseRequested => { log::debug!("Received WindowEvent::CloseRequested"); self.close = app.on_close_event() && viewport_id == ViewportId::ROOT; log::debug!("App::on_close_event returned {}", self.close); } WindowEvent::Destroyed => { log::debug!("Received WindowEvent::Destroyed"); self.close = true; } WindowEvent::MouseInput { button: MouseButton::Left, state: ElementState::Pressed, .. } => self.can_drag_window = true, WindowEvent::ScaleFactorChanged { scale_factor, .. } => { egui_winit.egui_input_mut().native_pixels_per_point = Some(*scale_factor as _); } WindowEvent::ThemeChanged(winit_theme) if self.follow_system_theme => { let theme = theme_from_winit_theme(*winit_theme); self.frame.info.system_theme = Some(theme); self.egui_ctx.set_visuals(theme.egui_visuals()); } _ => {} } egui_winit.on_event(&self.egui_ctx, event, viewport_id) } pub fn pre_update(&mut self) { self.frame_start = Instant::now(); self.app_icon_setter.update(); } /// Run user code - this can create immediate viewports, so hold no locks over this! /// /// If `viewport_ui_cb` is None, we are in the root viewport and will call [`crate::App::update`]. pub fn update( &mut self, app: &mut dyn epi::App, viewport_ui_cb: Option<&DeferredViewportUiCallback>, mut raw_input: egui::RawInput, ) -> egui::FullOutput { raw_input.time = Some(self.beginning.elapsed().as_secs_f64()); let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { if let Some(viewport_ui_cb) = viewport_ui_cb { // Child viewport crate::profile_scope!("viewport_callback"); viewport_ui_cb(egui_ctx); } else { // Root viewport if egui_ctx.input(|i| i.viewport().close_requested) { self.close = app.on_close_event(); log::debug!("App::on_close_event returned {}", self.close); } crate::profile_scope!("App::update"); app.update(egui_ctx, &mut self.frame); } }); self.pending_full_output.append(full_output); std::mem::take(&mut self.pending_full_output) } pub fn post_update(&mut self) { let frame_time = self.frame_start.elapsed().as_secs_f64() as f32; self.frame.info.cpu_usage = Some(frame_time); } pub fn post_rendering(&mut self, window: &winit::window::Window) { crate::profile_function!(); if std::mem::take(&mut self.is_first_frame) { // We keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279 window.set_visible(true); } } pub fn handle_platform_output( &mut self, window: &winit::window::Window, viewport_id: ViewportId, platform_output: egui::PlatformOutput, egui_winit: &mut egui_winit::State, ) { egui_winit.handle_platform_output(window, viewport_id, &self.egui_ctx, platform_output); } // ------------------------------------------------------------------------ // Persistence stuff: pub fn maybe_autosave( &mut self, app: &mut dyn epi::App, window: Option<&winit::window::Window>, ) { let now = Instant::now(); if now - self.last_auto_save > app.auto_save_interval() { self.save(app, window); self.last_auto_save = now; } } #[allow(clippy::unused_self)] pub fn save(&mut self, _app: &mut dyn epi::App, _window: Option<&winit::window::Window>) { #[cfg(feature = "persistence")] if let Some(storage) = self.frame.storage_mut() { crate::profile_function!(); if let Some(window) = _window { if self.persist_window { crate::profile_scope!("native_window"); epi::set_value( storage, STORAGE_WINDOW_KEY, &WindowSettings::from_display(window), ); } } if _app.persist_egui_memory() { crate::profile_scope!("egui_memory"); self.egui_ctx .memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem)); } { crate::profile_scope!("App::save"); _app.save(storage); } crate::profile_scope!("Storage::flush"); storage.flush(); } } } #[cfg(feature = "persistence")] const STORAGE_EGUI_MEMORY_KEY: &str = "egui"; #[cfg(feature = "persistence")] const STORAGE_WINDOW_KEY: &str = "window"; pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option { crate::profile_function!(); #[cfg(feature = "persistence")] { epi::get_value(_storage?, STORAGE_WINDOW_KEY) } #[cfg(not(feature = "persistence"))] None } pub fn load_egui_memory(_storage: Option<&dyn epi::Storage>) -> Option { crate::profile_function!(); #[cfg(feature = "persistence")] { epi::get_value(_storage?, STORAGE_EGUI_MEMORY_KEY) } #[cfg(not(feature = "persistence"))] None } pub(crate) fn theme_from_winit_theme(theme: winit::window::Theme) -> Theme { match theme { winit::window::Theme::Dark => Theme::Dark, winit::window::Theme::Light => Theme::Light, } }