1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-31 22:00:03 -04:00

Only run App::ui if the application is visible (#7950)

* Closes https://github.com/emilk/egui/issues/5113
* Part of https://github.com/emilk/egui/issues/5112
* Part of https://github.com/emilk/egui/issues/5136

If the application is invisible (occluded or minimized), and the user
calls `.request_repaint`, then we should call `App::logic`, but NOT
`App::ui`.

There are still some situations where `App::logic` is not called when it
should be, but at least now we can skip running the UI code when the app
is invisible.
This commit is contained in:
Emil Ernerfeldt
2026-03-02 19:30:24 +01:00
committed by GitHub
parent 2be6e225bf
commit 1b8a9fe95e
4 changed files with 164 additions and 129 deletions

View File

@@ -265,6 +265,7 @@ impl EpiIntegration {
app: &mut dyn epi::App, app: &mut dyn epi::App,
viewport_ui_cb: Option<&DeferredViewportUiCallback>, viewport_ui_cb: Option<&DeferredViewportUiCallback>,
mut raw_input: egui::RawInput, mut raw_input: egui::RawInput,
is_visible: bool,
) -> egui::FullOutput { ) -> egui::FullOutput {
raw_input.time = Some(self.beginning.elapsed().as_secs_f64()); raw_input.time = Some(self.beginning.elapsed().as_secs_f64());
@@ -275,23 +276,27 @@ impl EpiIntegration {
let full_output = self.egui_ctx.run_ui(raw_input, |ui| { let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
if let Some(viewport_ui_cb) = viewport_ui_cb { if let Some(viewport_ui_cb) = viewport_ui_cb {
// Child viewport // Child viewport
profiling::scope!("viewport_callback"); if is_visible {
viewport_ui_cb(ui); profiling::scope!("viewport_callback");
viewport_ui_cb(ui);
}
} else { } else {
{ {
profiling::scope!("App::logic"); profiling::scope!("App::logic");
app.logic(ui.ctx(), &mut self.frame); app.logic(ui.ctx(), &mut self.frame);
} }
{ if is_visible {
profiling::scope!("App::update"); {
#[expect(deprecated)] profiling::scope!("App::update");
app.update(ui.ctx(), &mut self.frame); #[expect(deprecated)]
} app.update(ui.ctx(), &mut self.frame);
}
{ {
profiling::scope!("App::ui"); profiling::scope!("App::ui");
app.ui(ui, &mut self.frame); app.ui(ui, &mut self.frame);
}
} }
} }
}); });

View File

@@ -545,7 +545,7 @@ impl GlowWinitRunning<'_> {
} }
} }
let (raw_input, viewport_ui_cb) = { let (raw_input, viewport_ui_cb, is_visible) = {
let mut glutin = self.glutin.borrow_mut(); let mut glutin = self.glutin.borrow_mut();
let egui_ctx = glutin.egui_ctx.clone(); let egui_ctx = glutin.egui_ctx.clone();
let Some(viewport) = glutin.viewports.get_mut(&viewport_id) else { let Some(viewport) = glutin.viewports.get_mut(&viewport_id) else {
@@ -556,6 +556,8 @@ impl GlowWinitRunning<'_> {
}; };
egui_winit::update_viewport_info(&mut viewport.info, &egui_ctx, window, false); egui_winit::update_viewport_info(&mut viewport.info, &egui_ctx, window, false);
let is_visible = viewport.info.visible().unwrap_or(true);
let Some(egui_winit) = viewport.egui_winit.as_mut() else { let Some(egui_winit) = viewport.egui_winit.as_mut() else {
return Ok(EventResult::Wait); return Ok(EventResult::Wait);
}; };
@@ -571,7 +573,7 @@ impl GlowWinitRunning<'_> {
.map(|(id, viewport)| (*id, viewport.info.clone())) .map(|(id, viewport)| (*id, viewport.info.clone()))
.collect(); .collect();
(raw_input, viewport_ui_cb) (raw_input, viewport_ui_cb, is_visible)
}; };
// HACK: In order to get the right clear_color, the system theme needs to be set, which // HACK: In order to get the right clear_color, the system theme needs to be set, which
@@ -587,7 +589,7 @@ impl GlowWinitRunning<'_> {
let has_many_viewports = self.glutin.borrow().viewports.len() > 1; let has_many_viewports = self.glutin.borrow().viewports.len() > 1;
let clear_before_update = !has_many_viewports; // HACK: for some reason, an early clear doesn't "take" on Mac with multiple viewports. let clear_before_update = !has_many_viewports; // HACK: for some reason, an early clear doesn't "take" on Mac with multiple viewports.
if clear_before_update { if is_visible && clear_before_update {
// clear before we call update, so users can paint between clear-color and egui windows: // clear before we call update, so users can paint between clear-color and egui windows:
let mut glutin = self.glutin.borrow_mut(); let mut glutin = self.glutin.borrow_mut();
@@ -622,9 +624,12 @@ impl GlowWinitRunning<'_> {
// The update function, which could call immediate viewports, // The update function, which could call immediate viewports,
// so make sure we don't hold any locks here required by the immediate viewports rendeer. // so make sure we don't hold any locks here required by the immediate viewports rendeer.
let full_output = let full_output = self.integration.update(
self.integration self.app.as_mut(),
.update(self.app.as_mut(), viewport_ui_cb.as_deref(), raw_input); viewport_ui_cb.as_deref(),
raw_input,
is_visible,
);
// ------------------------------------------------------------ // ------------------------------------------------------------
@@ -667,85 +672,87 @@ impl GlowWinitRunning<'_> {
egui_winit.handle_platform_output(&window, platform_output); egui_winit.handle_platform_output(&window, platform_output);
let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point); if is_visible {
let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point);
{ {
// We may need to switch contexts again, because of immediate viewports: // We may need to switch contexts again, because of immediate viewports:
frame_timer.pause(); frame_timer.pause();
change_gl_context(current_gl_context, not_current_gl_context, gl_surface); change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
frame_timer.resume(); frame_timer.resume();
} }
let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
if !clear_before_update { if !clear_before_update {
painter.clear(screen_size_in_pixels, clear_color); painter.clear(screen_size_in_pixels, clear_color);
} }
painter.paint_and_update_textures( painter.paint_and_update_textures(
screen_size_in_pixels, screen_size_in_pixels,
pixels_per_point, pixels_per_point,
&clipped_primitives, &clipped_primitives,
&textures_delta, &textures_delta,
); );
{ {
for action in viewport.actions_requested.drain(..) { for action in viewport.actions_requested.drain(..) {
match action { match action {
ActionRequested::Screenshot(user_data) => { ActionRequested::Screenshot(user_data) => {
let screenshot = painter.read_screen_rgba(screen_size_in_pixels); let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
egui_winit egui_winit
.egui_input_mut() .egui_input_mut()
.events .events
.push(egui::Event::Screenshot { .push(egui::Event::Screenshot {
viewport_id, viewport_id,
user_data, user_data,
image: screenshot.into(), image: screenshot.into(),
}); });
} }
ActionRequested::Cut => { ActionRequested::Cut => {
egui_winit.egui_input_mut().events.push(egui::Event::Cut); egui_winit.egui_input_mut().events.push(egui::Event::Cut);
} }
ActionRequested::Copy => { ActionRequested::Copy => {
egui_winit.egui_input_mut().events.push(egui::Event::Copy); egui_winit.egui_input_mut().events.push(egui::Event::Copy);
} }
ActionRequested::Paste => { ActionRequested::Paste => {
if let Some(contents) = egui_winit.clipboard_text() { if let Some(contents) = egui_winit.clipboard_text() {
let contents = contents.replace("\r\n", "\n"); let contents = contents.replace("\r\n", "\n");
if !contents.is_empty() { if !contents.is_empty() {
egui_winit egui_winit
.egui_input_mut() .egui_input_mut()
.events .events
.push(egui::Event::Paste(contents)); .push(egui::Event::Paste(contents));
}
} }
} }
} }
} }
integration.post_rendering(&window);
} }
integration.post_rendering(&window); {
} // vsync - don't count as frame-time:
frame_timer.pause();
profiling::scope!("swap_buffers");
let context = current_gl_context.as_ref().ok_or_else(|| {
egui_glow::PainterError::from(
"failed to get current context to swap buffers".to_owned(),
)
})?;
{ gl_surface.swap_buffers(context)?;
// vsync - don't count as frame-time: frame_timer.resume();
frame_timer.pause(); }
profiling::scope!("swap_buffers");
let context = current_gl_context.as_ref().ok_or_else(|| {
egui_glow::PainterError::from(
"failed to get current context to swap buffers".to_owned(),
)
})?;
gl_surface.swap_buffers(context)?; // give it time to settle:
frame_timer.resume(); #[cfg(feature = "__screenshot")]
} if integration.egui_ctx.cumulative_pass_nr() == 2
&& let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO")
// give it time to settle: {
#[cfg(feature = "__screenshot")] save_screenshot_and_exit(&path, &painter, screen_size_in_pixels);
if integration.egui_ctx.cumulative_pass_nr() == 2 }
&& let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO")
{
save_screenshot_and_exit(&path, &painter, screen_size_in_pixels);
} }
glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output); glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output);

