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

Untangle logic-only frames: one frame entry point, one output type

Same fixes as the previous commits (run no egui pass at all when
nothing will be shown), but the mechanisms are shared instead of
duplicated:

* `Context::run_frame(input, show_ui, f)` is the one entry point for
  integrations: one `FramePhase::Logic` (always, outside any pass),
  then one `FramePhase::Ui` per pass (none when `show_ui` is false).
  `Context::run_logic` is now a thin wrapper around it, and the
  logic-outside-of-pass sequencing lives in egui, not in each backend.

* egui itself buffers the input of pass-less frames
  (`ViewportState::pending_raw_input`) and prepends it to the next
  pass. This replaces `EpiIntegration::pending_raw_input` and the web
  backend's `input.raw.append`, so an integration cannot lose input.

* `FullOutput` is now `{ platform_output, viewport_commands,
  pass_output: Option<PassOutput> }`, and `LogicOutput` is gone.
  One-shot viewport commands (imperative) are separated from
  `ViewportOutput` (which viewports should exist - declarative), so
  each backend has exactly one command-handling path, shared by frames
  with and without a pass. `pass_output: None` encodes "no pass ran:
  paint nothing, leave the viewports alone" in the type.

* The glow/wgpu `!show_ui` early-return blocks are gone: a hidden root
  viewport flows through the same tail as a visible one, with the
  paint and viewport-structure steps gated on `pass_output`.

Behavioral fixes that fall out:

* `App::logic` now sees the current window state in visible frames
  too (it was one frame stale outside the hidden path).
* Auto-save keeps working while a window is minimized or occluded.
* Commands sent to a freshly created viewport apply in the same frame.
* The Wayland resize workaround now also covers commands from
  pass-less frames.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Lucas Meurer
2026-08-04 19:09:17 +02:00
parent 82d1c63c03
commit 33c85bd62a
18 changed files with 745 additions and 618 deletions

View File

