mirror of
https://github.com/emilk/egui.git
synced 2026-08-30 13:20:05 -04:00
Let app logic see the window state, and test it
`Context::run_logic` now takes the new `RawInput`, and copies only `RawInput::viewports` into the `InputState`, so that `App::logic` can tell that the window is minimized/occluded. The ui input (events, time) is left for the next pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -559,3 +559,104 @@ 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_state(&harness);
|
||||
|
||||
// The window is now occluded, so the integration runs no pass,
|
||||
// and only ticks the app logic:
|
||||
for _ in 0..2 {
|
||||
let mut raw_input = egui::RawInput::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"
|
||||
);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user