View File

@@ -573,7 +573,7 @@ impl WgpuWinitRunning<'_> {
let mut frame_timer = crate::stopwatch::Stopwatch::new(); let mut frame_timer = crate::stopwatch::Stopwatch::new();
frame_timer.start(); frame_timer.start();
let (viewport_ui_cb, raw_input) = { let (viewport_ui_cb, raw_input, is_visible) = {
profiling::scope!("Prepare"); profiling::scope!("Prepare");
let mut shared_lock = shared.borrow_mut(); let mut shared_lock = shared.borrow_mut();
@@ -617,6 +617,8 @@ impl WgpuWinitRunning<'_> {
}; };
egui_winit::update_viewport_info(info, &integration.egui_ctx, window, false); egui_winit::update_viewport_info(info, &integration.egui_ctx, window, false);
let is_visible = viewport.info.visible().unwrap_or(true);
{ {
profiling::scope!("set_window"); profiling::scope!("set_window");
pollster::block_on(painter.set_window(viewport_id, Some(Arc::clone(window))))?; pollster::block_on(painter.set_window(viewport_id, Some(Arc::clone(window))))?;
@@ -637,14 +639,19 @@ impl WgpuWinitRunning<'_> {
painter.handle_screenshots(&mut raw_input.events); painter.handle_screenshots(&mut raw_input.events);
(viewport_ui_cb, raw_input) (viewport_ui_cb, raw_input, is_visible)
}; };
// ------------------------------------------------------------ // ------------------------------------------------------------
// Runs the update, which could call immediate viewports, // Runs the update, which could call immediate viewports,
// so make sure we hold no locks here! // so make sure we hold no locks here!
let full_output = integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input); let full_output = integration.update(
app.as_mut(),
viewport_ui_cb.as_deref(),
raw_input,
is_visible,
);
// ------------------------------------------------------------ // ------------------------------------------------------------
@@ -685,52 +692,58 @@ impl WgpuWinitRunning<'_> {
egui_winit.handle_platform_output(window, platform_output); egui_winit.handle_platform_output(window, platform_output);
let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); let vsync_secs = if is_visible {
let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point);
let mut screenshot_commands = vec![]; let mut screenshot_commands = vec![];
viewport.actions_requested.retain(|cmd| { viewport.actions_requested.retain(|cmd| {
if let ActionRequested::Screenshot(info) = cmd { if let ActionRequested::Screenshot(info) = cmd {
screenshot_commands.push(info.clone()); screenshot_commands.push(info.clone());
false false
} else { } else {
true true
} }
}); });
let vsync_secs = painter.paint_and_update_textures( let vsync_secs = painter.paint_and_update_textures(
viewport_id, viewport_id,
pixels_per_point, pixels_per_point,
app.clear_color(&egui_ctx.global_style().visuals), app.clear_color(&egui_ctx.global_style().visuals),
&clipped_primitives, &clipped_primitives,
&textures_delta, &textures_delta,
screenshot_commands, screenshot_commands,
); );
for action in viewport.actions_requested.drain(..) { for action in viewport.actions_requested.drain(..) {
match action { match action {
ActionRequested::Screenshot { .. } => { ActionRequested::Screenshot { .. } => {
// already handled above // already handled above
} }
ActionRequested::Cut => { ActionRequested::Cut => {
egui_winit.egui_input_mut().events.push(egui::Event::Cut); egui_winit.egui_input_mut().events.push(egui::Event::Cut);
} }
ActionRequested::Copy => { ActionRequested::Copy => {
egui_winit.egui_input_mut().events.push(egui::Event::Copy); egui_winit.egui_input_mut().events.push(egui::Event::Copy);
} }
ActionRequested::Paste => { ActionRequested::Paste => {
if let Some(contents) = egui_winit.clipboard_text() { if let Some(contents) = egui_winit.clipboard_text() {
let contents = contents.replace("\r\n", "\n"); let contents = contents.replace("\r\n", "\n");
if !contents.is_empty() { if !contents.is_empty() {
egui_winit egui_winit
.egui_input_mut() .egui_input_mut()
.events .events
.push(egui::Event::Paste(contents)); .push(egui::Event::Paste(contents));
}
} }
} }
} }
} }
}
integration.post_rendering(window); integration.post_rendering(window);
vsync_secs
} else {
0.0
};
let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect(); let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect();