@@ -158,10 +158,6 @@ 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<egui::RawInput>,
/// When set, it is time to close the native window.
close: bool,
@@ -220,7 +216,6 @@ 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")]
@@ -267,111 +262,60 @@ 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::logic`] and [`crate::App::ui`].
/// If `viewport_ui_cb` is None, we are in the root viewport and run one app frame:
/// [`crate::App::logic`], and, if `show_ui`, [`crate::App::ui`].
///
/// Only call this when the ui will actually be shown;
/// use [`Self::update_logic_only`] otherwise.
/// Pass `show_ui: false` when nothing will be shown,
/// e.g. because the window is minimized or occluded.
/// Then no egui pass is run and all ui state is left untouched:
/// the app will find everything where it left it once the window is visible again.
/// The returned [`egui::FullOutput`] then has no [`egui::FullOutput::pass_output`].
pub fn update(
&mut self,
app: &mut dyn epi::App,
viewport_ui_cb: Option<&DeferredViewportUiCallback>,
raw_input: egui::RawInput,
mut raw_input: egui::RawInput,
show_ui: bool,
) -> egui::FullOutput {
let raw_input = self.prepare_raw_input(app, raw_input);
raw_input.time = Some(self.beginning.elapsed().as_secs_f64());
app.raw_input_hook(&self.egui_ctx, &mut raw_input);
let close_requested = raw_input.viewport().close_requested();
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
let full_output = if let Some(viewport_ui_cb) = viewport_ui_cb {
// Child viewport. It has no `App::logic`, so there is no frame to run,
// and the backend only calls us when there is something to show:
debug_assert!(show_ui, "update called for a hidden child viewport");
self.egui_ctx.run_ui(raw_input, |ui| {
profiling::scope!("viewport_callback");
viewport_ui_cb(ui);
} else {
profiling::scope!("App::ui");
app.ui(ui, &mut self.frame);
}
});
})
} else {
self.egui_ctx
.run_frame(raw_input, show_ui, |phase| match phase {
egui::FramePhase::Logic(ctx) => {
profiling::scope!("App::logic");
app.logic(ctx, &mut self.frame);
}
egui::FramePhase::Ui(ui) => {
profiling::scope!("App::ui");
app.ui(ui, &mut self.frame);
}
})
};
if is_root_viewport && close_requested {
let canceled = full_output.viewport_output[&ViewportId::ROOT]
.commands
.contains(&egui::ViewportCommand::CancelClose);
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();
let logic_output = self.egui_ctx.run_logic(&raw_input, |ctx| {
profiling::scope!("App::logic");
app.logic(ctx, &mut self.frame);
});
// No pass consumed 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),
}
if close_requested {
let canceled = logic_output
let canceled = full_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
self.pending_full_output.append(full_output);
std::mem::take(&mut self.pending_full_output)
}
fn handle_close_request(&mut self, canceled: bool) {

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},
};
// ----------------------------------------------------------------------------
@@ -613,36 +613,10 @@ impl GlowWinitRunning<'_> {
(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);
}
if !show_ui && viewport_ui_cb.is_some() {
// A hidden child viewport: it has no app logic to tick, and nothing to show,
// so there is nothing to do. We run no egui pass at all, so all its ui state
// is left untouched, and is still there when it becomes visible again.
self.sleep_if_minimized(viewport_id);
return Ok(if self.integration.should_close() {
@@ -700,9 +674,12 @@ 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);
let full_output = self.integration.update(
self.app.as_mut(),
viewport_ui_cb.as_deref(),
raw_input,
show_ui,
);
// ------------------------------------------------------------
@@ -720,14 +697,13 @@ impl GlowWinitRunning<'_> {
let egui::FullOutput {
platform_output,
textures_delta,
shapes,
pixels_per_point,
viewport_output,
viewport_commands,
pass_output,
} = full_output;
pending_deltas.append(textures_delta);
glutin.remove_viewports_not_in(&viewport_output);
if let Some(pass_output) = &pass_output {
glutin.remove_viewports_not_in(&pass_output.viewport_output);
}
let GlutinWindowContext {
viewports,
@@ -742,108 +718,113 @@ impl GlowWinitRunning<'_> {
viewport.info.events.clear(); // they should have been processed
let window = viewport.window.clone().unwrap();
let gl_surface = viewport.gl_surface.as_ref().unwrap();
let egui_winit = viewport.egui_winit.as_mut().unwrap();
egui_winit.handle_platform_output_with_event_loop(&window, event_loop, platform_output);
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:
frame_timer.pause();
change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
frame_timer.resume();
}
let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
if !clear_before_update {
painter.clear(screen_size_in_pixels, clear_color);
}
painter.paint_and_update_textures(
screen_size_in_pixels,
if let Some(pass_output) = pass_output {
let egui::PassOutput {
textures_delta,
shapes,
pixels_per_point,
&clipped_primitives,
pending_deltas,
);
viewport_output,
} = pass_output;
pending_deltas.append(textures_delta);
{
for action in viewport.actions_requested.drain(..) {
match action {
ActionRequested::Screenshot(user_data) => {
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
egui_winit
.egui_input_mut()
.events
.push(egui::Event::Screenshot {
viewport_id,
user_data,
image: screenshot.into(),
});
}
ActionRequested::Cut => {
egui_winit.egui_input_mut().events.push(egui::Event::Cut);
}
ActionRequested::Copy => {
egui_winit.egui_input_mut().events.push(egui::Event::Copy);
}
ActionRequested::Paste => {
if let Some(contents) = egui_winit.clipboard_text() {
let contents = contents.replace("\r\n", "\n");
if !contents.is_empty() {
egui_winit
.egui_input_mut()
.events
.push(egui::Event::Paste(contents));
if is_visible {
let gl_surface = viewport.gl_surface.as_ref().unwrap();
let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point);
{
// We may need to switch contexts again, because of immediate viewports:
frame_timer.pause();
change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
frame_timer.resume();
}
let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
if !clear_before_update {
painter.clear(screen_size_in_pixels, clear_color);
}
painter.paint_and_update_textures(
screen_size_in_pixels,
pixels_per_point,
&clipped_primitives,
pending_deltas,
);
{
for action in viewport.actions_requested.drain(..) {
match action {
ActionRequested::Screenshot(user_data) => {
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
egui_winit
.egui_input_mut()
.events
.push(egui::Event::Screenshot {
viewport_id,
user_data,
image: screenshot.into(),
});
}
ActionRequested::Cut => {
egui_winit.egui_input_mut().events.push(egui::Event::Cut);
}
ActionRequested::Copy => {
egui_winit.egui_input_mut().events.push(egui::Event::Copy);
}
ActionRequested::Paste => {
if let Some(contents) = egui_winit.clipboard_text() {
let contents = contents.replace("\r\n", "\n");
if !contents.is_empty() {
egui_winit
.egui_input_mut()
.events
.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)?;
frame_timer.resume();
}
// give it time to settle:
#[cfg(feature = "__screenshot")]
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);
}
}
{
// 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)?;
frame_timer.resume();
}
// give it time to settle:
#[cfg(feature = "__screenshot")]
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, &viewport_output);
}
glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output);
glutin.handle_viewport_commands(&integration.egui_ctx, viewport_commands);
integration.report_frame_time(frame_timer.total_time_sec()); // don't count auto-save time as part of regular frame time
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(&window);
if integration.should_close() {
Ok(EventResult::CloseRequested)
@@ -852,19 +833,12 @@ impl GlowWinitRunning<'_> {
}
}
/// 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>
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));
sleep_if_invisible_or_minimized(window);
}
}
@@ -1419,69 +1393,38 @@ impl GlutinWindowContext {
/// 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.
/// Which viewports should exist is decided by [`Self::handle_viewport_output`];
/// commands can also come from a frame that ran no pass
/// (see [`egui::Context::run_frame`]), and such a frame knows nothing about
/// which viewports should exist.
///
/// This also flushes any previously deferred commands,
/// e.g. those produced by patching a [`ViewportBuilder`].
fn handle_viewport_commands(
&mut self,
egui_ctx: &egui::Context,
viewport_commands: egui::OrderedViewportIdMap<Vec<egui::ViewportCommand>>,
mut viewport_commands: egui::OrderedViewportIdMap<Vec<egui::ViewportCommand>>,
) {
profiling::function_scope!();
for (viewport_id, mut commands) in viewport_commands {
let viewport_ids: Vec<ViewportId> = self.viewports.keys().copied().collect();
for viewport_id in viewport_ids {
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,
);
if let Some(mut commands) = viewport_commands.remove(&viewport_id) {
viewport.deferred_commands.append(&mut commands);
}
}
}
fn handle_viewport_output(
&mut self,
event_loop: &ActiveEventLoop,
egui_ctx: &egui::Context,
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
) {
profiling::function_scope!();
for (
viewport_id,
ViewportOutput {
parent,
class,
builder,
viewport_ui_cb,
mut commands,
repaint_delay: _, // ignored - we listened to the repaint callback instead
},
) in viewport_output.clone()
{
let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent);
let viewport = initialize_or_update_viewport(
&mut self.viewports,
ids,
class,
builder,
viewport_ui_cb,
);
if viewport.deferred_commands.is_empty() {
continue;
}
if let Some(window) = &viewport.window {
let old_inner_size = window.inner_size();
viewport.deferred_commands.append(&mut commands);
egui_winit::process_viewport_commands(
egui_ctx,
&mut viewport.info,
@@ -1499,6 +1442,31 @@ impl GlutinWindowContext {
}
}
}
}
/// Create, update and remove viewports (native windows), to match `viewport_output`.
fn handle_viewport_output(
&mut self,
event_loop: &ActiveEventLoop,
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
) {
profiling::function_scope!();
for (
viewport_id,
ViewportOutput {
parent,
class,
builder,
viewport_ui_cb,
repaint_delay: _, // ignored - we listened to the repaint callback instead
},
) in viewport_output.clone()
{
let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent);
initialize_or_update_viewport(&mut self.viewports, ids, class, builder, viewport_ui_cb);
}
// Create windows for any new viewports:
self.initialize_all_windows(event_loop);
@@ -1663,13 +1631,17 @@ fn render_immediate_viewport(
let egui::FullOutput {
platform_output,
viewport_commands,
pass_output,
} = egui_ctx.run_ui(input, |ui| {
viewport_ui_cb(ui);
});
let egui::PassOutput {
textures_delta,
shapes,
pixels_per_point,
viewport_output,
} = egui_ctx.run_ui(input, |ui| {
viewport_ui_cb(ui);
});
} = pass_output.expect("run_ui always runs a pass");
// ---------------------------------------------------
@@ -1737,8 +1709,9 @@ fn render_immediate_viewport(
egui_winit.handle_platform_output(window, platform_output);
event_loop_context::with_current_event_loop(|event_loop| {
glutin.handle_viewport_output(event_loop, egui_ctx, &viewport_output);
glutin.handle_viewport_output(event_loop, &viewport_output);
});
glutin.handle_viewport_commands(egui_ctx, viewport_commands);
}
#[cfg(feature = "__screenshot")]

View File

@@ -30,7 +30,7 @@ use crate::{
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::{
epi_integration::EpiIntegration,
winit_integration::{EventResult, is_invisible_or_minimized},
winit_integration::{EventResult, sleep_if_invisible_or_minimized},
},
};
@@ -695,40 +695,10 @@ impl WgpuWinitRunning<'_> {
(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);
}
if !show_ui && viewport_ui_cb.is_some() {
// A hidden child viewport: it has no app logic to tick, and nothing to show,
// so there is nothing to do. We run no egui pass at all, so all its ui state
// is left untouched, and is still there when it becomes visible again.
sleep_if_minimized(&shared.borrow(), viewport_id);
return Ok(if integration.should_close() {
@@ -742,7 +712,8 @@ impl WgpuWinitRunning<'_> {
// 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);
let full_output =
integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input, show_ui);
// ------------------------------------------------------------
@@ -758,15 +729,18 @@ impl WgpuWinitRunning<'_> {
let FullOutput {
platform_output,
textures_delta,
shapes,
pixels_per_point,
viewport_output,
viewport_commands,
pass_output,
} = full_output;
pending_deltas.append(textures_delta);
remove_viewports_not_in(viewports, painter, viewport_from_window, &viewport_output);
if let Some(pass_output) = &pass_output {
remove_viewports_not_in(
viewports,
painter,
viewport_from_window,
&pass_output.viewport_output,
);
}
let Some(viewport) = viewports.get_mut(&viewport_id) else {
return Ok(EventResult::Wait);
@@ -785,74 +759,79 @@ impl WgpuWinitRunning<'_> {
egui_winit.handle_platform_output_with_event_loop(window, event_loop, platform_output);
let vsync_secs = if is_visible {
let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point);
let mut vsync_secs = 0.0;
let mut screenshot_commands = vec![];
viewport.actions_requested.retain(|cmd| {
if let ActionRequested::Screenshot(info) = cmd {
screenshot_commands.push(info.clone());
false
} else {
true
}
});
let vsync_secs = painter.paint_and_update_textures(
viewport_id,
if let Some(pass_output) = pass_output {
let egui::PassOutput {
textures_delta,
shapes,
pixels_per_point,
app.clear_color(&egui_ctx.global_style().visuals),
&clipped_primitives,
pending_deltas,
screenshot_commands,
window,
);
viewport_output,
} = pass_output;
for action in viewport.actions_requested.drain(..) {
match action {
ActionRequested::Screenshot { .. } => {
// already handled above
pending_deltas.append(textures_delta);
if is_visible {
let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point);
let mut screenshot_commands = vec![];
viewport.actions_requested.retain(|cmd| {
if let ActionRequested::Screenshot(info) = cmd {
screenshot_commands.push(info.clone());
false
} else {
true
}
ActionRequested::Cut => {
egui_winit.egui_input_mut().events.push(egui::Event::Cut);
}
ActionRequested::Copy => {
egui_winit.egui_input_mut().events.push(egui::Event::Copy);
}
ActionRequested::Paste => {
if let Some(contents) = egui_winit.clipboard_text() {
let contents = contents.replace("\r\n", "\n");
if !contents.is_empty() {
egui_winit
.egui_input_mut()
.events
.push(egui::Event::Paste(contents));
});
vsync_secs = painter.paint_and_update_textures(
viewport_id,
pixels_per_point,
app.clear_color(&egui_ctx.global_style().visuals),
&clipped_primitives,
pending_deltas,
screenshot_commands,
window,
);
for action in viewport.actions_requested.drain(..) {
match action {
ActionRequested::Screenshot { .. } => {
// already handled above
}
ActionRequested::Cut => {
egui_winit.egui_input_mut().events.push(egui::Event::Cut);
}
ActionRequested::Copy => {
egui_winit.egui_input_mut().events.push(egui::Event::Copy);
}
ActionRequested::Paste => {
if let Some(contents) = egui_winit.clipboard_text() {
let contents = contents.replace("\r\n", "\n");
if !contents.is_empty() {
egui_winit
.egui_input_mut()
.events
.push(egui::Event::Paste(contents));
}
}
}
}
}
integration.post_rendering(window);
}
integration.post_rendering(window);
let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect();
vsync_secs
} else {
0.0
};
handle_viewport_output(&viewport_output, viewports, painter, viewport_from_window);
let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect();
// Prune dead viewports:
viewports.retain(|id, _| active_viewports_ids.contains(id));
viewport_from_window.retain(|_, id| active_viewports_ids.contains(id));
painter.gc_viewports(&active_viewports_ids);
}
handle_viewport_output(
&integration.egui_ctx,
&viewport_output,
viewports,
painter,
viewport_from_window,
);
// Prune dead viewports:
viewports.retain(|id, _| active_viewports_ids.contains(id));
viewport_from_window.retain(|_, id| active_viewports_ids.contains(id));
painter.gc_viewports(&active_viewports_ids);
handle_viewport_commands(&integration.egui_ctx, viewport_commands, viewports, painter);
let window = viewport_from_window
.get(&window_id)
@@ -863,15 +842,8 @@ impl WgpuWinitRunning<'_> {
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
if let Some(window) = window
&& 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));
if let Some(window) = window {
sleep_if_invisible_or_minimized(window);
}
if integration.should_close() {
@@ -1173,13 +1145,17 @@ fn render_immediate_viewport(
// Make sure no locks are held during this call.
let egui::FullOutput {
platform_output,
viewport_commands,
pass_output,
} = egui_ctx.run_ui(input, |ui| {
viewport_ui_cb(ui);
});
let egui::PassOutput {
textures_delta,
shapes,
pixels_per_point,
viewport_output,
} = egui_ctx.run_ui(input, |ui| {
viewport_ui_cb(ui);
});
} = pass_output.expect("run_ui always runs a pass");
// ------------------------------------------
@@ -1226,13 +1202,8 @@ fn render_immediate_viewport(
egui_winit.handle_platform_output(window, platform_output);
handle_viewport_output(
&egui_ctx,
&viewport_output,
viewports,
painter,
viewport_from_window,
);
handle_viewport_output(&viewport_output, viewports, painter, viewport_from_window);
handle_viewport_commands(&egui_ctx, viewport_commands, viewports, painter);
}
pub(crate) fn remove_viewports_not_in(
@@ -1249,81 +1220,41 @@ pub(crate) fn remove_viewports_not_in(
painter.gc_viewports(&active_viewports_ids);
}
/// 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.
/// Which viewports should exist is decided by [`handle_viewport_output`];
/// commands can also come from a frame that ran no pass
/// (see [`egui::Context::run_frame`]), and such a frame knows nothing about
/// which viewports should exist.
///
/// This also flushes any previously deferred commands,
/// e.g. those produced by patching a [`ViewportBuilder`].
fn handle_viewport_commands(
egui_ctx: &egui::Context,
viewport_commands: egui::OrderedViewportIdMap<Vec<egui::ViewportCommand>>,
mut viewport_commands: egui::OrderedViewportIdMap<Vec<egui::ViewportCommand>>,
viewports: &mut Viewports,
painter: &mut egui_wgpu::winit::Painter,
) {
profiling::function_scope!();
for (viewport_id, mut commands) in viewport_commands {
let viewport_ids: Vec<ViewportId> = viewports.keys().copied().collect();
for viewport_id in viewport_ids {
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,
);
if let Some(mut commands) = viewport_commands.remove(&viewport_id) {
viewport.deferred_commands.append(&mut commands);
}
}
}
/// 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>
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<ViewportOutput>,
viewports: &mut Viewports,
painter: &mut egui_wgpu::winit::Painter,
viewport_from_window: &mut HashMap<WindowId, ViewportId>,
) {
for (
viewport_id,
ViewportOutput {
parent,
class,
builder,
viewport_ui_cb,
mut commands,
repaint_delay: _, // ignored - we listened to the repaint callback instead
},
) in viewport_output.clone()
{
let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent);
let viewport =
initialize_or_update_viewport(viewports, ids, class, builder, viewport_ui_cb, painter);
if viewport.deferred_commands.is_empty() {
continue;
}
if let Some(window) = viewport.window.as_ref() {
let old_inner_size = window.inner_size();
viewport.deferred_commands.append(&mut commands);
egui_winit::process_viewport_commands(
egui_ctx,
&mut viewport.info,
@@ -1346,6 +1277,38 @@ fn handle_viewport_output(
}
}
}
}
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()
{
sleep_if_invisible_or_minimized(window);
}
}
/// Create, update and remove viewports (native windows), to match `viewport_output`.
fn handle_viewport_output(
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
viewports: &mut Viewports,
painter: &mut egui_wgpu::winit::Painter,
viewport_from_window: &mut HashMap<WindowId, ViewportId>,
) {
for (
viewport_id,
ViewportOutput {
parent,
class,
builder,
viewport_ui_cb,
repaint_delay: _, // ignored - we listened to the repaint callback instead
},
) in viewport_output.clone()
{
let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent);
initialize_or_update_viewport(viewports, ids, class, builder, viewport_ui_cb, painter);
}
remove_viewports_not_in(viewports, painter, viewport_from_window, viewport_output);
}

View File

@@ -17,6 +17,20 @@ pub fn is_invisible_or_minimized(window: &Window) -> bool {
window.is_visible() == Some(false) || window.is_minimized() == Some(true)
}
/// Sleep for a bit if the window is invisible or minimized.
///
/// 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>
pub fn sleep_if_invisible_or_minimized(window: &Window) {
if is_invisible_or_minimized(window) {
profiling::scope!("minimized_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
/// Create an egui context, restoring it from storage if possible.
pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context {
profiling::function_scope!();

View File

@@ -280,52 +280,41 @@ impl AppRunner {
.and_then(|v| v.visible())
.unwrap_or(true);
if !is_visible {
// The tab is hidden, so we run no egui pass at all.
// That way all ui state is left untouched, and is still there
// when the tab is shown again.
let egui::LogicOutput {
platform_output,
viewport_commands,
} = self.egui_ctx.run_logic(&raw_input, |ctx| {
self.app.logic(ctx, &mut self.frame);
});
// No pass consumed the input, so save it for the next one:
self.input.raw.append(raw_input);
self.handle_viewport_commands(viewport_commands.into_values().flatten());
self.handle_platform_output(platform_output);
return;
}
// `App::logic` may not show any ui, so it is called outside of the pass:
self.app.logic(&self.egui_ctx, &mut self.frame);
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
self.app.ui(ui, &mut self.frame);
});
// When the tab is hidden (`!is_visible`), we run no egui pass at all.
// That way all ui state is left untouched, and is still there
// when the tab is shown again.
let egui::FullOutput {
platform_output,
viewport_commands,
pass_output,
} = self
.egui_ctx
.run_frame(raw_input, is_visible, |phase| match phase {
egui::FramePhase::Logic(ctx) => {
self.app.logic(ctx, &mut self.frame);
}
egui::FramePhase::Ui(ui) => {
self.app.ui(ui, &mut self.frame);
}
});
self.handle_viewport_commands(viewport_commands.into_values().flatten());
self.handle_platform_output(platform_output);
if let Some(egui::PassOutput {
textures_delta,
shapes,
pixels_per_point,
viewport_output,
} = full_output;
}) = pass_output
{
if viewport_output.len() > 1 {
log::warn!("Multiple viewports not yet supported on the web");
}
if viewport_output.len() > 1 {
log::warn!("Multiple viewports not yet supported on the web");
self.textures_delta.append(textures_delta);
self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point));
}
self.handle_viewport_commands(
viewport_output
.into_values()
.flat_map(|viewport_output| viewport_output.commands),
);
self.handle_platform_output(platform_output);
self.textures_delta.append(textures_delta);
self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point));
}
fn handle_viewport_commands(&mut self, commands: impl Iterator<Item = ViewportCommand>) {