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:
@@ -155,6 +155,12 @@ pub trait App {
|
||||
///
|
||||
/// You may NOT show any ui or do any painting during the call to [`Self::logic`].
|
||||
///
|
||||
/// While the window is hidden, `eframe` runs no egui pass at all (so that no ui state is
|
||||
/// disturbed), and calls this via [`egui::Context::run_logic`] instead.
|
||||
/// You can then still tell that the window is hidden with
|
||||
/// [`egui::InputState::viewport`], but the rest of [`egui::Context::input`]
|
||||
/// (events, time, …) is that of the last shown frame.
|
||||
///
|
||||
/// The [`egui::Context`] can be cloned and saved if you like.
|
||||
///
|
||||
/// To force another call to [`Self::logic`], call [`egui::Context::request_repaint`] at any time (e.g. from another thread).
|
||||
|
||||
@@ -156,6 +156,11 @@ pub struct EpiIntegration {
|
||||
pub beginning: Instant,
|
||||
is_first_frame: bool,
|
||||
pub egui_ctx: egui::Context,
|
||||
|
||||
/// Input that we have received, but not yet given to egui,
|
||||
/// because we haven't run any pass since (see [`Self::update_logic_only`]).
|
||||
pending_raw_input: egui::RawInput,
|
||||
|
||||
pending_full_output: egui::FullOutput,
|
||||
|
||||
/// When set, it is time to close the native window.
|
||||
@@ -215,6 +220,7 @@ impl EpiIntegration {
|
||||
Self {
|
||||
frame,
|
||||
last_auto_save: Instant::now(),
|
||||
pending_raw_input: Default::default(),
|
||||
pending_full_output: Default::default(),
|
||||
close: false,
|
||||
can_drag_window: false,
|
||||
@@ -262,59 +268,111 @@ impl EpiIntegration {
|
||||
|
||||
/// Run user code - this can create immediate viewports, so hold no locks over this!
|
||||
///
|
||||
/// If `viewport_ui_cb` is None, we are in the root viewport and will call [`crate::App::ui`].
|
||||
/// If `viewport_ui_cb` is None, we are in the root viewport and will call
|
||||
/// [`crate::App::logic`] and [`crate::App::ui`].
|
||||
///
|
||||
/// Only call this when the ui will actually be shown;
|
||||
/// use [`Self::update_logic_only`] otherwise.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
app: &mut dyn epi::App,
|
||||
viewport_ui_cb: Option<&DeferredViewportUiCallback>,
|
||||
mut raw_input: egui::RawInput,
|
||||
is_visible: bool,
|
||||
raw_input: egui::RawInput,
|
||||
) -> egui::FullOutput {
|
||||
raw_input.time = Some(self.beginning.elapsed().as_secs_f64());
|
||||
let raw_input = self.prepare_raw_input(app, raw_input);
|
||||
|
||||
let close_requested = raw_input.viewport().close_requested();
|
||||
|
||||
app.raw_input_hook(&self.egui_ctx, &mut raw_input);
|
||||
let is_root_viewport = viewport_ui_cb.is_none();
|
||||
|
||||
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 {
|
||||
profiling::scope!("viewport_callback");
|
||||
viewport_ui_cb(ui);
|
||||
}
|
||||
profiling::scope!("viewport_callback");
|
||||
viewport_ui_cb(ui);
|
||||
} else {
|
||||
{
|
||||
profiling::scope!("App::logic");
|
||||
app.logic(ui.ctx(), &mut self.frame);
|
||||
}
|
||||
|
||||
if is_visible {
|
||||
{
|
||||
profiling::scope!("App::ui");
|
||||
app.ui(ui, &mut self.frame);
|
||||
}
|
||||
{
|
||||
profiling::scope!("App::ui");
|
||||
app.ui(ui, &mut self.frame);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let is_root_viewport = viewport_ui_cb.is_none();
|
||||
if is_root_viewport && close_requested {
|
||||
let canceled = full_output.viewport_output[&ViewportId::ROOT]
|
||||
.commands
|
||||
.contains(&egui::ViewportCommand::CancelClose);
|
||||
if canceled {
|
||||
log::debug!("Closing of root viewport canceled with ViewportCommand::CancelClose");
|
||||
} else {
|
||||
log::debug!("Closing root viewport (ViewportCommand::CancelClose was not sent)");
|
||||
self.close = true;
|
||||
}
|
||||
self.handle_close_request(canceled);
|
||||
}
|
||||
|
||||
self.pending_full_output.append(full_output);
|
||||
std::mem::take(&mut self.pending_full_output)
|
||||
}
|
||||
|
||||
/// Let the app tick its logic without showing any ui,
|
||||
/// because the window is minimized or occluded.
|
||||
///
|
||||
/// No egui pass is run, so all ui state is left untouched:
|
||||
/// the app will find everything where it left it once the window is visible again.
|
||||
///
|
||||
/// Only call this for the root viewport: only it has [`crate::App::logic`].
|
||||
pub fn update_logic_only(
|
||||
&mut self,
|
||||
app: &mut dyn epi::App,
|
||||
raw_input: egui::RawInput,
|
||||
) -> egui::LogicOutput {
|
||||
let raw_input = self.prepare_raw_input(app, raw_input);
|
||||
|
||||
let close_requested = raw_input.viewport().close_requested();
|
||||
|
||||
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:
|
||||
self.pending_raw_input = raw_input;
|
||||
|
||||
if close_requested {
|
||||
let canceled = logic_output
|
||||
.viewport_commands
|
||||
.get(&ViewportId::ROOT)
|
||||
.is_some_and(|commands| commands.contains(&egui::ViewportCommand::CancelClose));
|
||||
self.handle_close_request(canceled);
|
||||
}
|
||||
|
||||
logic_output
|
||||
}
|
||||
|
||||
/// Prepend any input we couldn't give to egui earlier, set the time, and run the app hook.
|
||||
fn prepare_raw_input(
|
||||
&mut self,
|
||||
app: &mut dyn epi::App,
|
||||
new_input: egui::RawInput,
|
||||
) -> egui::RawInput {
|
||||
let mut raw_input = std::mem::take(&mut self.pending_raw_input);
|
||||
raw_input.append(new_input); // The new input wins where they overlap
|
||||
|
||||
raw_input.time = Some(self.beginning.elapsed().as_secs_f64());
|
||||
|
||||
app.raw_input_hook(&self.egui_ctx, &mut raw_input);
|
||||
|
||||
raw_input
|
||||
}
|
||||
|
||||
fn handle_close_request(&mut self, canceled: bool) {
|
||||
if canceled {
|
||||
log::debug!("Closing of root viewport canceled with ViewportCommand::CancelClose");
|
||||
} else {
|
||||
log::debug!("Closing root viewport (ViewportCommand::CancelClose was not sent)");
|
||||
self.close = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_frame_time(&mut self, seconds: f32) {
|
||||
self.frame.info.cpu_usage = Some(seconds);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ use super::{
|
||||
use crate::epaint::textures::TexturesDelta;
|
||||
use crate::{
|
||||
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
||||
native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized},
|
||||
native::{epi_integration::EpiIntegration, winit_integration::sleep_if_invisible_or_minimized},
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -139,6 +139,27 @@ struct Viewport {
|
||||
egui_winit: Option<egui_winit::State>,
|
||||
}
|
||||
|
||||
impl Viewport {
|
||||
/// Apply the commands, or defer them until we have a window.
|
||||
fn process_commands(
|
||||
&mut self,
|
||||
egui_ctx: &egui::Context,
|
||||
mut commands: Vec<egui::ViewportCommand>,
|
||||
) {
|
||||
self.deferred_commands.append(&mut commands);
|
||||
|
||||
if let Some(window) = &self.window {
|
||||
egui_winit::process_viewport_commands(
|
||||
egui_ctx,
|
||||
&mut self.info,
|
||||
std::mem::take(&mut self.deferred_commands),
|
||||
window,
|
||||
&mut self.actions_requested,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Viewport {
|
||||
fn drop(&mut self) {
|
||||
// Avoid debug panic when dropping unapplied deltas on teardown
|
||||
@@ -579,7 +600,7 @@ impl GlowWinitRunning<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
let (raw_input, viewport_ui_cb, is_visible, run_ui) = {
|
||||
let (raw_input, viewport_ui_cb, is_visible, show_ui) = {
|
||||
let mut glutin = self.glutin.borrow_mut();
|
||||
let egui_ctx = glutin.egui_ctx.clone();
|
||||
let Some(viewport) = glutin.viewports.get_mut(&viewport_id) else {
|
||||
@@ -598,7 +619,7 @@ impl GlowWinitRunning<'_> {
|
||||
let mut raw_input = egui_winit.take_egui_input(window);
|
||||
let viewport_ui_cb = viewport.viewport_ui_cb.clone();
|
||||
|
||||
let run_ui =
|
||||
let show_ui =
|
||||
is_visible || is_viewport_or_descendant_visible(&glutin.viewports, viewport_id);
|
||||
|
||||
self.integration.pre_update();
|
||||
@@ -610,9 +631,58 @@ impl GlowWinitRunning<'_> {
|
||||
.map(|(id, viewport)| (*id, viewport.info.clone()))
|
||||
.collect();
|
||||
|
||||
(raw_input, viewport_ui_cb, is_visible, run_ui)
|
||||
(raw_input, viewport_ui_cb, is_visible, show_ui)
|
||||
};
|
||||
|
||||
if !show_ui {
|
||||
// Nothing will be shown, so we run no egui pass at all.
|
||||
// That way all ui state is left untouched, and is still there
|
||||
// when this viewport becomes visible again.
|
||||
let is_root_viewport = viewport_ui_cb.is_none();
|
||||
if is_root_viewport {
|
||||
// The app logic keeps ticking, so it can e.g. ask to be shown again:
|
||||
let egui::LogicOutput {
|
||||
platform_output,
|
||||
viewport_commands,
|
||||
} = self
|
||||
.integration
|
||||
.update_logic_only(self.app.as_mut(), raw_input);
|
||||
|
||||
let mut glutin = self.glutin.borrow_mut();
|
||||
if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) {
|
||||
viewport.info.events.clear(); // they should have been processed
|
||||
if let Some(window) = viewport.window.clone()
|
||||
&& let Some(egui_winit) = viewport.egui_winit.as_mut()
|
||||
{
|
||||
egui_winit.handle_platform_output_with_event_loop(
|
||||
&window,
|
||||
event_loop,
|
||||
platform_output,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (id, commands) in viewport_commands {
|
||||
if let Some(viewport) = glutin.viewports.get_mut(&id) {
|
||||
viewport.process_commands(&self.integration.egui_ctx, commands);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sleep_if_invisible_or_minimized(
|
||||
self.glutin
|
||||
.borrow()
|
||||
.viewports
|
||||
.get(&viewport_id)
|
||||
.and_then(|viewport| viewport.window.as_deref()),
|
||||
);
|
||||
|
||||
return Ok(if self.integration.should_close() {
|
||||
EventResult::CloseRequested
|
||||
} else {
|
||||
EventResult::Wait
|
||||
});
|
||||
}
|
||||
|
||||
// HACK: In order to get the right clear_color, the system theme needs to be set, which
|
||||
// usually only happens in the `update` call. So we call Options::begin_pass early
|
||||
// to set the right theme. Without this there would be a black flash on the first frame.
|
||||
@@ -661,12 +731,9 @@ impl GlowWinitRunning<'_> {
|
||||
// The update function, which could call immediate viewports,
|
||||
// so make sure we don't hold any locks here required by the immediate viewports rendeer.
|
||||
|
||||
let full_output = self.integration.update(
|
||||
self.app.as_mut(),
|
||||
viewport_ui_cb.as_deref(),
|
||||
raw_input,
|
||||
run_ui,
|
||||
);
|
||||
let full_output =
|
||||
self.integration
|
||||
.update(self.app.as_mut(), viewport_ui_cb.as_deref(), raw_input);
|
||||
|
||||
// ------------------------------------------------------------
|
||||
|
||||
@@ -800,14 +867,7 @@ impl GlowWinitRunning<'_> {
|
||||
|
||||
integration.maybe_autosave(app.as_mut(), Some(&window));
|
||||
|
||||
if is_invisible_or_minimized(&window) {
|
||||
// On Mac, a minimized Window uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/325
|
||||
// On Windows, an invisible window also uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/7776
|
||||
profiling::scope!("minimized_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
sleep_if_invisible_or_minimized(Some(&window));
|
||||
|
||||
if integration.should_close() {
|
||||
Ok(EventResult::CloseRequested)
|
||||
@@ -1380,7 +1440,7 @@ impl GlutinWindowContext {
|
||||
class,
|
||||
builder,
|
||||
viewport_ui_cb,
|
||||
mut commands,
|
||||
commands,
|
||||
repaint_delay: _, // ignored - we listened to the repaint callback instead
|
||||
},
|
||||
) in viewport_output.clone()
|
||||
@@ -1395,25 +1455,18 @@ impl GlutinWindowContext {
|
||||
viewport_ui_cb,
|
||||
);
|
||||
|
||||
if let Some(window) = &viewport.window {
|
||||
let old_inner_size = window.inner_size();
|
||||
let old_inner_size = viewport.window.as_ref().map(|window| window.inner_size());
|
||||
|
||||
viewport.deferred_commands.append(&mut commands);
|
||||
viewport.process_commands(egui_ctx, commands);
|
||||
|
||||
egui_winit::process_viewport_commands(
|
||||
egui_ctx,
|
||||
&mut viewport.info,
|
||||
std::mem::take(&mut viewport.deferred_commands),
|
||||
window,
|
||||
&mut viewport.actions_requested,
|
||||
);
|
||||
|
||||
// For Wayland : https://github.com/emilk/egui/issues/4196
|
||||
if cfg!(target_os = "linux") {
|
||||
let new_inner_size = window.inner_size();
|
||||
if new_inner_size != old_inner_size {
|
||||
self.resize(viewport_id, new_inner_size);
|
||||
}
|
||||
// For Wayland : https://github.com/emilk/egui/issues/4196
|
||||
if cfg!(target_os = "linux")
|
||||
&& let Some(window) = &viewport.window
|
||||
&& let Some(old_inner_size) = old_inner_size
|
||||
{
|
||||
let new_inner_size = window.inner_size();
|
||||
if new_inner_size != old_inner_size {
|
||||
self.resize(viewport_id, new_inner_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ use crate::{
|
||||
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
||||
native::{
|
||||
epi_integration::EpiIntegration,
|
||||
winit_integration::{EventResult, is_invisible_or_minimized},
|
||||
winit_integration::{EventResult, sleep_if_invisible_or_minimized},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -624,7 +624,7 @@ impl WgpuWinitRunning<'_> {
|
||||
let mut frame_timer = crate::stopwatch::Stopwatch::new();
|
||||
frame_timer.start();
|
||||
|
||||
let (viewport_ui_cb, raw_input, is_visible, run_ui) = {
|
||||
let (viewport_ui_cb, raw_input, is_visible, show_ui) = {
|
||||
profiling::scope!("Prepare");
|
||||
let mut shared_lock = shared.borrow_mut();
|
||||
|
||||
@@ -680,7 +680,7 @@ impl WgpuWinitRunning<'_> {
|
||||
};
|
||||
let mut raw_input = egui_winit.take_egui_input(window);
|
||||
|
||||
let run_ui = is_visible || is_viewport_or_descendant_visible(viewports, viewport_id);
|
||||
let show_ui = is_visible || is_viewport_or_descendant_visible(viewports, viewport_id);
|
||||
|
||||
integration.pre_update();
|
||||
|
||||
@@ -692,15 +692,67 @@ impl WgpuWinitRunning<'_> {
|
||||
|
||||
painter.handle_screenshots(&mut raw_input.events);
|
||||
|
||||
(viewport_ui_cb, raw_input, is_visible, run_ui)
|
||||
(viewport_ui_cb, raw_input, is_visible, show_ui)
|
||||
};
|
||||
|
||||
if !show_ui {
|
||||
// Nothing will be shown, so we run no egui pass at all.
|
||||
// That way all ui state is left untouched, and is still there
|
||||
// when this viewport becomes visible again.
|
||||
let is_root_viewport = viewport_ui_cb.is_none();
|
||||
if is_root_viewport {
|
||||
// The app logic keeps ticking, so it can e.g. ask to be shown again:
|
||||
let egui::LogicOutput {
|
||||
platform_output,
|
||||
viewport_commands,
|
||||
} = integration.update_logic_only(app.as_mut(), raw_input);
|
||||
|
||||
let mut shared_mut = shared.borrow_mut();
|
||||
let SharedState { viewports, .. } = &mut *shared_mut;
|
||||
|
||||
if let Some(viewport) = viewports.get_mut(&viewport_id) {
|
||||
viewport.info.events.clear(); // they should have been processed
|
||||
if let Viewport {
|
||||
window: Some(window),
|
||||
egui_winit: Some(egui_winit),
|
||||
..
|
||||
} = viewport
|
||||
{
|
||||
egui_winit.handle_platform_output_with_event_loop(
|
||||
window,
|
||||
event_loop,
|
||||
platform_output,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (id, commands) in viewport_commands {
|
||||
if let Some(viewport) = viewports.get_mut(&id) {
|
||||
viewport.process_commands(&integration.egui_ctx, commands);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sleep_if_invisible_or_minimized(
|
||||
shared
|
||||
.borrow()
|
||||
.viewports
|
||||
.get(&viewport_id)
|
||||
.and_then(|viewport| viewport.window.as_deref()),
|
||||
);
|
||||
|
||||
return Ok(if integration.should_close() {
|
||||
EventResult::CloseRequested
|
||||
} else {
|
||||
EventResult::Wait
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
|
||||
// Runs the update, which could call immediate viewports,
|
||||
// so make sure we hold no locks here!
|
||||
let full_output =
|
||||
integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input, run_ui);
|
||||
let full_output = integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input);
|
||||
|
||||
// ------------------------------------------------------------
|
||||
|
||||
@@ -821,16 +873,7 @@ impl WgpuWinitRunning<'_> {
|
||||
|
||||
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
|
||||
|
||||
if let Some(window) = window
|
||||
&& is_invisible_or_minimized(window)
|
||||
{
|
||||
// On Mac, a minimized Window uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/325
|
||||
// On Windows, an invisible window also uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/7776
|
||||
profiling::scope!("minimized_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
sleep_if_invisible_or_minimized(window.map(|window| window.as_ref()));
|
||||
|
||||
if integration.should_close() {
|
||||
Ok(EventResult::CloseRequested)
|
||||
@@ -985,6 +1028,25 @@ impl WgpuWinitRunning<'_> {
|
||||
}
|
||||
|
||||
impl Viewport {
|
||||
/// Apply the commands, or defer them until we have a window.
|
||||
fn process_commands(
|
||||
&mut self,
|
||||
egui_ctx: &egui::Context,
|
||||
mut commands: Vec<egui::ViewportCommand>,
|
||||
) {
|
||||
self.deferred_commands.append(&mut commands);
|
||||
|
||||
if let Some(window) = self.window.as_ref() {
|
||||
egui_winit::process_viewport_commands(
|
||||
egui_ctx,
|
||||
&mut self.info,
|
||||
std::mem::take(&mut self.deferred_commands),
|
||||
window,
|
||||
&mut self.actions_requested,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create winit window, if needed.
|
||||
fn initialize_window(
|
||||
&mut self,
|
||||
@@ -1222,7 +1284,7 @@ fn handle_viewport_output(
|
||||
class,
|
||||
builder,
|
||||
viewport_ui_cb,
|
||||
mut commands,
|
||||
commands,
|
||||
repaint_delay: _, // ignored - we listened to the repaint callback instead
|
||||
},
|
||||
) in viewport_output.clone()
|
||||
@@ -1232,30 +1294,23 @@ fn handle_viewport_output(
|
||||
let viewport =
|
||||
initialize_or_update_viewport(viewports, ids, class, builder, viewport_ui_cb, painter);
|
||||
|
||||
if let Some(window) = viewport.window.as_ref() {
|
||||
let old_inner_size = window.inner_size();
|
||||
let old_inner_size = viewport.window.as_ref().map(|window| window.inner_size());
|
||||
|
||||
viewport.deferred_commands.append(&mut commands);
|
||||
viewport.process_commands(egui_ctx, commands);
|
||||
|
||||
egui_winit::process_viewport_commands(
|
||||
egui_ctx,
|
||||
&mut viewport.info,
|
||||
std::mem::take(&mut viewport.deferred_commands),
|
||||
window,
|
||||
&mut viewport.actions_requested,
|
||||
);
|
||||
|
||||
// For Wayland : https://github.com/emilk/egui/issues/4196
|
||||
if cfg!(target_os = "linux") {
|
||||
let new_inner_size = window.inner_size();
|
||||
if new_inner_size != old_inner_size
|
||||
&& let (Some(width), Some(height)) = (
|
||||
NonZeroU32::new(new_inner_size.width),
|
||||
NonZeroU32::new(new_inner_size.height),
|
||||
)
|
||||
{
|
||||
painter.on_window_resized(viewport_id, width, height);
|
||||
}
|
||||
// For Wayland : https://github.com/emilk/egui/issues/4196
|
||||
if cfg!(target_os = "linux")
|
||||
&& let Some(window) = viewport.window.as_ref()
|
||||
&& let Some(old_inner_size) = old_inner_size
|
||||
{
|
||||
let new_inner_size = window.inner_size();
|
||||
if new_inner_size != old_inner_size
|
||||
&& let (Some(width), Some(height)) = (
|
||||
NonZeroU32::new(new_inner_size.width),
|
||||
NonZeroU32::new(new_inner_size.height),
|
||||
)
|
||||
{
|
||||
painter.on_window_resized(viewport_id, width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,18 @@ pub fn is_invisible_or_minimized(window: &Window) -> bool {
|
||||
window.is_visible() == Some(false) || window.is_minimized() == Some(true)
|
||||
}
|
||||
|
||||
/// On Mac, a minimized window uses up all CPU:
|
||||
/// <https://github.com/emilk/egui/issues/325>
|
||||
///
|
||||
/// On Windows, an invisible window also uses up all CPU:
|
||||
/// <https://github.com/emilk/egui/issues/7776>
|
||||
pub fn sleep_if_invisible_or_minimized(window: Option<&Window>) {
|
||||
if window.is_some_and(is_invisible_or_minimized) {
|
||||
profiling::scope!("minimized_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an egui context, restoring it from storage if possible.
|
||||
pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context {
|
||||
profiling::function_scope!();
|
||||
|
||||
@@ -280,45 +280,65 @@ impl AppRunner {
|
||||
.and_then(|v| v.visible())
|
||||
.unwrap_or(true);
|
||||
|
||||
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
|
||||
self.app.logic(ui.ctx(), &mut self.frame);
|
||||
|
||||
if is_visible {
|
||||
if is_visible {
|
||||
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
|
||||
self.app.logic(ui.ctx(), &mut self.frame);
|
||||
self.app.ui(ui, &mut self.frame);
|
||||
}
|
||||
});
|
||||
let egui::FullOutput {
|
||||
platform_output,
|
||||
textures_delta,
|
||||
shapes,
|
||||
pixels_per_point,
|
||||
viewport_output,
|
||||
} = full_output;
|
||||
});
|
||||
let egui::FullOutput {
|
||||
platform_output,
|
||||
textures_delta,
|
||||
shapes,
|
||||
pixels_per_point,
|
||||
viewport_output,
|
||||
} = full_output;
|
||||
|
||||
if viewport_output.len() > 1 {
|
||||
log::warn!("Multiple viewports not yet supported on the web");
|
||||
}
|
||||
for (_viewport_id, viewport_output) in viewport_output {
|
||||
for command in viewport_output.commands {
|
||||
match command {
|
||||
ViewportCommand::Screenshot(user_data) => {
|
||||
self.screenshot_commands_with_frame_delay
|
||||
.push((user_data, 1));
|
||||
}
|
||||
_ => {
|
||||
// TODO(emilk): handle some of the commands
|
||||
log::warn!(
|
||||
"Unhandled egui viewport command: {command:?} - not implemented in web backend"
|
||||
);
|
||||
}
|
||||
}
|
||||
if viewport_output.len() > 1 {
|
||||
log::warn!("Multiple viewports not yet supported on the web");
|
||||
}
|
||||
}
|
||||
self.handle_viewport_commands(
|
||||
viewport_output
|
||||
.into_values()
|
||||
.flat_map(|viewport_output| viewport_output.commands),
|
||||
);
|
||||
|
||||
self.handle_platform_output(platform_output);
|
||||
if is_visible || !textures_delta.is_empty() {
|
||||
self.handle_platform_output(platform_output);
|
||||
self.textures_delta.append(textures_delta);
|
||||
self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point));
|
||||
} else {
|
||||
// The tab is hidden, so we run no egui pass at all.
|
||||
// That way all ui state is left untouched, and is still there
|
||||
// when the tab is shown again.
|
||||
|
||||
let egui::LogicOutput {
|
||||
platform_output,
|
||||
viewport_commands,
|
||||
} = self.egui_ctx.run_logic(&raw_input, |ctx| {
|
||||
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_platform_output(platform_output);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_viewport_commands(&mut self, commands: impl Iterator<Item = ViewportCommand>) {
|
||||
for command in commands {
|
||||
match command {
|
||||
ViewportCommand::Screenshot(user_data) => {
|
||||
self.screenshot_commands_with_frame_delay
|
||||
.push((user_data, 1));
|
||||
}
|
||||
_ => {
|
||||
// TODO(emilk): handle some of the commands
|
||||
log::warn!(
|
||||
"Unhandled egui viewport command: {command:?} - not implemented in web backend"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ use crate::{
|
||||
load::{self, Bytes, Loaders, SizedTexture},
|
||||
memory::{Options, Theme},
|
||||
os::OperatingSystem,
|
||||
output::FullOutput,
|
||||
output::{FullOutput, LogicOutput},
|
||||
pass_state::PassState,
|
||||
plugin::{self, TypedPluginHandle},
|
||||
resize, response, scroll_area,
|
||||
@@ -888,6 +888,57 @@ impl Context {
|
||||
output
|
||||
}
|
||||
|
||||
/// Run app logic without showing any ui.
|
||||
///
|
||||
/// Use this instead of [`Self::run_ui`] when nothing will be shown,
|
||||
/// e.g. because the window is minimized or occluded,
|
||||
/// but you still want to let the app tick its logic
|
||||
/// (so that it can e.g. ask to be shown again with [`ViewportCommand::Focus`]).
|
||||
///
|
||||
/// No pass is run, so `f` must not show any ui.
|
||||
/// This means everything egui knows about the ui is left untouched:
|
||||
/// no widget state is garbage-collected, no animation advances,
|
||||
/// and nothing loses focus.
|
||||
///
|
||||
/// Of `new_input`, only the window state ([`RawInput::viewports`] and
|
||||
/// [`RawInput::focused`]) is used, so that `f` can tell that the window is hidden.
|
||||
/// The ui input (events, time, …) is _not_ interpreted, and is left for the next
|
||||
/// call to [`Self::run_ui`]: [`Self::input`] is otherwise still that of the last pass.
|
||||
///
|
||||
/// The returned [`LogicOutput`] is what [`FullOutput`] would have carried:
|
||||
/// anything `f` asked the integration to do.
|
||||
/// There is nothing to paint.
|
||||
#[must_use]
|
||||
pub fn run_logic(&self, new_input: &RawInput, logic: impl FnOnce(&Self)) -> LogicOutput {
|
||||
profiling::function_scope!();
|
||||
|
||||
let viewport_id = new_input.viewport_id;
|
||||
|
||||
self.write(|ctx| {
|
||||
// Consume any outstanding repaint request, so that a new request from `logic`
|
||||
// reaches the integration instead of being considered already served:
|
||||
ctx.begin_pass_repaint_logic(viewport_id);
|
||||
|
||||
// Tell `logic` 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;
|
||||
});
|
||||
|
||||
logic(self);
|
||||
|
||||
self.write(|ctx| LogicOutput {
|
||||
platform_output: std::mem::take(&mut ctx.viewport_for(viewport_id).output),
|
||||
viewport_commands: ctx
|
||||
.viewports
|
||||
.iter_mut()
|
||||
.filter(|(_, viewport)| !viewport.commands.is_empty())
|
||||
.map(|(&id, viewport)| (id, std::mem::take(&mut viewport.commands)))
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
/// An alternative to calling [`Self::run_ui`].
|
||||
///
|
||||
/// It is usually better to use [`Self::run_ui`], because
|
||||
|
||||
@@ -76,6 +76,22 @@ impl FullOutput {
|
||||
}
|
||||
}
|
||||
|
||||
/// What egui emits from [`crate::Context::run_logic`], i.e. from a tick where no ui was shown.
|
||||
///
|
||||
/// There is nothing to paint, but the app may still have asked the integration to do things,
|
||||
/// e.g. to show a hidden window again with [`crate::ViewportCommand::Focus`].
|
||||
#[derive(Clone, Default)]
|
||||
pub struct LogicOutput {
|
||||
/// Non-rendering related output.
|
||||
pub platform_output: PlatformOutput,
|
||||
|
||||
/// The commands sent with [`crate::Context::send_viewport_cmd`] and friends.
|
||||
///
|
||||
/// Note that this contains no information about which viewports exist:
|
||||
/// the integration should leave its viewports as they are.
|
||||
pub viewport_commands: OrderedViewportIdMap<Vec<crate::ViewportCommand>>,
|
||||
}
|
||||
|
||||
/// Information about text being edited.
|
||||
///
|
||||
/// Useful for IME.
|
||||
|
||||
@@ -467,7 +467,7 @@ pub use self::{
|
||||
Key, UserData,
|
||||
input::*,
|
||||
output::{
|
||||
self, CursorIcon, CustomCursorImage, FullOutput, OpenUrl, OutputCommand,
|
||||
self, CursorIcon, CustomCursorImage, FullOutput, LogicOutput, OpenUrl, OutputCommand,
|
||||
PlatformOutput, UserAttentionType, WidgetInfo,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user