1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-03 15:20:05 -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

@@ -147,7 +147,9 @@ impl<'a, State> Harness<'a, State> {
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 {
app,
@@ -269,7 +271,9 @@ impl<'a, State> Harness<'a, State> {
.take()
.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.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.
/// Will be overwritten on the next call to [`Self::run`].
pub fn mask(&mut self, rect: Rect) {
self.output.shapes.push(ClippedShape {
self.pass_output_mut().shapes.push(ClippedShape {
clip_rect: Rect::EVERYTHING,
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),
];
output.shapes.push(ClippedShape {
clip_rect: self.ctx.content_rect(),
shape: egui::epaint::PathShape::convex_polygon(
triangle,
Color32::WHITE,
egui::Stroke::new(1.0, Color32::BLACK),
)
.into(),
});
output
.pass_output
.as_mut()
.expect("Harness always runs a pass")
.shapes
.push(ClippedShape {
clip_rect: self.ctx.content_rect(),
shape: egui::epaint::PathShape::convex_polygon(
triangle,
Color32::WHITE,
egui::Stroke::new(1.0, Color32::BLACK),
)
.into(),
});
}
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
/// during the last frame, if any.
fn handle_inner_size(&mut self) {
let new_inner_size =
self.root_viewport_output()
.commands
.iter()
.rev()
.find_map(|command| {
if let egui::ViewportCommand::InnerSize(size) = command {
Some(*size)
} else {
None
}
});
let new_inner_size = self
.output
.viewport_commands
.get(&ViewportId::ROOT)
.into_iter()
.flatten()
.rev()
.find_map(|command| {
if let egui::ViewportCommand::InnerSize(size) = command {
Some(*size)
} else {
None
}
});
if let Some(size) = new_inner_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.
#[cfg(any(feature = "wgpu", feature = "snapshot"))]
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
.output
.viewport_output
.viewport_commands
.iter()
.flat_map(|(id, viewport)| {
viewport.commands.iter().filter_map(move |command| {
.flat_map(|(id, commands)| {
commands.iter().filter_map(move |command| {
if let egui::ViewportCommand::Screenshot(user_data) = command {
Some((*id, user_data.clone()))
} else {
@@ -749,11 +760,22 @@ impl<'a, State> Harness<'a, State> {
/// Get the root viewport output
fn root_viewport_output(&self) -> &egui::ViewportOutput {
self.output
.expect_pass()
.viewport_output
.get(&ViewportId::ROOT)
.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.
pub fn root(&self) -> Node<'_> {
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],
};
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(
&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();
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
.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 {
let shape_rect = text_shape.visual_bounding_rect();
assert!(
@@ -638,7 +638,7 @@ fn window_fixed_size_is_outer_size() {
}
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);
}
@@ -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).
let hint_shape = harness
.output()
.expect_pass()
.shapes
.iter()
.find_map(|clipped| {