View File

@@ -274,13 +274,21 @@ impl AppRunner {
self.app.raw_input_hook(&self.egui_ctx, &mut raw_input); self.app.raw_input_hook(&self.egui_ctx, &mut raw_input);
let is_visible = raw_input
.viewports
.get(&egui::ViewportId::ROOT)
.and_then(|v| v.visible())
.unwrap_or(true);
let full_output = self.egui_ctx.run_ui(raw_input, |ui| { let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
self.app.logic(ui.ctx(), &mut self.frame); self.app.logic(ui.ctx(), &mut self.frame);
#[expect(deprecated)] if is_visible {
self.app.update(ui.ctx(), &mut self.frame); #[expect(deprecated)]
self.app.update(ui.ctx(), &mut self.frame);
self.app.ui(ui, &mut self.frame); self.app.ui(ui, &mut self.frame);
}
}); });
let egui::FullOutput { let egui::FullOutput {
platform_output, platform_output,
@@ -311,8 +319,10 @@ impl AppRunner {
} }
self.handle_platform_output(platform_output); self.handle_platform_output(platform_output);
self.textures_delta.append(textures_delta); if is_visible {
self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point)); self.textures_delta.append(textures_delta);
self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point));
}
} }
/// Paint the results of the last call to [`Self::logic`]. /// Paint the results of the last call to [`Self::logic`].