mirror of
https://github.com/emilk/egui.git
synced 2026-09-01 06:10:06 -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:
@@ -331,17 +331,17 @@ impl EpiIntegration {
|
|||||||
|
|
||||||
let close_requested = raw_input.viewport().close_requested();
|
let close_requested = raw_input.viewport().close_requested();
|
||||||
|
|
||||||
// No pass will consume the input, so save it for the next one:
|
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 {
|
match &mut self.pending_raw_input {
|
||||||
Some(pending) => pending.append(raw_input),
|
Some(pending) => pending.append(raw_input),
|
||||||
None => self.pending_raw_input = Some(raw_input),
|
None => self.pending_raw_input = Some(raw_input),
|
||||||
}
|
}
|
||||||
|
|
||||||
let logic_output = self.egui_ctx.run_logic(|ctx| {
|
|
||||||
profiling::scope!("App::logic");
|
|
||||||
app.logic(ctx, &mut self.frame);
|
|
||||||
});
|
|
||||||
|
|
||||||
if close_requested {
|
if close_requested {
|
||||||
let canceled = logic_output
|
let canceled = logic_output
|
||||||
.viewport_commands
|
.viewport_commands
|
||||||
|
|||||||
@@ -285,16 +285,16 @@ impl AppRunner {
|
|||||||
// 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.
|
||||||
|
|
||||||
// No pass will consume the input, so save it for the next one:
|
|
||||||
self.input.raw.append(raw_input);
|
|
||||||
|
|
||||||
let egui::LogicOutput {
|
let egui::LogicOutput {
|
||||||
platform_output,
|
platform_output,
|
||||||
viewport_commands,
|
viewport_commands,
|
||||||
} = self.egui_ctx.run_logic(|ctx| {
|
} = self.egui_ctx.run_logic(&raw_input, |ctx| {
|
||||||
self.app.logic(ctx, &mut self.frame);
|
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_viewport_commands(viewport_commands.into_values().flatten());
|
||||||
self.handle_platform_output(platform_output);
|
self.handle_platform_output(platform_output);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -898,21 +898,32 @@ impl Context {
|
|||||||
/// No pass is run, so `f` must not show any ui.
|
/// No pass is run, so `f` must not show any ui.
|
||||||
/// This means everything egui knows about the ui is left untouched:
|
/// This means 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,
|
||||||
/// nothing loses focus, and [`Self::input`] still refers to the last pass.
|
/// and nothing loses focus.
|
||||||
|
///
|
||||||
|
/// Of `new_input`, only [`RawInput::viewports`] is used: `f` can learn about the state of
|
||||||
|
/// the windows with [`InputState::viewport`], but the ui input (events, time, …)
|
||||||
|
/// is left as it was, and should be given to the next call to [`Self::run_ui`].
|
||||||
///
|
///
|
||||||
/// The returned [`LogicOutput`] is what [`FullOutput`] would have carried:
|
/// The returned [`LogicOutput`] is what [`FullOutput`] would have carried:
|
||||||
/// anything `f` asked the integration to do.
|
/// anything `f` asked the integration to do.
|
||||||
/// There is nothing to paint.
|
/// There is nothing to paint.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn run_logic(&self, f: impl FnOnce(&Self)) -> LogicOutput {
|
pub fn run_logic(&self, new_input: &RawInput, f: impl FnOnce(&Self)) -> LogicOutput {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
|
|
||||||
// Outside of a pass this is the root viewport:
|
let viewport_id = new_input.viewport_id;
|
||||||
let viewport_id = self.viewport_id();
|
|
||||||
|
|
||||||
// Consume any outstanding repaint request, so that a new request from `f`
|
self.write(|ctx| {
|
||||||
// reaches the integration instead of being considered already served:
|
// Consume any outstanding repaint request, so that a new request from `f`
|
||||||
self.write(|ctx| ctx.begin_pass_repaint_logic(viewport_id));
|
// 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 raw = &mut ctx.viewport_for(viewport_id).input.raw;
|
||||||
|
raw.viewport_id = viewport_id;
|
||||||
|
raw.viewports = new_input.viewports.clone();
|
||||||
|
raw.focused = new_input.focused;
|
||||||
|
});
|
||||||
|
|
||||||
f(self);
|
f(self);
|
||||||
|
|
||||||
|
|||||||
@@ -559,3 +559,104 @@ fn tooltip_should_hand_over_to_neighboring_widget() {
|
|||||||
"Tooltip A should be hidden when hovering Button B"
|
"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