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

Never run an egui pass when nothing will be shown (#8387)

* Closes <https://github.com/emilk/egui/issues/8266>
* Alternative to #8385

Not the most simple or beautiful code, but it works, and makes sense.

What makes it complex: `app.logic` should still see some input (e.g.
what viewports are visible) and emit some output (e.g. "open this link",
or "focus and repaint").

## TODO
* [x] test multiple viewports

## Clanker says
Instead of teaching egui to skip book-keeping during a pass where no ui
is shown, we simply run no pass at all. Then there is nothing to
special-case: all ui state is left untouched, and the app finds
everything where it left it when the window is shown again.

* New `Context::run_logic(&raw_input, f)`: ticks app logic without a
pass, returning the `LogicOutput` (platform output + viewport commands)
that a pass would otherwise have carried, so e.g.
`ViewportCommand::Focus` still reaches the integration.
* All three eframe backends (glow, wgpu, web) call `run_logic` instead
of `run_ui` when the viewport is minimized/occluded (and has no visible
descendant viewport) or, on web, when the tab is hidden.
* `App::logic` is still called from inside the pass when the window is
visible, so it sees the current frame's input.

While hidden, `run_logic` fills in only the window state
(`RawInput::viewports` / `focused`), so the app can tell that it is
hidden. The ui input (events, time, …) is not interpreted, and is
instead given to the next real pass.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Emil Ernerfeldt
2026-08-05 00:14:35 -07:00
committed by GitHub
parent 90e03028f7
commit e37d44ad8a
10 changed files with 519 additions and 132 deletions

View File

@@ -559,3 +559,119 @@ fn tooltip_should_hand_over_to_neighboring_widget() {
"Tooltip A should be hidden when hovering Button B"
);
}
/// When a window is minimized or occluded, the integration runs no pass at all,
/// and instead ticks the app logic with [`egui::Context::run_logic`].
///
/// Such a tick must leave all ui state alone. Otherwise areas think they were hidden and
/// replay their fade-in, popups close, focus is lost, and child viewports pop back up.
/// See <https://github.com/emilk/egui/issues/8266>.
#[test]
fn run_logic_should_not_disturb_ui_state() {
const MENU: &str = "My menu";
const MENU_ITEM: &str = "Button in my menu";
const FOCUSED_BUTTON: &str = "Click me";
let child_viewport = egui::ViewportId::from_hash_of("My child viewport");
let area_id = egui::Id::new("My area");
let area_layer = egui::LayerId::new(egui::Order::Middle, area_id);
let mut harness = Harness::builder()
.with_size(Vec2::new(400.0, 300.0))
.build_ui(move |ui| {
// A backend that can open real windows, like eframe:
ui.ctx().set_embed_viewports(false);
ui.ctx()
.show_viewport_deferred(child_viewport, Default::default(), |_ui, _class| {});
ui.menu_button(MENU, |ui| {
_ = ui.button(MENU_ITEM);
});
egui::Area::new(area_id)
.fixed_pos((150.0, 120.0))
.show(ui.ctx(), |ui| {
_ = ui.button(FOCUSED_BUTTON);
});
});
harness.get_by_label(MENU).click();
harness.run();
// Nothing asks for focus again, so the test fails if egui ever loses it:
harness.get_by_label(FOCUSED_BUTTON).focus();
harness.run();
let assert_state = |harness: &Harness<'_>| {
assert!(
harness
.get_by_label(FOCUSED_BUTTON)
.accesskit_node()
.is_focused(),
"The button lost focus"
);
harness.get_by_label(MENU_ITEM); // Panics if the menu closed
assert!(
harness
.ctx
.memory(|m| m.areas().visible_last_frame(&area_layer)),
"Area state was reset"
);
assert!(
harness
.ctx
.viewport_for(child_viewport, |viewport| viewport.class)
== egui::ViewportClass::Deferred,
"The child viewport was closed"
);
};
assert_state(&harness);
// The window is now occluded, so the integration runs no pass,
// and only ticks the app logic:
for i in 0..2 {
let time = 100.0 + f64::from(i);
let mut raw_input = egui::RawInput {
time: Some(time),
..Default::default()
};
raw_input
.viewports
.entry(egui::ViewportId::ROOT)
.or_default()
.occluded = Some(true);
let output = harness.ctx.run_logic(&raw_input, |ctx| {
assert_eq!(
ctx.input(|i| i.viewport().occluded),
Some(true),
"App logic should be able to tell that the window is occluded"
);
assert!(
ctx.input(|i| i.time) != time,
"The ui input should not be interpreted: it is for the next pass"
);
// The app asks to be shown again:
ctx.send_viewport_cmd(egui::ViewportCommand::Focus);
});
assert_eq!(
output
.viewport_commands
.into_values()
.flatten()
.collect::<Vec<_>>(),
vec![egui::ViewportCommand::Focus],
"The integration should receive the command, even though there was no pass"
);
assert_state(&harness);
}
// The window is visible again, and everything should be where we left it:
harness.run();
assert_state(&harness);
}