1
0
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:
Emil Ernerfeldt
2026-08-05 00:14:35 -07:00
committed by GitHub
parent 90e03028f7
commit e37d44ad8a
10 changed files with 519 additions and 132 deletions

View File

@@ -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);
}
}
}