mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -04:00
Never run an egui pass when nothing will be shown (#8387)
* Closes <https://github.com/emilk/egui/issues/8266> * Alternative to #8385 Not the most simple or beautiful code, but it works, and makes sense. What makes it complex: `app.logic` should still see some input (e.g. what viewports are visible) and emit some output (e.g. "open this link", or "focus and repaint"). ## TODO * [x] test multiple viewports ## Clanker says Instead of teaching egui to skip book-keeping during a pass where no ui is shown, we simply run no pass at all. Then there is nothing to special-case: all ui state is left untouched, and the app finds everything where it left it when the window is shown again. * New `Context::run_logic(&raw_input, f)`: ticks app logic without a pass, returning the `LogicOutput` (platform output + viewport commands) that a pass would otherwise have carried, so e.g. `ViewportCommand::Focus` still reaches the integration. * All three eframe backends (glow, wgpu, web) call `run_logic` instead of `run_ui` when the viewport is minimized/occluded (and has no visible descendant viewport) or, on web, when the tab is hidden. * `App::logic` is still called from inside the pass when the window is visible, so it sees the current frame's input. While hidden, `run_logic` fills in only the window state (`RawInput::viewports` / `focused`), so the app can tell that it is hidden. The ui input (events, time, …) is not interpreted, and is instead given to the next real pass. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,7 +40,7 @@ use super::{
|
||||
use crate::epaint::textures::TexturesDelta;
|
||||
use crate::{
|
||||
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
||||
native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized},
|
||||
native::{epi_integration::EpiIntegration, winit_integration::sleep_if_invisible_or_minimized},
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -139,6 +139,27 @@ struct Viewport {
|
||||
egui_winit: Option<egui_winit::State>,
|
||||
}
|
||||
|
||||
impl Viewport {
|
||||
/// Apply the commands, or defer them until we have a window.
|
||||
fn process_commands(
|
||||
&mut self,
|
||||
egui_ctx: &egui::Context,
|
||||
mut commands: Vec<egui::ViewportCommand>,
|
||||
) {
|
||||
self.deferred_commands.append(&mut commands);
|
||||
|
||||
if let Some(window) = &self.window {
|
||||
egui_winit::process_viewport_commands(
|
||||
egui_ctx,
|
||||
&mut self.info,
|
||||
std::mem::take(&mut self.deferred_commands),
|
||||
window,
|
||||
&mut self.actions_requested,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Viewport {
|
||||
fn drop(&mut self) {
|
||||
// Avoid debug panic when dropping unapplied deltas on teardown
|
||||
@@ -579,7 +600,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 +619,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 +631,58 @@ 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (id, commands) in viewport_commands {
|
||||
if let Some(viewport) = glutin.viewports.get_mut(&id) {
|
||||
viewport.process_commands(&self.integration.egui_ctx, commands);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sleep_if_invisible_or_minimized(
|
||||
self.glutin
|
||||
.borrow()
|
||||
.viewports
|
||||
.get(&viewport_id)
|
||||
.and_then(|viewport| viewport.window.as_deref()),
|
||||
);
|
||||
|
||||
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 +731,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);
|
||||
|
||||
// ------------------------------------------------------------
|
||||
|
||||
@@ -800,14 +867,7 @@ impl GlowWinitRunning<'_> {
|
||||
|
||||
integration.maybe_autosave(app.as_mut(), Some(&window));
|
||||
|
||||
if is_invisible_or_minimized(&window) {
|
||||
// On Mac, a minimized Window uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/325
|
||||
// On Windows, an invisible window also uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/7776
|
||||
profiling::scope!("minimized_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
sleep_if_invisible_or_minimized(Some(&window));
|
||||
|
||||
if integration.should_close() {
|
||||
Ok(EventResult::CloseRequested)
|
||||
@@ -1380,7 +1440,7 @@ impl GlutinWindowContext {
|
||||
class,
|
||||
builder,
|
||||
viewport_ui_cb,
|
||||
mut commands,
|
||||
commands,
|
||||
repaint_delay: _, // ignored - we listened to the repaint callback instead
|
||||
},
|
||||
) in viewport_output.clone()
|
||||
@@ -1395,25 +1455,18 @@ impl GlutinWindowContext {
|
||||
viewport_ui_cb,
|
||||
);
|
||||
|
||||
if let Some(window) = &viewport.window {
|
||||
let old_inner_size = window.inner_size();
|
||||
let old_inner_size = viewport.window.as_ref().map(|window| window.inner_size());
|
||||
|
||||
viewport.deferred_commands.append(&mut commands);
|
||||
viewport.process_commands(egui_ctx, commands);
|
||||
|
||||
egui_winit::process_viewport_commands(
|
||||
egui_ctx,
|
||||
&mut viewport.info,
|
||||
std::mem::take(&mut viewport.deferred_commands),
|
||||
window,
|
||||
&mut viewport.actions_requested,
|
||||
);
|
||||
|
||||
// For Wayland : https://github.com/emilk/egui/issues/4196
|
||||
if cfg!(target_os = "linux") {
|
||||
let new_inner_size = window.inner_size();
|
||||
if new_inner_size != old_inner_size {
|
||||
self.resize(viewport_id, new_inner_size);
|
||||
}
|
||||
// For Wayland : https://github.com/emilk/egui/issues/4196
|
||||
if cfg!(target_os = "linux")
|
||||
&& let Some(window) = &viewport.window
|
||||
&& let Some(old_inner_size) = old_inner_size
|
||||
{
|
||||
let new_inner_size = window.inner_size();
|
||||
if new_inner_size != old_inner_size {
|
||||
self.resize(viewport_id, new_inner_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user