1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 04:40: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, pub egui_ctx: egui::Context,
pending_full_output: egui::FullOutput, 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. /// When set, it is time to close the native window.
close: bool, close: bool,
@@ -220,7 +216,6 @@ impl EpiIntegration {
frame, frame,
last_auto_save: Instant::now(), last_auto_save: Instant::now(),
pending_full_output: Default::default(), pending_full_output: Default::default(),
pending_raw_input: None,
close: false, close: false,
can_drag_window: false, can_drag_window: false,
#[cfg(feature = "persistence")] #[cfg(feature = "persistence")]
@@ -267,111 +262,60 @@ impl EpiIntegration {
/// Run user code - this can create immediate viewports, so hold no locks over this! /// 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 /// If `viewport_ui_cb` is None, we are in the root viewport and run one app frame:
/// [`crate::App::logic`] and [`crate::App::ui`]. /// [`crate::App::logic`], and, if `show_ui`, [`crate::App::ui`].
/// ///
/// Only call this when the ui will actually be shown; /// Pass `show_ui: false` when nothing will be shown,
/// use [`Self::update_logic_only`] otherwise. /// 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( pub fn update(
&mut self, &mut self,
app: &mut dyn epi::App, app: &mut dyn epi::App,
viewport_ui_cb: Option<&DeferredViewportUiCallback>, viewport_ui_cb: Option<&DeferredViewportUiCallback>,
raw_input: egui::RawInput, mut raw_input: egui::RawInput,
show_ui: bool,
) -> egui::FullOutput { ) -> 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 close_requested = raw_input.viewport().close_requested();
let is_root_viewport = viewport_ui_cb.is_none(); let is_root_viewport = viewport_ui_cb.is_none();
if is_root_viewport { let full_output = if let Some(viewport_ui_cb) = viewport_ui_cb {
// Note that this is _not_ inside the pass below: // Child viewport. It has no `App::logic`, so there is no frame to run,
// `App::logic` may not show any ui, and should not affect any ui state. // and the backend only calls us when there is something to show:
profiling::scope!("App::logic"); debug_assert!(show_ui, "update called for a hidden child viewport");
app.logic(&self.egui_ctx, &mut self.frame); self.egui_ctx.run_ui(raw_input, |ui| {
}
// 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
profiling::scope!("viewport_callback"); profiling::scope!("viewport_callback");
viewport_ui_cb(ui); viewport_ui_cb(ui);
} else { })
profiling::scope!("App::ui"); } else {
app.ui(ui, &mut self.frame); 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 { if is_root_viewport && close_requested {
let canceled = full_output.viewport_output[&ViewportId::ROOT] let canceled = full_output
.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
.viewport_commands .viewport_commands
.get(&ViewportId::ROOT) .get(&ViewportId::ROOT)
.is_some_and(|commands| commands.contains(&egui::ViewportCommand::CancelClose)); .is_some_and(|commands| commands.contains(&egui::ViewportCommand::CancelClose));
self.handle_close_request(canceled); self.handle_close_request(canceled);
} }
logic_output self.pending_full_output.append(full_output);
} std::mem::take(&mut self.pending_full_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) { fn handle_close_request(&mut self, canceled: bool) {

View File

@@ -40,7 +40,7 @@ use super::{
use crate::epaint::textures::TexturesDelta; use crate::epaint::textures::TexturesDelta;
use crate::{ use crate::{
App, AppCreator, CreationContext, NativeOptions, Result, Storage, 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) (raw_input, viewport_ui_cb, is_visible, show_ui)
}; };
if !show_ui { if !show_ui && viewport_ui_cb.is_some() {
// Nothing will be shown, so we run no egui pass at all. // A hidden child viewport: it has no app logic to tick, and nothing to show,
// That way all ui state is left untouched, and is still there // so there is nothing to do. We run no egui pass at all, so all its ui state
// when this viewport becomes visible again. // is left untouched, and is still there when it 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); self.sleep_if_minimized(viewport_id);
return Ok(if self.integration.should_close() { return Ok(if self.integration.should_close() {
@@ -700,9 +674,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,
show_ui,
);
// ------------------------------------------------------------ // ------------------------------------------------------------
@@ -720,14 +697,13 @@ impl GlowWinitRunning<'_> {
let egui::FullOutput { let egui::FullOutput {
platform_output, platform_output,
textures_delta, viewport_commands,
shapes, pass_output,
pixels_per_point,
viewport_output,
} = full_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 { let GlutinWindowContext {
viewports, viewports,
@@ -742,108 +718,113 @@ impl GlowWinitRunning<'_> {
viewport.info.events.clear(); // they should have been processed viewport.info.events.clear(); // they should have been processed
let window = viewport.window.clone().unwrap(); let window = viewport.window.clone().unwrap();
let gl_surface = viewport.gl_surface.as_ref().unwrap();
let egui_winit = viewport.egui_winit.as_mut().unwrap(); let egui_winit = viewport.egui_winit.as_mut().unwrap();
egui_winit.handle_platform_output_with_event_loop(&window, event_loop, platform_output); egui_winit.handle_platform_output_with_event_loop(&window, event_loop, platform_output);
if is_visible { if let Some(pass_output) = pass_output {
let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point); let egui::PassOutput {
textures_delta,
{ shapes,
// 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, pixels_per_point,
&clipped_primitives, viewport_output,
pending_deltas, } = pass_output;
); pending_deltas.append(textures_delta);
{ if is_visible {
for action in viewport.actions_requested.drain(..) { let gl_surface = viewport.gl_surface.as_ref().unwrap();
match action { let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point);
ActionRequested::Screenshot(user_data) => {
let screenshot = painter.read_screen_rgba(screen_size_in_pixels); {
egui_winit // We may need to switch contexts again, because of immediate viewports:
.egui_input_mut() frame_timer.pause();
.events change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
.push(egui::Event::Screenshot { frame_timer.resume();
viewport_id, }
user_data,
image: screenshot.into(), let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
});
} if !clear_before_update {
ActionRequested::Cut => { painter.clear(screen_size_in_pixels, clear_color);
egui_winit.egui_input_mut().events.push(egui::Event::Cut); }
}
ActionRequested::Copy => { painter.paint_and_update_textures(
egui_winit.egui_input_mut().events.push(egui::Event::Copy); screen_size_in_pixels,
} pixels_per_point,
ActionRequested::Paste => { &clipped_primitives,
if let Some(contents) = egui_winit.clipboard_text() { pending_deltas,
let contents = contents.replace("\r\n", "\n"); );
if !contents.is_empty() {
egui_winit {
.egui_input_mut() for action in viewport.actions_requested.drain(..) {
.events match action {
.push(egui::Event::Paste(contents)); 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);
}
} }
{ glutin.handle_viewport_output(event_loop, &viewport_output);
// 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, &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.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)); integration.maybe_autosave(app.as_mut(), Some(&window));
if is_invisible_or_minimized(&window) { sleep_if_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 integration.should_close() { if integration.should_close() {
Ok(EventResult::CloseRequested) 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) { fn sleep_if_minimized(&self, viewport_id: ViewportId) {
let glutin = self.glutin.borrow(); let glutin = self.glutin.borrow();
if let Some(viewport) = glutin.viewports.get(&viewport_id) if let Some(viewport) = glutin.viewports.get(&viewport_id)
&& let Some(window) = viewport.window.as_ref() && let Some(window) = viewport.window.as_ref()
&& is_invisible_or_minimized(window)
{ {
profiling::scope!("minimized_sleep"); sleep_if_invisible_or_minimized(window);
std::thread::sleep(std::time::Duration::from_millis(10));
} }
} }
@@ -1419,69 +1393,38 @@ impl GlutinWindowContext {
/// Apply commands to already existing viewports, without creating or removing any. /// Apply commands to already existing viewports, without creating or removing any.
/// ///
/// This is for commands that came out of [`egui::Context::run_logic`], /// Which viewports should exist is decided by [`Self::handle_viewport_output`];
/// which knows nothing about which viewports should exist. /// 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( fn handle_viewport_commands(
&mut self, &mut self,
egui_ctx: &egui::Context, egui_ctx: &egui::Context,
viewport_commands: egui::OrderedViewportIdMap<Vec<egui::ViewportCommand>>, mut viewport_commands: egui::OrderedViewportIdMap<Vec<egui::ViewportCommand>>,
) { ) {
profiling::function_scope!(); 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 { let Some(viewport) = self.viewports.get_mut(&viewport_id) else {
continue; continue;
}; };
viewport.deferred_commands.append(&mut commands); if let Some(mut commands) = viewport_commands.remove(&viewport_id) {
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( if viewport.deferred_commands.is_empty() {
&mut self, continue;
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 let Some(window) = &viewport.window { if let Some(window) = &viewport.window {
let old_inner_size = window.inner_size(); let old_inner_size = window.inner_size();
viewport.deferred_commands.append(&mut commands);
egui_winit::process_viewport_commands( egui_winit::process_viewport_commands(
egui_ctx, egui_ctx,
&mut viewport.info, &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: // Create windows for any new viewports:
self.initialize_all_windows(event_loop); self.initialize_all_windows(event_loop);
@@ -1663,13 +1631,17 @@ fn render_immediate_viewport(
let egui::FullOutput { let egui::FullOutput {
platform_output, platform_output,
viewport_commands,
pass_output,
} = egui_ctx.run_ui(input, |ui| {
viewport_ui_cb(ui);
});
let egui::PassOutput {
textures_delta, textures_delta,
shapes, shapes,
pixels_per_point, pixels_per_point,
viewport_output, viewport_output,
} = egui_ctx.run_ui(input, |ui| { } = pass_output.expect("run_ui always runs a pass");
viewport_ui_cb(ui);
});
// --------------------------------------------------- // ---------------------------------------------------
@@ -1737,8 +1709,9 @@ fn render_immediate_viewport(
egui_winit.handle_platform_output(window, platform_output); egui_winit.handle_platform_output(window, platform_output);
event_loop_context::with_current_event_loop(|event_loop| { 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")] #[cfg(feature = "__screenshot")]

View File

@@ -30,7 +30,7 @@ use crate::{
App, AppCreator, CreationContext, NativeOptions, Result, Storage, App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::{ native::{
epi_integration::EpiIntegration, 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) (viewport_ui_cb, raw_input, is_visible, show_ui)
}; };
if !show_ui { if !show_ui && viewport_ui_cb.is_some() {
// Nothing will be shown, so we run no egui pass at all. // A hidden child viewport: it has no app logic to tick, and nothing to show,
// That way all ui state is left untouched, and is still there // so there is nothing to do. We run no egui pass at all, so all its ui state
// when this viewport becomes visible again. // is left untouched, and is still there when it 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); sleep_if_minimized(&shared.borrow(), viewport_id);
return Ok(if integration.should_close() { return Ok(if integration.should_close() {
@@ -742,7 +712,8 @@ impl WgpuWinitRunning<'_> {
// 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, show_ui);
// ------------------------------------------------------------ // ------------------------------------------------------------
@@ -758,15 +729,18 @@ impl WgpuWinitRunning<'_> {
let FullOutput { let FullOutput {
platform_output, platform_output,
textures_delta, viewport_commands,
shapes, pass_output,
pixels_per_point,
viewport_output,
} = full_output; } = full_output;
pending_deltas.append(textures_delta); if let Some(pass_output) = &pass_output {
remove_viewports_not_in(
remove_viewports_not_in(viewports, painter, viewport_from_window, &viewport_output); viewports,
painter,
viewport_from_window,
&pass_output.viewport_output,
);
}
let Some(viewport) = viewports.get_mut(&viewport_id) else { let Some(viewport) = viewports.get_mut(&viewport_id) else {
return Ok(EventResult::Wait); return Ok(EventResult::Wait);
@@ -785,74 +759,79 @@ impl WgpuWinitRunning<'_> {
egui_winit.handle_platform_output_with_event_loop(window, event_loop, platform_output); egui_winit.handle_platform_output_with_event_loop(window, event_loop, platform_output);
let vsync_secs = if is_visible { let mut vsync_secs = 0.0;
let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point);
let mut screenshot_commands = vec![]; if let Some(pass_output) = pass_output {
viewport.actions_requested.retain(|cmd| { let egui::PassOutput {
if let ActionRequested::Screenshot(info) = cmd { textures_delta,
screenshot_commands.push(info.clone()); shapes,
false
} else {
true
}
});
let vsync_secs = painter.paint_and_update_textures(
viewport_id,
pixels_per_point, pixels_per_point,
app.clear_color(&egui_ctx.global_style().visuals), viewport_output,
&clipped_primitives, } = pass_output;
pending_deltas,
screenshot_commands,
window,
);
for action in viewport.actions_requested.drain(..) { pending_deltas.append(textures_delta);
match action {
ActionRequested::Screenshot { .. } => { if is_visible {
// already handled above 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); vsync_secs = painter.paint_and_update_textures(
} viewport_id,
ActionRequested::Copy => { pixels_per_point,
egui_winit.egui_input_mut().events.push(egui::Event::Copy); app.clear_color(&egui_ctx.global_style().visuals),
} &clipped_primitives,
ActionRequested::Paste => { pending_deltas,
if let Some(contents) = egui_winit.clipboard_text() { screenshot_commands,
let contents = contents.replace("\r\n", "\n"); window,
if !contents.is_empty() { );
egui_winit
.egui_input_mut() for action in viewport.actions_requested.drain(..) {
.events match action {
.push(egui::Event::Paste(contents)); 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 handle_viewport_output(&viewport_output, viewports, painter, viewport_from_window);
} else {
0.0
};
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( handle_viewport_commands(&integration.egui_ctx, viewport_commands, viewports, painter);
&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);
let window = viewport_from_window let window = viewport_from_window
.get(&window_id) .get(&window_id)
@@ -863,15 +842,8 @@ impl WgpuWinitRunning<'_> {
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref())); integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
if let Some(window) = window if let Some(window) = window {
&& is_invisible_or_minimized(window) sleep_if_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 integration.should_close() { if integration.should_close() {
@@ -1173,13 +1145,17 @@ fn render_immediate_viewport(
// Make sure no locks are held during this call. // Make sure no locks are held during this call.
let egui::FullOutput { let egui::FullOutput {
platform_output, platform_output,
viewport_commands,
pass_output,
} = egui_ctx.run_ui(input, |ui| {
viewport_ui_cb(ui);
});
let egui::PassOutput {
textures_delta, textures_delta,
shapes, shapes,
pixels_per_point, pixels_per_point,
viewport_output, viewport_output,
} = egui_ctx.run_ui(input, |ui| { } = pass_output.expect("run_ui always runs a pass");
viewport_ui_cb(ui);
});
// ------------------------------------------ // ------------------------------------------
@@ -1226,13 +1202,8 @@ fn render_immediate_viewport(
egui_winit.handle_platform_output(window, platform_output); egui_winit.handle_platform_output(window, platform_output);
handle_viewport_output( handle_viewport_output(&viewport_output, viewports, painter, viewport_from_window);
&egui_ctx, handle_viewport_commands(&egui_ctx, viewport_commands, viewports, painter);
&viewport_output,
viewports,
painter,
viewport_from_window,
);
} }
pub(crate) fn remove_viewports_not_in( pub(crate) fn remove_viewports_not_in(
@@ -1249,81 +1220,41 @@ pub(crate) fn remove_viewports_not_in(
painter.gc_viewports(&active_viewports_ids); painter.gc_viewports(&active_viewports_ids);
} }
/// Add new viewports, and update existing ones:
/// Apply commands to already existing viewports, without creating or removing any. /// Apply commands to already existing viewports, without creating or removing any.
/// ///
/// This is for commands that came out of [`egui::Context::run_logic`], /// Which viewports should exist is decided by [`handle_viewport_output`];
/// which knows nothing about which viewports should exist. /// 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( fn handle_viewport_commands(
egui_ctx: &egui::Context, egui_ctx: &egui::Context,
viewport_commands: egui::OrderedViewportIdMap<Vec<egui::ViewportCommand>>, mut viewport_commands: egui::OrderedViewportIdMap<Vec<egui::ViewportCommand>>,
viewports: &mut Viewports, viewports: &mut Viewports,
painter: &mut egui_wgpu::winit::Painter,
) { ) {
profiling::function_scope!(); 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 { let Some(viewport) = viewports.get_mut(&viewport_id) else {
continue; continue;
}; };
viewport.deferred_commands.append(&mut commands); if let Some(mut commands) = viewport_commands.remove(&viewport_id) {
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: if viewport.deferred_commands.is_empty() {
/// <https://github.com/emilk/egui/issues/325> continue;
/// }
/// 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 let Some(window) = viewport.window.as_ref() { if let Some(window) = viewport.window.as_ref() {
let old_inner_size = window.inner_size(); let old_inner_size = window.inner_size();
viewport.deferred_commands.append(&mut commands);
egui_winit::process_viewport_commands( egui_winit::process_viewport_commands(
egui_ctx, egui_ctx,
&mut viewport.info, &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); 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) 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. /// Create an egui context, restoring it from storage if possible.
pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context { pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context {
profiling::function_scope!(); profiling::function_scope!();

View File

@@ -280,52 +280,41 @@ impl AppRunner {
.and_then(|v| v.visible()) .and_then(|v| v.visible())
.unwrap_or(true); .unwrap_or(true);
if !is_visible { // When the tab is hidden (`!is_visible`), we run no egui pass at all.
// The tab is hidden, so we run no egui pass at all. // That way all ui state is left untouched, and is still there
// That way all ui state is left untouched, and is still there // when the tab is shown again.
// 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);
});
let egui::FullOutput { let egui::FullOutput {
platform_output, 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, textures_delta,
shapes, shapes,
pixels_per_point, pixels_per_point,
viewport_output, 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 { self.textures_delta.append(textures_delta);
log::warn!("Multiple viewports not yet supported on the web"); 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>) { fn handle_viewport_commands(&mut self, commands: impl Iterator<Item = ViewportCommand>) {

View File

@@ -17,10 +17,10 @@ use epaint::{
use crate::{ use crate::{
Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport, Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport,
ImmediateViewportRendererCallback, Key, KeyboardShortcut, Label, LayerId, Memory, ImmediateViewportRendererCallback, Key, KeyboardShortcut, Label, LayerId, Memory,
ModifierNames, Modifiers, NumExt as _, Order, Painter, RawInput, Response, RichText, ModifierNames, Modifiers, NumExt as _, Order, OrderedViewportIdMap, Painter, RawInput,
SafeAreaInsets, ScrollArea, Sense, Style, TextStyle, TextureHandle, TextureOptions, Ui, Response, RichText, SafeAreaInsets, ScrollArea, Sense, Style, TextStyle, TextureHandle,
UiBuilder, ViewportBuilder, ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair, TextureOptions, Ui, UiBuilder, ViewportBuilder, ViewportCommand, ViewportId, ViewportIdMap,
ViewportIdSet, ViewportOutput, Visuals, Widget as _, WidgetRect, WidgetText, ViewportIdPair, ViewportIdSet, ViewportOutput, Visuals, Widget as _, WidgetRect, WidgetText,
animation_manager::AnimationManager, animation_manager::AnimationManager,
containers::{self, area::AreaState}, containers::{self, area::AreaState},
data::output::PlatformOutput, data::output::PlatformOutput,
@@ -32,7 +32,7 @@ use crate::{
load::{self, Bytes, Loaders, SizedTexture}, load::{self, Bytes, Loaders, SizedTexture},
memory::{Options, Theme}, memory::{Options, Theme},
os::OperatingSystem, os::OperatingSystem,
output::{FullOutput, LogicOutput}, output::{FullOutput, PassOutput},
pass_state::PassState, pass_state::PassState,
plugin::{self, TypedPluginHandle}, plugin::{self, TypedPluginHandle},
resize, response, scroll_area, resize, response, scroll_area,
@@ -240,6 +240,12 @@ pub struct ViewportState {
pub output: PlatformOutput, pub output: PlatformOutput,
pub commands: Vec<ViewportCommand>, pub commands: Vec<ViewportCommand>,
/// Input that was given to [`Context::run_frame`] while no pass ran.
///
/// No pass consumed it, so we keep it and prepend it to the input
/// of the next pass.
pending_raw_input: RawInput,
// ---------------------- // ----------------------
// Cross-frame statistics: // Cross-frame statistics:
pub num_multipass_in_row: usize, pub num_multipass_in_row: usize,
@@ -414,8 +420,35 @@ struct ContextImpl {
} }
impl ContextImpl { impl ContextImpl {
fn begin_pass(&mut self, mut new_raw_input: RawInput) { /// Apply the window-related parts of new input ([`RawInput::viewports`] and
/// [`RawInput::focused`]) to the stored input, without disturbing the rest
/// (events, time, …).
///
/// This lets app logic see the current state of the windows
/// before (or without) a pass consuming the full input.
fn ingest_window_state(&mut self, new_input: &RawInput) {
let raw = &mut self.viewport_for(new_input.viewport_id).input.raw;
raw.viewport_id = new_input.viewport_id;
raw.viewports = new_input.viewports.clone();
raw.focused = new_input.focused;
}
fn begin_pass(&mut self, new_raw_input: RawInput) {
let viewport_id = new_raw_input.viewport_id; let viewport_id = new_raw_input.viewport_id;
// Prepend any input that [`Context::run_frame`] received while no pass ran:
let mut new_raw_input = {
let mut pending = std::mem::take(
&mut self
.viewports
.entry(viewport_id)
.or_default()
.pending_raw_input,
);
pending.append(new_raw_input); // The new input wins where they overlap
pending
};
let parent_id = new_raw_input let parent_id = new_raw_input
.viewports .viewports
.get(&viewport_id) .get(&viewport_id)
@@ -714,8 +747,9 @@ impl ContextImpl {
/// }); /// });
/// }); /// });
/// handle_platform_output(full_output.platform_output); /// handle_platform_output(full_output.platform_output);
/// let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point); /// let pass_output = full_output.pass_output.expect("run_ui always runs a pass");
/// paint(full_output.textures_delta, clipped_primitives); /// let clipped_primitives = ctx.tessellate(pass_output.shapes, pass_output.pixels_per_point);
/// paint(pass_output.textures_delta, clipped_primitives);
/// } /// }
/// ``` /// ```
#[derive(Clone)] #[derive(Clone)]
@@ -753,6 +787,23 @@ impl Default for Context {
} }
} }
/// One phase of an app frame; see [`Context::run_frame`].
pub enum FramePhase<'a> {
/// Tick the app logic.
///
/// This happens exactly once per frame, before any pass,
/// so it must not show any ui.
Logic(&'a Context),
/// Show the ui.
///
/// This happens once per pass:
/// not at all when nothing will be shown,
/// and more than once per frame during multi-pass layout
/// (see [`Context::request_discard`]).
Ui(&'a mut Ui),
}
impl Context { impl Context {
/// Do read-only (shared access) transaction on Context /// Do read-only (shared access) transaction on Context
fn read<R>(&self, reader: impl FnOnce(&ContextImpl) -> R) -> R { fn read<R>(&self, reader: impl FnOnce(&ContextImpl) -> R) -> R {
@@ -888,53 +939,101 @@ impl Context {
output output
} }
/// Run app logic without showing any ui. /// Run one frame of an app that separates its logic from its ui.
/// ///
/// Use this instead of [`Self::run_ui`] when nothing will be shown, /// A frame consists of one [`FramePhase::Logic`],
/// e.g. because the window is minimized or occluded, /// followed by one [`FramePhase::Ui`] per pass —
/// but you still want to let the app tick its logic /// or by no ui phase at all if `show_ui` is `false`,
/// (so that it can e.g. ask to be shown again with [`ViewportCommand::Focus`]). /// e.g. because the window is minimized or occluded.
/// ///
/// No pass is run, so `f` must not show any ui. /// The logic phase runs outside of any pass. It always sees the current state of the
/// This means everything egui knows about the ui is left untouched: /// windows ([`InputState::viewport`]), so it can e.g. tell that the window is hidden,
/// and ask for it to be shown again with [`ViewportCommand::Focus`].
/// The rest of the input (events, time, …) is not applied until the first pass,
/// so during the logic phase it is still that of the previous pass.
///
/// When no pass is run, everything egui knows about the ui is left untouched:
/// no widget state is garbage-collected, no animation advances, /// no widget state is garbage-collected, no animation advances,
/// and nothing loses focus. /// and nothing loses focus.
/// /// egui keeps `new_input` and feeds it to the first pass of a later frame,
/// Of `new_input`, only [`RawInput::viewports`] is used: `f` can learn about the state of /// so no input is lost while nothing is shown.
/// the windows with [`InputState::viewport`], but the ui input (events, time, …) /// The returned [`FullOutput`] then has no [`FullOutput::pass_output`]:
/// is left as it was, and should be given to the next call to [`Self::run_ui`]. /// there is nothing to paint, and the integration should leave its windows as they are.
///
/// The returned [`LogicOutput`] is what [`FullOutput`] would have carried:
/// anything `f` asked the integration to do.
/// There is nothing to paint.
#[must_use] #[must_use]
pub fn run_logic(&self, new_input: &RawInput, f: impl FnOnce(&Self)) -> LogicOutput { pub fn run_frame(
&self,
new_input: RawInput,
show_ui: bool,
mut frame: impl FnMut(FramePhase<'_>),
) -> FullOutput {
self.run_frame_dyn(new_input, show_ui, &mut frame)
}
#[must_use]
fn run_frame_dyn(
&self,
new_input: RawInput,
show_ui: bool,
frame: &mut dyn FnMut(FramePhase<'_>),
) -> FullOutput {
profiling::function_scope!(); profiling::function_scope!();
let viewport_id = new_input.viewport_id; let viewport_id = new_input.viewport_id;
self.write(|ctx| { self.write(|ctx| {
// Consume any outstanding repaint request, so that a new request from `f` if !show_ui {
// reaches the integration instead of being considered already served: // No pass will consume the outstanding repaint request, so consume it here,
ctx.begin_pass_repaint_logic(viewport_id); // so that a new request from the app logic reaches the integration
// instead of being considered already served:
ctx.begin_pass_repaint_logic(viewport_id);
}
// Tell the app about the windows, but leave the ui input alone: // Let the app logic see the current state of the windows:
let raw = &mut ctx.viewport_for(viewport_id).input.raw; ctx.ingest_window_state(&new_input);
raw.viewport_id = viewport_id;
raw.viewports = new_input.viewports.clone();
raw.focused = new_input.focused;
}); });
f(self); frame(FramePhase::Logic(self));
self.write(|ctx| LogicOutput { if show_ui {
platform_output: std::mem::take(&mut ctx.viewport_for(viewport_id).output), // Anything the logic phase asked for (viewport commands etc)
viewport_commands: ctx // is still in the `Context`, and will come out of the pass we are about to run.
.viewports self.run_ui_dyn(new_input, &mut |ui| frame(FramePhase::Ui(ui)))
.iter_mut() } else {
.filter(|(_, viewport)| !viewport.commands.is_empty()) // The plugins see the input of every frame exactly once,
.map(|(&id, viewport)| (id, std::mem::take(&mut viewport.commands))) // so it must happen now, before we buffer it:
.collect(), let mut new_input = new_input;
let plugins = self.read(|ctx| ctx.plugins.ordered_plugins());
plugins.on_input(self, &mut new_input);
self.write(|ctx| {
// No pass will consume the input, so keep it for the next one:
ctx.viewport_for(viewport_id)
.pending_raw_input
.append(new_input);
FullOutput {
platform_output: std::mem::take(&mut ctx.viewport_for(viewport_id).output),
viewport_commands: ctx.take_viewport_commands(),
pass_output: None,
}
})
}
}
/// Run app logic without showing any ui.
///
/// This is [`Self::run_frame`] without a ui phase.
/// Use it 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`]).
#[must_use]
pub fn run_logic(&self, new_input: RawInput, f: impl FnOnce(&Self)) -> FullOutput {
let mut f = Some(f);
self.run_frame(new_input, false, |phase| {
if let (FramePhase::Logic(ctx), Some(f)) = (phase, f.take()) {
f(ctx);
}
}) })
} }
@@ -2792,19 +2891,20 @@ impl ContextImpl {
// just the top _immediate_ viewport. // just the top _immediate_ viewport.
let is_last = self.viewport_stack.is_empty(); let is_last = self.viewport_stack.is_empty();
let viewport_commands = if is_last {
// Let the primary immediate viewport handle the commands of its children too.
// This can make things easier for the backend, as otherwise we may get commands
// that affect a viewport while its egui logic is running.
self.take_viewport_commands()
} else {
Default::default()
};
let viewport_output = self let viewport_output = self
.viewports .viewports
.iter_mut() .iter_mut()
.map(|(&id, viewport)| { .map(|(&id, viewport)| {
let parent = *self.viewport_parents.entry(id).or_default(); let parent = *self.viewport_parents.entry(id).or_default();
let commands = if is_last {
// Let the primary immediate viewport handle the commands of its children too.
// This can make things easier for the backend, as otherwise we may get commands
// that affect a viewport while its egui logic is running.
std::mem::take(&mut viewport.commands)
} else {
vec![]
};
( (
id, id,
@@ -2813,7 +2913,6 @@ impl ContextImpl {
class: viewport.class, class: viewport.class,
builder: viewport.builder.clone(), builder: viewport.builder.clone(),
viewport_ui_cb: viewport.viewport_ui_cb.clone(), viewport_ui_cb: viewport.viewport_ui_cb.clone(),
commands,
repaint_delay: viewport.repaint.repaint_delay, repaint_delay: viewport.repaint.repaint_delay,
}, },
) )
@@ -2838,12 +2937,24 @@ impl ContextImpl {
FullOutput { FullOutput {
platform_output, platform_output,
textures_delta, viewport_commands,
shapes, pass_output: Some(PassOutput {
pixels_per_point, textures_delta,
viewport_output, shapes,
pixels_per_point,
viewport_output,
}),
} }
} }
/// Take all outstanding [`ViewportCommand`]s, keyed by the viewport they apply to.
fn take_viewport_commands(&mut self) -> OrderedViewportIdMap<Vec<ViewportCommand>> {
self.viewports
.iter_mut()
.filter(|(_, viewport)| !viewport.commands.is_empty())
.map(|(&id, viewport)| (id, std::mem::take(&mut viewport.commands)))
.collect()
}
} }
impl Context { impl Context {

View File

@@ -6,7 +6,7 @@ use epaint::text::CharIndex;
use crate::{OrderedViewportIdMap, RepaintCause, ViewportOutput, WidgetType}; use crate::{OrderedViewportIdMap, RepaintCause, ViewportOutput, WidgetType};
/// What egui emits each frame from [`crate::Context::run_ui`]. /// What egui emits each frame from [`crate::Context::run_frame`] and friends.
/// ///
/// The backend should use this. /// The backend should use this.
#[derive(Clone, Default)] #[derive(Clone, Default)]
@@ -14,6 +14,72 @@ pub struct FullOutput {
/// Non-rendering related output. /// Non-rendering related output.
pub platform_output: PlatformOutput, pub platform_output: PlatformOutput,
/// The commands sent with [`crate::Context::send_viewport_cmd`] and friends.
///
/// These are one-shot commands (e.g. "focus this window"),
/// and say nothing about which viewports should exist.
pub viewport_commands: OrderedViewportIdMap<Vec<crate::ViewportCommand>>,
/// What the egui pass(es) produced: things to paint, and which viewports should exist.
///
/// This is `None` if no pass was run
/// (i.e. the output came from [`crate::Context::run_logic`],
/// because nothing was going to be shown).
/// If so, all ui state was left untouched:
/// the integration should paint nothing and leave its viewports as they are.
pub pass_output: Option<PassOutput>,
}
impl FullOutput {
/// Add on new output.
pub fn append(&mut self, newer: Self) {
let Self {
platform_output,
viewport_commands,
pass_output,
} = newer;
self.platform_output.append(platform_output);
for (id, mut commands) in viewport_commands {
self.viewport_commands
.entry(id)
.or_default()
.append(&mut commands);
}
match (&mut self.pass_output, pass_output) {
(Some(old), Some(new)) => old.append(new),
(old @ None, new) => *old = new,
(Some(_), None) => {}
}
}
/// The output of the egui pass(es), if any was run.
///
/// Panics if no pass was run.
/// This cannot happen for the output of [`crate::Context::run_ui`],
/// which always runs at least one pass.
#[track_caller]
pub fn expect_pass(&self) -> &PassOutput {
self.pass_output.as_ref().expect("No egui pass was run")
}
/// [`epaint::textures::TexturesDelta`] will panic when dropped with still unapplied deltas,
/// this is a helper to clear the deltas.
pub fn drop_without_applying_deltas(mut self) {
if let Some(pass_output) = &mut self.pass_output {
pass_output.textures_delta.clear();
}
}
}
/// What the egui pass(es) of one frame produced: things to paint, and which viewports should exist.
///
/// This is the part of [`FullOutput`] that only a real egui pass can produce,
/// as opposed to the parts that app logic can also produce (see [`crate::Context::run_logic`]).
#[derive(Clone, Default)]
pub struct PassOutput {
/// Texture changes since last frame (including the font texture). /// Texture changes since last frame (including the font texture).
/// ///
/// The backend needs to apply [`crate::TexturesDelta::push`] _before_ painting, /// The backend needs to apply [`crate::TexturesDelta::push`] _before_ painting,
@@ -39,20 +105,18 @@ pub struct FullOutput {
pub viewport_output: OrderedViewportIdMap<ViewportOutput>, pub viewport_output: OrderedViewportIdMap<ViewportOutput>,
} }
impl FullOutput { impl PassOutput {
/// Add on new output. /// Add on new output.
pub fn append(&mut self, newer: Self) { pub fn append(&mut self, newer: Self) {
use std::collections::btree_map::Entry; use std::collections::btree_map::Entry;
let Self { let Self {
platform_output,
textures_delta, textures_delta,
shapes, shapes,
pixels_per_point, pixels_per_point,
viewport_output, viewport_output,
} = newer; } = newer;
self.platform_output.append(platform_output);
self.textures_delta.append(textures_delta); self.textures_delta.append(textures_delta);
self.shapes = shapes; // Only paint the latest self.shapes = shapes; // Only paint the latest
self.pixels_per_point = pixels_per_point; // Use latest self.pixels_per_point = pixels_per_point; // Use latest
@@ -68,28 +132,6 @@ impl FullOutput {
} }
} }
} }
/// [`epaint::textures::TexturesDelta`] will panic when dropped with still unapplied deltas,
/// this is a helper to clear the deltas.
pub fn drop_without_applying_deltas(mut self) {
self.textures_delta.clear();
}
}
/// 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<Vec<crate::ViewportCommand>>,
} }
/// Information about text being edited. /// Information about text being edited.

View File

@@ -121,8 +121,9 @@
//! }); //! });
//! }); //! });
//! handle_platform_output(full_output.platform_output); //! handle_platform_output(full_output.platform_output);
//! let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point); //! let pass_output = full_output.pass_output.expect("run_ui always runs a pass");
//! paint(full_output.textures_delta, clipped_primitives); //! let clipped_primitives = ctx.tessellate(pass_output.shapes, pass_output.pixels_per_point);
//! paint(pass_output.textures_delta, clipped_primitives);
//! } //! }
//! ``` //! ```
//! //!
@@ -462,12 +463,12 @@ pub mod text {
pub use self::{ pub use self::{
atomics::*, atomics::*,
containers::{menu::MenuBar, *}, containers::{menu::MenuBar, *},
context::{Context, RepaintCause, RequestRepaintInfo}, context::{Context, FramePhase, RepaintCause, RequestRepaintInfo},
data::{ data::{
Key, UserData, Key, UserData,
input::*, input::*,
output::{ output::{
self, CursorIcon, CustomCursorImage, FullOutput, LogicOutput, OpenUrl, OutputCommand, self, CursorIcon, CustomCursorImage, FullOutput, OpenUrl, OutputCommand, PassOutput,
PlatformOutput, UserAttentionType, WidgetInfo, PlatformOutput, UserAttentionType, WidgetInfo,
}, },
}, },

View File

@@ -1266,9 +1266,6 @@ pub struct ViewportOutput {
/// `None` for immediate viewports and the ROOT viewport. /// `None` for immediate viewports and the ROOT viewport.
pub viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>, pub viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
/// Commands to change the viewport, e.g. window title and size.
pub commands: Vec<ViewportCommand>,
/// Schedule a repaint of this viewport after this delay. /// Schedule a repaint of this viewport after this delay.
/// ///
/// It is preferable to instead install a [`Context::set_request_repaint_callback`], /// It is preferable to instead install a [`Context::set_request_repaint_callback`],
@@ -1286,7 +1283,6 @@ impl ViewportOutput {
class, class,
builder, builder,
viewport_ui_cb, viewport_ui_cb,
mut commands,
repaint_delay, repaint_delay,
} = newer; } = newer;
@@ -1294,7 +1290,6 @@ impl ViewportOutput {
self.class = class; self.class = class;
let _ = self.builder.patch(builder); // we ignore the returned command, because `self.builder` will be the basis of a new patch let _ = self.builder.patch(builder); // we ignore the returned command, because `self.builder` will be the basis of a new patch
self.viewport_ui_cb = viewport_ui_cb; self.viewport_ui_cb = viewport_ui_cb;
self.commands.append(&mut commands);
self.repaint_delay = self.repaint_delay.min(repaint_delay); self.repaint_delay = self.repaint_delay.min(repaint_delay);
} }
} }

View File

@@ -28,12 +28,13 @@ pub fn criterion_benchmark(c: &mut Criterion) {
// The most end-to-end benchmark. // The most end-to-end benchmark.
c.bench_function("demo_with_tessellate__realistic", |b| { c.bench_function("demo_with_tessellate__realistic", |b| {
b.iter(|| { b.iter(|| {
let mut full_output = ctx.run_ui(RawInput::default(), |ui| { let full_output = ctx.run_ui(RawInput::default(), |ui| {
demo_windows.ui(ui); demo_windows.ui(ui);
}); });
ctx.tessellate(full_output.shapes, full_output.pixels_per_point); let mut pass_output = full_output.pass_output.expect("run_ui always runs a pass");
ctx.tessellate(pass_output.shapes, pass_output.pixels_per_point);
full_output.textures_delta.clear(); // Don't panic on drop with unapplied deltas pass_output.textures_delta.clear(); // Don't panic on drop with unapplied deltas
}); });
}); });
@@ -50,7 +51,10 @@ pub fn criterion_benchmark(c: &mut Criterion) {
demo_windows.ui(ui); demo_windows.ui(ui);
}); });
c.bench_function("demo_only_tessellate", |b| { c.bench_function("demo_only_tessellate", |b| {
b.iter(|| ctx.tessellate(full_output.shapes.clone(), full_output.pixels_per_point)); b.iter(|| {
let pass_output = full_output.expect_pass();
ctx.tessellate(pass_output.shapes.clone(), pass_output.pixels_per_point)
});
}); });
full_output.drop_without_applying_deltas(); full_output.drop_without_applying_deltas();
} }

View File

@@ -72,12 +72,13 @@ fn test_egui_e2e() {
const NUM_FRAMES: usize = 5; const NUM_FRAMES: usize = 5;
for _ in 0..NUM_FRAMES { for _ in 0..NUM_FRAMES {
let mut full_output = ctx.run_ui(raw_input.clone(), |ui| { let full_output = ctx.run_ui(raw_input.clone(), |ui| {
demo_windows.ui(ui); demo_windows.ui(ui);
}); });
let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point); let mut pass_output = full_output.pass_output.expect("run_ui always runs a pass");
let clipped_primitives = ctx.tessellate(pass_output.shapes, pass_output.pixels_per_point);
assert!(!clipped_primitives.is_empty()); assert!(!clipped_primitives.is_empty());
full_output.textures_delta.clear(); // Don't panic on drop with unapplied deltas pass_output.textures_delta.clear(); // Don't panic on drop with unapplied deltas
} }
} }
@@ -92,16 +93,17 @@ fn test_egui_zero_window_size() {
const NUM_FRAMES: usize = 5; const NUM_FRAMES: usize = 5;
for _ in 0..NUM_FRAMES { for _ in 0..NUM_FRAMES {
let mut full_output = ctx.run_ui(raw_input.clone(), |ui| { let full_output = ctx.run_ui(raw_input.clone(), |ui| {
demo_windows.ui(ui); demo_windows.ui(ui);
}); });
let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point); let mut pass_output = full_output.pass_output.expect("run_ui always runs a pass");
let clipped_primitives = ctx.tessellate(pass_output.shapes, pass_output.pixels_per_point);
assert!( assert!(
clipped_primitives.is_empty(), clipped_primitives.is_empty(),
"There should be nothing to show, has at least one primitive with clip_rect: {:?}", "There should be nothing to show, has at least one primitive with clip_rect: {:?}",
clipped_primitives[0].clip_rect clipped_primitives[0].clip_rect
); );
full_output.textures_delta.clear(); // Don't panic on drop with unapplied deltas pass_output.textures_delta.clear(); // Don't panic on drop with unapplied deltas
} }
} }

View File

@@ -71,16 +71,20 @@ impl EguiGlow {
let egui::FullOutput { let egui::FullOutput {
platform_output, platform_output,
viewport_commands,
pass_output,
} = self.egui_ctx.run_ui(raw_input, run_ui);
let egui::PassOutput {
textures_delta, textures_delta,
shapes, shapes,
pixels_per_point, pixels_per_point,
viewport_output, viewport_output,
} = self.egui_ctx.run_ui(raw_input, run_ui); } = pass_output.expect("run_ui always runs a pass");
if viewport_output.len() > 1 { if viewport_output.len() > 1 {
log::warn!("Multiple viewports not yet supported by EguiGlow"); log::warn!("Multiple viewports not yet supported by EguiGlow");
} }
for (_, ViewportOutput { commands, .. }) in viewport_output { for (_, commands) in viewport_commands {
let mut actions_requested = Default::default(); let mut actions_requested = Default::default();
egui_winit::process_viewport_commands( egui_winit::process_viewport_commands(
&self.egui_ctx, &self.egui_ctx,

View File

@@ -264,10 +264,13 @@ impl egui::Plugin for InspectionPlugin {
return; return;
} }
let immediate_repaint = output let pass_output = output.expect_pass();
let immediate_repaint = pass_output
.viewport_output .viewport_output
.values() .values()
.any(|viewport| viewport.repaint_delay == Duration::ZERO); .any(|viewport| viewport.repaint_delay == Duration::ZERO);
let pixels_per_point = pass_output.pixels_per_point;
let step = self.step; let step = self.step;
self.in_flight self.in_flight
@@ -276,7 +279,7 @@ impl egui::Plugin for InspectionPlugin {
if let Some(reply) = item.reply.take() { if let Some(reply) = item.reply.take() {
reply(Response::Tree { reply(Response::Tree {
step, step,
pixels_per_point: output.pixels_per_point, pixels_per_point,
accesskit: output.platform_output.accesskit_update.clone(), accesskit: output.platform_output.accesskit_update.clone(),
}); });
} }

View File

@@ -147,7 +147,9 @@ impl<'a, State> Harness<'a, State> {
response = app.run(ui, &mut state, false); response = app.run(ui, &mut state, false);
}); });
renderer.handle_delta(&mut output.textures_delta); if let Some(pass_output) = &mut output.pass_output {
renderer.handle_delta(&mut pass_output.textures_delta);
}
let mut harness = Self { let mut harness = Self {
app, app,
@@ -269,7 +271,9 @@ impl<'a, State> Harness<'a, State> {
.take() .take()
.expect("AccessKit was disabled"), .expect("AccessKit was disabled"),
); );
self.renderer.handle_delta(&mut output.textures_delta); if let Some(pass_output) = &mut output.pass_output {
self.renderer.handle_delta(&mut pass_output.textures_delta);
}
self.output = output; self.output = output;
self.handle_viewport_commands(); self.handle_viewport_commands();
@@ -630,7 +634,7 @@ impl<'a, State> Harness<'a, State> {
/// This will add a [`RectShape`] to the output shapes, for the current frame. /// This will add a [`RectShape`] to the output shapes, for the current frame.
/// Will be overwritten on the next call to [`Self::run`]. /// Will be overwritten on the next call to [`Self::run`].
pub fn mask(&mut self, rect: Rect) { pub fn mask(&mut self, rect: Rect) {
self.output.shapes.push(ClippedShape { self.pass_output_mut().shapes.push(ClippedShape {
clip_rect: Rect::EVERYTHING, clip_rect: Rect::EVERYTHING,
shape: Shape::Rect(RectShape::filled(rect, 0.0, Color32::MAGENTA)), shape: Shape::Rect(RectShape::filled(rect, 0.0, Color32::MAGENTA)),
}); });
@@ -652,15 +656,20 @@ impl<'a, State> Harness<'a, State> {
mouse_pos + egui::vec2(8.0, 16.0), mouse_pos + egui::vec2(8.0, 16.0),
]; ];
output.shapes.push(ClippedShape { output
clip_rect: self.ctx.content_rect(), .pass_output
shape: egui::epaint::PathShape::convex_polygon( .as_mut()
triangle, .expect("Harness always runs a pass")
Color32::WHITE, .shapes
egui::Stroke::new(1.0, Color32::BLACK), .push(ClippedShape {
) clip_rect: self.ctx.content_rect(),
.into(), shape: egui::epaint::PathShape::convex_polygon(
}); triangle,
Color32::WHITE,
egui::Stroke::new(1.0, Color32::BLACK),
)
.into(),
});
} }
self.renderer.render(&self.ctx, &output) self.renderer.render(&self.ctx, &output)
@@ -677,18 +686,20 @@ impl<'a, State> Harness<'a, State> {
/// Resize the harness to the last [`egui::ViewportCommand::InnerSize`] requested by the app /// Resize the harness to the last [`egui::ViewportCommand::InnerSize`] requested by the app
/// during the last frame, if any. /// during the last frame, if any.
fn handle_inner_size(&mut self) { fn handle_inner_size(&mut self) {
let new_inner_size = let new_inner_size = self
self.root_viewport_output() .output
.commands .viewport_commands
.iter() .get(&ViewportId::ROOT)
.rev() .into_iter()
.find_map(|command| { .flatten()
if let egui::ViewportCommand::InnerSize(size) = command { .rev()
Some(*size) .find_map(|command| {
} else { if let egui::ViewportCommand::InnerSize(size) = command {
None Some(*size)
} } else {
}); None
}
});
if let Some(size) = new_inner_size { if let Some(size) = new_inner_size {
self.set_size(size); self.set_size(size);
@@ -702,13 +713,13 @@ impl<'a, State> Harness<'a, State> {
/// If a screenshot was requested and no renderer is available, an error will be logged. /// If a screenshot was requested and no renderer is available, an error will be logged.
#[cfg(any(feature = "wgpu", feature = "snapshot"))] #[cfg(any(feature = "wgpu", feature = "snapshot"))]
fn handle_screenshots(&mut self) { fn handle_screenshots(&mut self) {
// Collect all screenshot requests from this frame's viewport output. // Collect all screenshot requests from this frame's viewport commands.
let requests: Vec<(ViewportId, egui::UserData)> = self let requests: Vec<(ViewportId, egui::UserData)> = self
.output .output
.viewport_output .viewport_commands
.iter() .iter()
.flat_map(|(id, viewport)| { .flat_map(|(id, commands)| {
viewport.commands.iter().filter_map(move |command| { commands.iter().filter_map(move |command| {
if let egui::ViewportCommand::Screenshot(user_data) = command { if let egui::ViewportCommand::Screenshot(user_data) = command {
Some((*id, user_data.clone())) Some((*id, user_data.clone()))
} else { } else {
@@ -749,11 +760,22 @@ impl<'a, State> Harness<'a, State> {
/// Get the root viewport output /// Get the root viewport output
fn root_viewport_output(&self) -> &egui::ViewportOutput { fn root_viewport_output(&self) -> &egui::ViewportOutput {
self.output self.output
.expect_pass()
.viewport_output .viewport_output
.get(&ViewportId::ROOT) .get(&ViewportId::ROOT)
.expect("Missing root viewport") .expect("Missing root viewport")
} }
/// The output of the last pass.
///
/// The harness always runs a pass each step, so this always exists.
fn pass_output_mut(&mut self) -> &mut egui::PassOutput {
self.output
.pass_output
.as_mut()
.expect("Harness always runs a pass")
}
/// The root node of the test harness. /// The root node of the test harness.
pub fn root(&self) -> Node<'_> { pub fn root(&self) -> Node<'_> {
Node::new( Node::new(

View File

@@ -178,7 +178,8 @@ impl crate::TestRenderer for WgpuTestRenderer {
size_in_pixels: [size.x.round() as u32, size.y.round() as u32], size_in_pixels: [size.x.round() as u32, size.y.round() as u32],
}; };
let tessellated = ctx.tessellate(output.shapes.clone(), ctx.pixels_per_point()); let tessellated =
ctx.tessellate(output.expect_pass().shapes.clone(), ctx.pixels_per_point());
let user_buffers = renderer.update_buffers( let user_buffers = renderer.update_buffers(
&self.render_state.device, &self.render_state.device,

View File

@@ -145,7 +145,9 @@ fn accesskit_output_single_egui_frame(run_ui: impl FnMut(&mut Ui)) -> TreeUpdate
ctx.enable_accesskit(); ctx.enable_accesskit();
let mut output = ctx.run_ui(RawInput::default(), run_ui); let mut output = ctx.run_ui(RawInput::default(), run_ui);
output.textures_delta.clear(); // Don't panic on drop with unapplied deltas if let Some(pass_output) = &mut output.pass_output {
pass_output.textures_delta.clear(); // Don't panic on drop with unapplied deltas
}
output output
.platform_output .platform_output

View File

@@ -402,7 +402,7 @@ pub fn horizontal_wrapped_text_should_not_overlap() {
); );
} }
for clipped in &harness.output().shapes { for clipped in &harness.output().expect_pass().shapes {
if let egui::epaint::Shape::Text(text_shape) = &clipped.shape { if let egui::epaint::Shape::Text(text_shape) = &clipped.shape {
let shape_rect = text_shape.visual_bounding_rect(); let shape_rect = text_shape.visual_bounding_rect();
assert!( assert!(
@@ -638,7 +638,7 @@ fn window_fixed_size_is_outer_size() {
} }
let mut sizes = Vec::new(); let mut sizes = Vec::new();
for clipped in &harness.output().shapes { for clipped in &harness.output().expect_pass().shapes {
collect_filled_rect_sizes(&clipped.shape, &mut sizes); collect_filled_rect_sizes(&clipped.shape, &mut sizes);
} }
@@ -795,6 +795,7 @@ pub fn textedit_hint_text_should_follow_text_alignment() {
// Find the hint text shape (the only text shape while the input is empty). // Find the hint text shape (the only text shape while the input is empty).
let hint_shape = harness let hint_shape = harness
.output() .output()
.expect_pass()
.shapes .shapes
.iter() .iter()
.find_map(|clipped| { .find_map(|clipped| {

View File

@@ -264,7 +264,7 @@ fn interact_on_ui_response_should_be_stable() {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
fn has_red_warning_rect(output: &egui::FullOutput) -> bool { fn has_red_warning_rect(output: &egui::FullOutput) -> bool {
output.shapes.iter().any(|clipped| { output.expect_pass().shapes.iter().any(|clipped| {
matches!( matches!(
&clipped.shape, &clipped.shape,
Shape::Rect(rect_shape) Shape::Rect(rect_shape)
@@ -631,7 +631,7 @@ fn run_logic_should_not_disturb_ui_state() {
.or_default() .or_default()
.occluded = Some(true); .occluded = Some(true);
let output = harness.ctx.run_logic(&raw_input, |ctx| { let output = harness.ctx.run_logic(raw_input, |ctx| {
assert_eq!( assert_eq!(
ctx.input(|i| i.viewport().occluded), ctx.input(|i| i.viewport().occluded),
Some(true), Some(true),
@@ -642,6 +642,10 @@ fn run_logic_should_not_disturb_ui_state() {
ctx.send_viewport_cmd(egui::ViewportCommand::Focus); ctx.send_viewport_cmd(egui::ViewportCommand::Focus);
}); });
assert!(
output.pass_output.is_none(),
"No pass was run, so there should be no pass output"
);
assert_eq!( assert_eq!(
output output
.viewport_commands .viewport_commands
@@ -660,3 +664,55 @@ fn run_logic_should_not_disturb_ui_state() {
assert_state(&harness); assert_state(&harness);
} }
/// Input given to [`egui::Context::run_logic`] is not consumed by any pass,
/// so egui must keep it and feed it to the next pass.
#[test]
fn run_logic_should_buffer_input_for_the_next_pass() {
let ctx = egui::Context::default();
let mut raw_input = egui::RawInput::default();
raw_input.events.push(egui::Event::Key {
key: egui::Key::A,
physical_key: None,
pressed: true,
repeat: false,
modifiers: egui::Modifiers::NONE,
});
let logic_output = ctx.run_logic(raw_input, |_ctx| {});
logic_output.drop_without_applying_deltas();
let ui_output = ctx.run_ui(egui::RawInput::default(), |ui| {
assert!(
ui.input(|i| i.key_pressed(egui::Key::A)),
"The key press from the pass-less frame should reach the next pass"
);
});
ui_output.drop_without_applying_deltas();
}
/// The logic phase of [`egui::Context::run_frame`] should see the current window state,
/// also in frames where ui is shown (and not the state of the previous frame).
#[test]
fn logic_phase_should_see_fresh_window_state() {
let ctx = egui::Context::default();
let mut raw_input = egui::RawInput::default();
raw_input
.viewports
.entry(egui::ViewportId::ROOT)
.or_default()
.focused = Some(true);
let output = ctx.run_frame(raw_input, true, |phase| {
if let egui::FramePhase::Logic(ctx) = phase {
assert_eq!(
ctx.input(|i| i.viewport().focused),
Some(true),
"App logic should see the current window state, not last frame's"
);
}
});
output.drop_without_applying_deltas();
}