diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index 270c38388..926070506 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -158,6 +158,10 @@ pub struct EpiIntegration { pub egui_ctx: egui::Context, pending_full_output: egui::FullOutput, + /// Input that we have received, but not yet given to egui, + /// because we haven't run any pass since (see [`Self::update_logic_only`]). + pending_raw_input: Option, + /// When set, it is time to close the native window. close: bool, @@ -216,6 +220,7 @@ impl EpiIntegration { frame, last_auto_save: Instant::now(), pending_full_output: Default::default(), + pending_raw_input: None, close: false, can_drag_window: false, #[cfg(feature = "persistence")] @@ -262,59 +267,122 @@ impl EpiIntegration { /// 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::ui`]. + /// If `viewport_ui_cb` is None, we are in the root viewport and will call + /// [`crate::App::logic`] and [`crate::App::ui`]. + /// + /// Only call this when the ui will actually be shown; + /// use [`Self::update_logic_only`] otherwise. pub fn update( &mut self, app: &mut dyn epi::App, viewport_ui_cb: Option<&DeferredViewportUiCallback>, - mut raw_input: egui::RawInput, - is_visible: bool, + raw_input: egui::RawInput, ) -> egui::FullOutput { - raw_input.time = Some(self.beginning.elapsed().as_secs_f64()); + let raw_input = self.prepare_raw_input(app, raw_input); let close_requested = raw_input.viewport().close_requested(); - app.raw_input_hook(&self.egui_ctx, &mut raw_input); + let is_root_viewport = viewport_ui_cb.is_none(); + if is_root_viewport { + // Note that this is _not_ inside the pass below: + // `App::logic` may not show any ui, and should not affect any ui state. + profiling::scope!("App::logic"); + app.logic(&self.egui_ctx, &mut self.frame); + } + + // Anything `App::logic` asked for (viewport commands etc) is still in the + // `Context`, and will come out of the pass we are about to run. let full_output = self.egui_ctx.run_ui(raw_input, |ui| { if let Some(viewport_ui_cb) = viewport_ui_cb { // Child viewport - if is_visible { - profiling::scope!("viewport_callback"); - viewport_ui_cb(ui); - } + profiling::scope!("viewport_callback"); + viewport_ui_cb(ui); } else { - { - profiling::scope!("App::logic"); - app.logic(ui.ctx(), &mut self.frame); - } - - if is_visible { - { - profiling::scope!("App::ui"); - app.ui(ui, &mut self.frame); - } - } + profiling::scope!("App::ui"); + app.ui(ui, &mut self.frame); } }); - let is_root_viewport = viewport_ui_cb.is_none(); if is_root_viewport && close_requested { let canceled = full_output.viewport_output[&ViewportId::ROOT] .commands .contains(&egui::ViewportCommand::CancelClose); - if canceled { - log::debug!("Closing of root viewport canceled with ViewportCommand::CancelClose"); - } else { - log::debug!("Closing root viewport (ViewportCommand::CancelClose was not sent)"); - self.close = true; - } + self.handle_close_request(canceled); } self.pending_full_output.append(full_output); std::mem::take(&mut self.pending_full_output) } + /// Let the app tick its logic without showing any ui, + /// because the window is minimized or occluded. + /// + /// No egui pass is run, so all ui state is left untouched: + /// the app will find everything where it left it once the window is visible again. + /// + /// Only call this for the root viewport: only it has [`crate::App::logic`]. + pub fn update_logic_only( + &mut self, + app: &mut dyn epi::App, + raw_input: egui::RawInput, + ) -> egui::LogicOutput { + let raw_input = self.prepare_raw_input(app, raw_input); + + let close_requested = raw_input.viewport().close_requested(); + + // No pass will consume the input, so save it for the next one: + match &mut self.pending_raw_input { + Some(pending) => pending.append(raw_input), + None => self.pending_raw_input = Some(raw_input), + } + + let logic_output = self.egui_ctx.run_logic(|ctx| { + profiling::scope!("App::logic"); + app.logic(ctx, &mut self.frame); + }); + + if close_requested { + let canceled = logic_output + .viewport_commands + .get(&ViewportId::ROOT) + .is_some_and(|commands| commands.contains(&egui::ViewportCommand::CancelClose)); + self.handle_close_request(canceled); + } + + logic_output + } + + /// Set the time, prepend any input we couldn't give to egui earlier, and run the app hook. + fn prepare_raw_input( + &mut self, + app: &mut dyn epi::App, + raw_input: egui::RawInput, + ) -> egui::RawInput { + let mut raw_input = match self.pending_raw_input.take() { + Some(mut pending) => { + pending.append(raw_input); // The new input wins where they overlap + pending + } + None => raw_input, + }; + + raw_input.time = Some(self.beginning.elapsed().as_secs_f64()); + + app.raw_input_hook(&self.egui_ctx, &mut raw_input); + + raw_input + } + + fn handle_close_request(&mut self, canceled: bool) { + if canceled { + log::debug!("Closing of root viewport canceled with ViewportCommand::CancelClose"); + } else { + log::debug!("Closing root viewport (ViewportCommand::CancelClose was not sent)"); + self.close = true; + } + } + pub fn report_frame_time(&mut self, seconds: f32) { self.frame.info.cpu_usage = Some(seconds); } diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 451aa0d82..f3c3a7cab 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -579,7 +579,7 @@ impl GlowWinitRunning<'_> { } } - let (raw_input, viewport_ui_cb, is_visible, run_ui) = { + let (raw_input, viewport_ui_cb, is_visible, show_ui) = { let mut glutin = self.glutin.borrow_mut(); let egui_ctx = glutin.egui_ctx.clone(); let Some(viewport) = glutin.viewports.get_mut(&viewport_id) else { @@ -598,7 +598,7 @@ impl GlowWinitRunning<'_> { let mut raw_input = egui_winit.take_egui_input(window); let viewport_ui_cb = viewport.viewport_ui_cb.clone(); - let run_ui = + let show_ui = is_visible || is_viewport_or_descendant_visible(&glutin.viewports, viewport_id); self.integration.pre_update(); @@ -610,9 +610,48 @@ impl GlowWinitRunning<'_> { .map(|(id, viewport)| (*id, viewport.info.clone())) .collect(); - (raw_input, viewport_ui_cb, is_visible, run_ui) + (raw_input, viewport_ui_cb, is_visible, show_ui) }; + if !show_ui { + // Nothing will be shown, so we run no egui pass at all. + // That way all ui state is left untouched, and is still there + // when this viewport becomes visible again. + let is_root_viewport = viewport_ui_cb.is_none(); + if is_root_viewport { + // The app logic keeps ticking, so it can e.g. ask to be shown again: + let egui::LogicOutput { + platform_output, + viewport_commands, + } = self + .integration + .update_logic_only(self.app.as_mut(), raw_input); + + let mut glutin = self.glutin.borrow_mut(); + if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) { + viewport.info.events.clear(); // they should have been processed + if let Some(window) = viewport.window.clone() + && let Some(egui_winit) = viewport.egui_winit.as_mut() + { + egui_winit.handle_platform_output_with_event_loop( + &window, + event_loop, + platform_output, + ); + } + } + glutin.handle_viewport_commands(&self.integration.egui_ctx, viewport_commands); + } + + self.sleep_if_minimized(viewport_id); + + return Ok(if self.integration.should_close() { + EventResult::CloseRequested + } else { + EventResult::Wait + }); + } + // HACK: In order to get the right clear_color, the system theme needs to be set, which // usually only happens in the `update` call. So we call Options::begin_pass early // to set the right theme. Without this there would be a black flash on the first frame. @@ -661,12 +700,9 @@ impl GlowWinitRunning<'_> { // 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, - run_ui, - ); + let full_output = + self.integration + .update(self.app.as_mut(), viewport_ui_cb.as_deref(), raw_input); // ------------------------------------------------------------ @@ -816,6 +852,22 @@ impl GlowWinitRunning<'_> { } } + /// On Mac, a minimized Window uses up all CPU: + /// + /// + /// On Windows, an invisible window also uses up all CPU: + /// + fn sleep_if_minimized(&self, viewport_id: ViewportId) { + let glutin = self.glutin.borrow(); + if let Some(viewport) = glutin.viewports.get(&viewport_id) + && let Some(window) = viewport.window.as_ref() + && is_invisible_or_minimized(window) + { + profiling::scope!("minimized_sleep"); + std::thread::sleep(std::time::Duration::from_millis(10)); + } + } + fn on_window_event( &mut self, window_id: WindowId, @@ -1365,6 +1417,36 @@ impl GlutinWindowContext { .retain(|id, _| viewport_output.contains_key(id)); } + /// Apply commands to already existing viewports, without creating or removing any. + /// + /// This is for commands that came out of [`egui::Context::run_logic`], + /// which knows nothing about which viewports should exist. + fn handle_viewport_commands( + &mut self, + egui_ctx: &egui::Context, + viewport_commands: egui::OrderedViewportIdMap>, + ) { + profiling::function_scope!(); + + for (viewport_id, mut commands) in viewport_commands { + let Some(viewport) = self.viewports.get_mut(&viewport_id) else { + continue; + }; + + viewport.deferred_commands.append(&mut commands); + + if let Some(window) = &viewport.window { + egui_winit::process_viewport_commands( + egui_ctx, + &mut viewport.info, + std::mem::take(&mut viewport.deferred_commands), + window, + &mut viewport.actions_requested, + ); + } + } + } + fn handle_viewport_output( &mut self, event_loop: &ActiveEventLoop, diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index e01b4d9a3..5be541d17 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -624,7 +624,7 @@ impl WgpuWinitRunning<'_> { let mut frame_timer = crate::stopwatch::Stopwatch::new(); frame_timer.start(); - let (viewport_ui_cb, raw_input, is_visible, run_ui) = { + let (viewport_ui_cb, raw_input, is_visible, show_ui) = { profiling::scope!("Prepare"); let mut shared_lock = shared.borrow_mut(); @@ -680,7 +680,7 @@ impl WgpuWinitRunning<'_> { }; let mut raw_input = egui_winit.take_egui_input(window); - let run_ui = is_visible || is_viewport_or_descendant_visible(viewports, viewport_id); + let show_ui = is_visible || is_viewport_or_descendant_visible(viewports, viewport_id); integration.pre_update(); @@ -692,15 +692,57 @@ impl WgpuWinitRunning<'_> { painter.handle_screenshots(&mut raw_input.events); - (viewport_ui_cb, raw_input, is_visible, run_ui) + (viewport_ui_cb, raw_input, is_visible, show_ui) }; + if !show_ui { + // Nothing will be shown, so we run no egui pass at all. + // That way all ui state is left untouched, and is still there + // when this viewport becomes visible again. + let is_root_viewport = viewport_ui_cb.is_none(); + if is_root_viewport { + // The app logic keeps ticking, so it can e.g. ask to be shown again: + let egui::LogicOutput { + platform_output, + viewport_commands, + } = integration.update_logic_only(app.as_mut(), raw_input); + + let mut shared_mut = shared.borrow_mut(); + let SharedState { viewports, .. } = &mut *shared_mut; + + if let Some(viewport) = viewports.get_mut(&viewport_id) { + viewport.info.events.clear(); // they should have been processed + if let Viewport { + window: Some(window), + egui_winit: Some(egui_winit), + .. + } = viewport + { + egui_winit.handle_platform_output_with_event_loop( + window, + event_loop, + platform_output, + ); + } + } + + handle_viewport_commands(&integration.egui_ctx, viewport_commands, viewports); + } + + sleep_if_minimized(&shared.borrow(), viewport_id); + + return Ok(if integration.should_close() { + EventResult::CloseRequested + } else { + EventResult::Wait + }); + } + // ------------------------------------------------------------ // 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, run_ui); + let full_output = integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input); // ------------------------------------------------------------ @@ -1208,6 +1250,51 @@ pub(crate) fn remove_viewports_not_in( } /// Add new viewports, and update existing ones: +/// Apply commands to already existing viewports, without creating or removing any. +/// +/// This is for commands that came out of [`egui::Context::run_logic`], +/// which knows nothing about which viewports should exist. +fn handle_viewport_commands( + egui_ctx: &egui::Context, + viewport_commands: egui::OrderedViewportIdMap>, + viewports: &mut Viewports, +) { + profiling::function_scope!(); + + for (viewport_id, mut commands) in viewport_commands { + let Some(viewport) = viewports.get_mut(&viewport_id) else { + continue; + }; + + viewport.deferred_commands.append(&mut commands); + + if let Some(window) = viewport.window.as_ref() { + egui_winit::process_viewport_commands( + egui_ctx, + &mut viewport.info, + std::mem::take(&mut viewport.deferred_commands), + window, + &mut viewport.actions_requested, + ); + } + } +} + +/// On Mac, a minimized Window uses up all CPU: +/// +/// +/// On Windows, an invisible window also uses up all CPU: +/// +fn sleep_if_minimized(shared: &SharedState, viewport_id: ViewportId) { + if let Some(viewport) = shared.viewports.get(&viewport_id) + && let Some(window) = viewport.window.as_ref() + && is_invisible_or_minimized(window) + { + profiling::scope!("minimized_sleep"); + std::thread::sleep(std::time::Duration::from_millis(10)); + } +} + fn handle_viewport_output( egui_ctx: &egui::Context, viewport_output: &OrderedViewportIdMap, diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index dbb714bed..9fb30c601 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -32,7 +32,7 @@ use crate::{ load::{self, Bytes, Loaders, SizedTexture}, memory::{Options, Theme}, os::OperatingSystem, - output::FullOutput, + output::{FullOutput, LogicOutput}, pass_state::PassState, plugin::{self, TypedPluginHandle}, resize, response, scroll_area, @@ -888,6 +888,45 @@ impl Context { output } + /// Run app logic without showing any ui. + /// + /// Use this instead of [`Self::run_ui`] when nothing will be shown, + /// e.g. because the window is minimized or occluded, + /// but you still want to let the app tick its logic + /// (so that it can e.g. ask to be shown again with [`ViewportCommand::Focus`]). + /// + /// No pass is run, so `f` must not show any ui. + /// This means everything egui knows about the ui is left untouched: + /// no widget state is garbage-collected, no animation advances, + /// nothing loses focus, and [`Self::input`] still refers to the last pass. + /// + /// The returned [`LogicOutput`] is what [`FullOutput`] would have carried: + /// anything `f` asked the integration to do. + /// There is nothing to paint. + #[must_use] + pub fn run_logic(&self, f: impl FnOnce(&Self)) -> LogicOutput { + profiling::function_scope!(); + + // Outside of a pass this is the root viewport: + let viewport_id = self.viewport_id(); + + // Consume any outstanding repaint request, so that a new request from `f` + // reaches the integration instead of being considered already served: + self.write(|ctx| ctx.begin_pass_repaint_logic(viewport_id)); + + f(self); + + self.write(|ctx| LogicOutput { + platform_output: std::mem::take(&mut ctx.viewport_for(viewport_id).output), + viewport_commands: ctx + .viewports + .iter_mut() + .filter(|(_, viewport)| !viewport.commands.is_empty()) + .map(|(&id, viewport)| (id, std::mem::take(&mut viewport.commands))) + .collect(), + }) + } + /// An alternative to calling [`Self::run_ui`]. /// /// It is usually better to use [`Self::run_ui`], because diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index bbd271b71..ae1363641 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -76,6 +76,22 @@ impl FullOutput { } } +/// What egui emits from [`crate::Context::run_logic`], i.e. from a tick where no ui was shown. +/// +/// There is nothing to paint, but the app may still have asked the integration to do things, +/// e.g. to show a hidden window again with [`crate::ViewportCommand::Focus`]. +#[derive(Clone, Default)] +pub struct LogicOutput { + /// Non-rendering related output. + pub platform_output: PlatformOutput, + + /// The commands sent with [`crate::Context::send_viewport_cmd`] and friends. + /// + /// Note that this contains no information about which viewports exist: + /// the integration should leave its viewports as they are. + pub viewport_commands: OrderedViewportIdMap>, +} + /// Information about text being edited. /// /// Useful for IME. diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 88de74b49..27ab7035c 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -467,7 +467,7 @@ pub use self::{ Key, UserData, input::*, output::{ - self, CursorIcon, CustomCursorImage, FullOutput, OpenUrl, OutputCommand, + self, CursorIcon, CustomCursorImage, FullOutput, LogicOutput, OpenUrl, OutputCommand, PlatformOutput, UserAttentionType, WidgetInfo, }, },