mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -04:00
Add uiless_pass flag to retain state on empty / occluded frames
This commit is contained in:
@@ -268,10 +268,14 @@ impl EpiIntegration {
|
||||
app: &mut dyn epi::App,
|
||||
viewport_ui_cb: Option<&DeferredViewportUiCallback>,
|
||||
mut raw_input: egui::RawInput,
|
||||
is_visible: bool,
|
||||
show_ui: bool,
|
||||
) -> egui::FullOutput {
|
||||
raw_input.time = Some(self.beginning.elapsed().as_secs_f64());
|
||||
|
||||
// We still run a pass while hidden so that app logic keeps ticking,
|
||||
// but we tell egui to skip all book-keeping that assumes ui was shown:
|
||||
raw_input.uiless_pass = !show_ui;
|
||||
|
||||
let close_requested = raw_input.viewport().close_requested();
|
||||
|
||||
app.raw_input_hook(&self.egui_ctx, &mut raw_input);
|
||||
@@ -279,7 +283,7 @@ impl EpiIntegration {
|
||||
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
|
||||
if let Some(viewport_ui_cb) = viewport_ui_cb {
|
||||
// Child viewport
|
||||
if is_visible {
|
||||
if show_ui {
|
||||
profiling::scope!("viewport_callback");
|
||||
viewport_ui_cb(ui);
|
||||
}
|
||||
@@ -289,7 +293,7 @@ impl EpiIntegration {
|
||||
app.logic(ui.ctx(), &mut self.frame);
|
||||
}
|
||||
|
||||
if is_visible {
|
||||
if show_ui {
|
||||
{
|
||||
profiling::scope!("App::ui");
|
||||
app.ui(ui, &mut self.frame);
|
||||
|
||||
@@ -280,6 +280,10 @@ impl AppRunner {
|
||||
.and_then(|v| v.visible())
|
||||
.unwrap_or(true);
|
||||
|
||||
// We still run a pass while hidden so that app logic keeps ticking,
|
||||
// but we tell egui to skip all book-keeping that assumes ui was shown:
|
||||
raw_input.uiless_pass = !is_visible;
|
||||
|
||||
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
|
||||
self.app.logic(ui.ctx(), &mut self.frame);
|
||||
|
||||
|
||||
@@ -457,6 +457,10 @@ impl ContextImpl {
|
||||
|
||||
let viewport = self.viewports.entry(self.viewport_id()).or_default();
|
||||
|
||||
// If no ui will be shown this pass we skip all book-keeping that assumes ui was shown.
|
||||
// See `RawInput::uiless_pass`.
|
||||
let uiless_pass = new_raw_input.uiless_pass;
|
||||
|
||||
self.memory.begin_pass(&new_raw_input, &all_viewport_ids);
|
||||
|
||||
viewport.input = std::mem::take(&mut viewport.input).begin_pass(
|
||||
@@ -471,7 +475,7 @@ impl ContextImpl {
|
||||
|
||||
viewport.this_pass.begin_pass();
|
||||
|
||||
{
|
||||
if !uiless_pass {
|
||||
// Areas that are not interactable are click-through: skip them in the hit-test.
|
||||
let mut layers: Vec<LayerId> = viewport
|
||||
.prev_pass
|
||||
@@ -516,7 +520,7 @@ impl ContextImpl {
|
||||
},
|
||||
);
|
||||
|
||||
if self.is_accesskit_enabled {
|
||||
if self.is_accesskit_enabled && !uiless_pass {
|
||||
profiling::scope!("accesskit");
|
||||
use crate::pass_state::AccessKitPassState;
|
||||
let id = crate::accesskit_root_id();
|
||||
@@ -807,9 +811,17 @@ impl Context {
|
||||
);
|
||||
|
||||
{
|
||||
plugins.on_begin_pass(&mut root_ui);
|
||||
// Plugins may show ui, so don't run them when no ui should be shown.
|
||||
// See `RawInput::uiless_pass`.
|
||||
let show_ui = !ctx.is_uiless_pass();
|
||||
|
||||
if show_ui {
|
||||
plugins.on_begin_pass(&mut root_ui);
|
||||
}
|
||||
run_ui(&mut root_ui);
|
||||
plugins.on_end_pass(&mut root_ui);
|
||||
if show_ui {
|
||||
plugins.on_end_pass(&mut root_ui);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.pass_state_mut(|state| {
|
||||
@@ -823,6 +835,7 @@ impl Context {
|
||||
fn run_dyn(&self, mut new_input: RawInput, run_ui: &mut dyn FnMut(&Self)) -> FullOutput {
|
||||
profiling::function_scope!();
|
||||
let viewport_id = new_input.viewport_id;
|
||||
let uiless_pass = new_input.uiless_pass;
|
||||
let max_passes = self.write(|ctx| ctx.memory.options.max_passes.get());
|
||||
|
||||
let mut output = FullOutput::default();
|
||||
@@ -882,7 +895,10 @@ impl Context {
|
||||
} else {
|
||||
viewport.num_multipass_in_row = 0;
|
||||
}
|
||||
viewport.repaint.cumulative_frame_nr += 1;
|
||||
if !uiless_pass {
|
||||
// A uiless pass is not a frame: nothing was shown. See `RawInput::uiless_pass`.
|
||||
viewport.repaint.cumulative_frame_nr += 1;
|
||||
}
|
||||
});
|
||||
|
||||
output
|
||||
@@ -1718,6 +1734,17 @@ impl Context {
|
||||
})
|
||||
}
|
||||
|
||||
/// Is this a pass where no ui will be shown?
|
||||
///
|
||||
/// This is `true` when the integration runs a pass only to keep app logic ticking,
|
||||
/// e.g. because the window is minimized or occluded.
|
||||
/// Skip any ui work if this is `true`.
|
||||
///
|
||||
/// See [`RawInput::uiless_pass`].
|
||||
pub fn is_uiless_pass(&self) -> bool {
|
||||
self.input(|i| i.raw.uiless_pass)
|
||||
}
|
||||
|
||||
/// The total number of completed passes (usually there is one pass per rendered frame).
|
||||
///
|
||||
/// Starts at zero, and is incremented for each completed pass inside of [`Self::run_ui`] (usually once).
|
||||
@@ -2402,7 +2429,9 @@ impl Context {
|
||||
self.sync_window_theme();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
self.debug_painting();
|
||||
if !self.is_uiless_pass() {
|
||||
self.debug_painting();
|
||||
}
|
||||
|
||||
let mut output = self.write(|ctx| ctx.end_pass());
|
||||
|
||||
@@ -2612,11 +2641,19 @@ impl ContextImpl {
|
||||
let viewport = self.viewports.entry(ended_viewport_id).or_default();
|
||||
let pixels_per_point = viewport.input.pixels_per_point;
|
||||
|
||||
self.loaders.end_pass(viewport.repaint.cumulative_pass_nr);
|
||||
// If no ui was shown this pass we skip all book-keeping that assumes ui was shown.
|
||||
// See `RawInput::uiless_pass`.
|
||||
let uiless_pass = viewport.input.raw.uiless_pass;
|
||||
|
||||
viewport.repaint.cumulative_pass_nr += 1;
|
||||
if !uiless_pass {
|
||||
// A uiless pass uses no images and no widget ids,
|
||||
// so garbage-collecting based on it would throw away things we still need.
|
||||
self.loaders.end_pass(viewport.repaint.cumulative_pass_nr);
|
||||
|
||||
self.memory.end_pass(&viewport.this_pass.used_ids);
|
||||
viewport.repaint.cumulative_pass_nr += 1;
|
||||
|
||||
self.memory.end_pass(&viewport.this_pass.used_ids);
|
||||
}
|
||||
|
||||
if let Some(fonts) = self.fonts.as_mut() {
|
||||
let tex_mngr = &mut self.tex_manager.0.write();
|
||||
@@ -2668,7 +2705,7 @@ impl ContextImpl {
|
||||
|
||||
let mut repaint_needed = false;
|
||||
|
||||
if self.memory.options.repaint_on_widget_change {
|
||||
if self.memory.options.repaint_on_widget_change && !uiless_pass {
|
||||
profiling::scope!("compare-widget-rects");
|
||||
#[allow(clippy::allow_attributes, clippy::collapsible_if)] // false positive on wasm
|
||||
if viewport.prev_pass.widgets != viewport.this_pass.widgets {
|
||||
@@ -2689,7 +2726,11 @@ impl ContextImpl {
|
||||
shapes
|
||||
};
|
||||
|
||||
std::mem::swap(&mut viewport.prev_pass, &mut viewport.this_pass);
|
||||
if !uiless_pass {
|
||||
// Keep `prev_pass` from the last pass that actually showed ui,
|
||||
// so widgets are still found by the next hit-test and interaction.
|
||||
std::mem::swap(&mut viewport.prev_pass, &mut viewport.this_pass);
|
||||
}
|
||||
|
||||
if repaint_needed {
|
||||
self.request_repaint(ended_viewport_id, RepaintCause::new());
|
||||
@@ -2718,7 +2759,10 @@ impl ContextImpl {
|
||||
}
|
||||
|
||||
let is_our_child = parent == ended_viewport_id && id != ViewportId::ROOT;
|
||||
if is_our_child {
|
||||
if is_our_child && !uiless_pass {
|
||||
// A uiless pass never calls `Context::show_viewport_deferred`,
|
||||
// so we must not read `used` from it: that would close all our child
|
||||
// viewports, only for them to pop back up on the next normal pass.
|
||||
if !viewport.used {
|
||||
log::debug!(
|
||||
"Removing viewport {:?} ({:?}): it was never used this pass",
|
||||
@@ -4327,6 +4371,106 @@ fn warn_if_rect_changes_id(
|
||||
mod test {
|
||||
use super::Context;
|
||||
|
||||
/// A pass with [`RawInput::uiless_pass`] set must leave all ui state alone,
|
||||
/// so that nothing thinks it was hidden. See <https://github.com/emilk/egui/issues/8266>.
|
||||
#[test]
|
||||
fn test_uiless_pass_preserves_ui_state() {
|
||||
use crate::{Id, LayerId, RawInput, Window};
|
||||
|
||||
let ctx = Context::default();
|
||||
let window_layer = std::cell::Cell::new(LayerId::background());
|
||||
|
||||
let run = |uiless_pass: bool| {
|
||||
let input = RawInput {
|
||||
uiless_pass,
|
||||
..Default::default()
|
||||
};
|
||||
ctx.run_ui(input, |ui| {
|
||||
if uiless_pass {
|
||||
return; // The integration shows no ui during a uiless pass.
|
||||
}
|
||||
let response = Window::new("My window")
|
||||
.show(ui.ctx(), |ui| {
|
||||
ui.button("Click me").request_focus();
|
||||
})
|
||||
.expect("The window should be open");
|
||||
window_layer.set(response.response.layer_id);
|
||||
})
|
||||
.drop_without_applying_deltas();
|
||||
};
|
||||
|
||||
// Two normal passes, so that all the "previous pass" state has settled:
|
||||
run(false);
|
||||
run(false);
|
||||
|
||||
let focused = ctx.memory(|m| m.focused());
|
||||
assert!(focused.is_some(), "The button should have focus");
|
||||
assert!(ctx.memory(|m| m.areas().visible_last_frame(&window_layer.get())));
|
||||
|
||||
let popup_id = Id::new("My popup");
|
||||
ctx.memory_mut(|m| m.open_popup(popup_id));
|
||||
|
||||
let pass_nr = ctx.cumulative_pass_nr();
|
||||
let frame_nr = ctx.cumulative_frame_nr();
|
||||
|
||||
// A uiless pass must disturb none of it:
|
||||
run(true);
|
||||
|
||||
assert_eq!(ctx.cumulative_pass_nr(), pass_nr, "Counted as a pass");
|
||||
assert_eq!(ctx.cumulative_frame_nr(), frame_nr, "Counted as a frame");
|
||||
assert_eq!(ctx.memory(|m| m.focused()), focused, "Lost focus");
|
||||
assert!(
|
||||
ctx.memory(|m| m.areas().visible_last_frame(&window_layer.get())),
|
||||
"The window would replay its appear-animation"
|
||||
);
|
||||
assert!(
|
||||
ctx.memory(|m| m.is_popup_open(popup_id)),
|
||||
"Closed the popup"
|
||||
);
|
||||
|
||||
// …and the window is still there on the next normal pass:
|
||||
run(false);
|
||||
assert!(ctx.memory(|m| m.areas().visible_last_frame(&window_layer.get())));
|
||||
}
|
||||
|
||||
/// A uiless pass never calls [`Context::show_viewport_deferred`], so it must not
|
||||
/// garbage-collect child viewports — they would pop back up on the next normal pass.
|
||||
/// See <https://github.com/emilk/egui/issues/8266>.
|
||||
#[test]
|
||||
fn test_uiless_pass_keeps_deferred_viewports() {
|
||||
use crate::{RawInput, ViewportBuilder, ViewportId};
|
||||
|
||||
let ctx = Context::default();
|
||||
ctx.set_embed_viewports(false);
|
||||
|
||||
let child_id = ViewportId::from_hash_of("My child viewport");
|
||||
|
||||
// Runs one pass and returns the viewports the backend is told to keep.
|
||||
let run = |uiless_pass: bool| {
|
||||
let input = RawInput {
|
||||
uiless_pass,
|
||||
..Default::default()
|
||||
};
|
||||
let output = ctx.run_ui(input, |ui| {
|
||||
if uiless_pass {
|
||||
return; // The integration shows no ui during a uiless pass.
|
||||
}
|
||||
ui.ctx().show_viewport_deferred(
|
||||
child_id,
|
||||
ViewportBuilder::default(),
|
||||
|_ui, _class| {},
|
||||
);
|
||||
});
|
||||
let has_child = output.viewport_output.contains_key(&child_id);
|
||||
output.drop_without_applying_deltas();
|
||||
has_child
|
||||
};
|
||||
|
||||
assert!(run(false), "The child viewport should have been created");
|
||||
assert!(run(true), "The child viewport was closed");
|
||||
assert!(run(false), "The child viewport should still be there");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_pass() {
|
||||
let ctx = Context::default();
|
||||
|
||||
@@ -89,6 +89,20 @@ pub struct RawInput {
|
||||
///
|
||||
/// `None` means "don't know".
|
||||
pub system_theme: Option<Theme>,
|
||||
|
||||
/// Set this if no ui will be shown during this pass.
|
||||
///
|
||||
/// An integration sets this when the window is hidden, minimized, or occluded,
|
||||
/// but it still wants to run a pass so that background logic keeps ticking
|
||||
/// (e.g. so the app can send a [`crate::ViewportCommand`] to show itself again).
|
||||
///
|
||||
/// egui then skips all the book-keeping that assumes ui was shown, so that
|
||||
/// widgets don't think they were hidden and replay their appear-animations,
|
||||
/// popups don't close, focus isn't lost, and so on.
|
||||
/// See <https://github.com/emilk/egui/issues/8266>.
|
||||
///
|
||||
/// If you set this, you must not show any ui during the pass.
|
||||
pub uiless_pass: bool,
|
||||
}
|
||||
|
||||
impl Default for RawInput {
|
||||
@@ -106,6 +120,7 @@ impl Default for RawInput {
|
||||
focused: true, // integrations opt into global focus tracking
|
||||
system_theme: None,
|
||||
safe_area_insets: Default::default(),
|
||||
uiless_pass: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,6 +154,7 @@ impl RawInput {
|
||||
dropped_files: std::mem::take(&mut self.dropped_files),
|
||||
focused: self.focused,
|
||||
system_theme: self.system_theme,
|
||||
uiless_pass: self.uiless_pass,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +173,7 @@ impl RawInput {
|
||||
focused,
|
||||
system_theme,
|
||||
safe_area_insets: safe_area,
|
||||
uiless_pass,
|
||||
} = newer;
|
||||
|
||||
self.viewport_id = viewport_ids;
|
||||
@@ -171,6 +188,7 @@ impl RawInput {
|
||||
self.focused = focused;
|
||||
self.system_theme = system_theme;
|
||||
self.safe_area_insets = safe_area;
|
||||
self.uiless_pass = uiless_pass;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +207,7 @@ impl RawInput {
|
||||
focused,
|
||||
system_theme,
|
||||
safe_area_insets: safe_area,
|
||||
uiless_pass,
|
||||
} = self;
|
||||
|
||||
ui.label(format!("Active viewport: {viewport_id:?}"));
|
||||
@@ -218,6 +237,7 @@ impl RawInput {
|
||||
ui.label(format!("focused: {focused}"));
|
||||
ui.label(format!("system_theme: {system_theme:?}"));
|
||||
ui.label(format!("safe_area: {safe_area:?}"));
|
||||
ui.label(format!("uiless_pass: {uiless_pass}"));
|
||||
ui.scope(|ui| {
|
||||
ui.set_min_height(150.0);
|
||||
ui.label(format!("events: {events:#?}"))
|
||||
|
||||
@@ -797,10 +797,11 @@ impl Memory {
|
||||
|
||||
self.options.begin_pass(new_raw_input);
|
||||
|
||||
self.focus
|
||||
.entry(self.viewport_id)
|
||||
.or_default()
|
||||
.begin_pass(new_raw_input);
|
||||
let focus = self.focus.entry(self.viewport_id).or_default();
|
||||
if !new_raw_input.uiless_pass {
|
||||
// No widget will ask for focus this pass, so leave the focus state alone.
|
||||
focus.begin_pass(new_raw_input);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn end_pass(&mut self, used_ids: &IdMap<Rect>) {
|
||||
|
||||
@@ -60,6 +60,10 @@ fn viewport_info(ctx: &egui::Context) -> String {
|
||||
write!(s, " {name}={value}").ok();
|
||||
}
|
||||
}
|
||||
|
||||
use std::fmt::Write as _;
|
||||
write!(s, " uiless_pass={}", i.raw.uiless_pass).ok();
|
||||
|
||||
s
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user