mirror of
https://github.com/emilk/egui.git
synced 2026-06-27 15:13:12 -04:00
## Summary * Closes #5229 * Closes #7776 On Windows, once a window is hidden with `ViewportCommand::Visible(false)`, two problems occur: 1. **Window can never be shown again** — Windows stops sending `RedrawRequested` events to invisible windows, and viewport commands are only processed during `run_ui_and_paint`, which is triggered by `RedrawRequested`. This creates a deadlock: ``` Visible(false) → window hidden → no RedrawRequested → run_ui_and_paint never called → Visible(true) stuck in queue → window stays hidden forever ``` 2. **High CPU usage** — The event loop spins at full speed with `ControlFlow::Poll` even for invisible windows, and repaint requests are scheduled immediately, causing a tight loop that burns CPU. ## Fix **For #5229:** In `check_redraw_requests`, after calling `window.request_redraw()`, detect invisible windows via `window.is_visible() == Some(false)` and call `run_ui_and_paint` directly for them. This ensures pending viewport commands (including `Visible(true)`) are still processed even when the OS doesn't send redraw events. **For #7776:** Three layers of throttling for invisible windows: - **Heartbeat scheduling:** After painting an invisible window, schedule the next repaint 100ms in the future (instead of immediately). This keeps viewport commands flowing while limiting to ~10 repaints/sec. - **Event throttling:** In `user_event`, throttle `RequestRepaint` events for invisible windows to at least 100ms delay, preventing egui's repaint callback from bypassing the heartbeat. - **ControlFlow fix:** Only set `ControlFlow::Poll` for visible windows. Invisible windows use `WaitUntil` instead of spinning. - **Backend sleep:** Add `is_visible() == Some(false)` alongside the existing `is_minimized()` sleep check in both wgpu and glow backends (defense in depth). The fix is platform-agnostic: `is_visible()` returns `Some(false)` only when the platform can confirm invisibility, so it won't trigger on platforms where invisible windows still receive `RedrawRequested`. ## Test plan - [x] `cargo fmt` passes - [x] `cargo clippy -p eframe --all-features` passes with no warnings - [x] Manual test on Windows: window reappears after `Visible(true)` when hidden - [x] Manual test on Windows: CPU stays near 0% while window is invisible (was ~16% before fix) --------- Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
192 lines
6.2 KiB
Rust
192 lines
6.2 KiB
Rust
use std::{sync::Arc, time::Instant};
|
|
|
|
use winit::{
|
|
event_loop::ActiveEventLoop,
|
|
window::{Window, WindowId},
|
|
};
|
|
|
|
use egui::ViewportId;
|
|
#[cfg(feature = "accesskit")]
|
|
use egui_winit::accesskit_winit;
|
|
|
|
/// Returns `true` if the window is invisible or minimized.
|
|
///
|
|
/// These windows don't receive `RedrawRequested` events on Windows,
|
|
/// so they need special handling to keep processing viewport commands.
|
|
pub fn is_invisible_or_minimized(window: &Window) -> bool {
|
|
window.is_visible() == Some(false) || window.is_minimized() == Some(true)
|
|
}
|
|
|
|
/// 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!();
|
|
|
|
pub const IS_DESKTOP: bool = cfg!(any(
|
|
target_os = "freebsd",
|
|
target_os = "linux",
|
|
target_os = "macos",
|
|
target_os = "openbsd",
|
|
target_os = "windows",
|
|
));
|
|
|
|
let egui_ctx = egui::Context::default();
|
|
|
|
egui_ctx.set_embed_viewports(!IS_DESKTOP);
|
|
|
|
egui_ctx.options_mut(|o| {
|
|
// eframe supports multi-pass (Context::request_discard).
|
|
#[expect(clippy::unwrap_used)]
|
|
{
|
|
o.max_passes = 2.try_into().unwrap();
|
|
}
|
|
});
|
|
|
|
let memory = crate::native::epi_integration::load_egui_memory(storage).unwrap_or_default();
|
|
egui_ctx.memory_mut(|mem| *mem = memory);
|
|
|
|
egui_ctx
|
|
}
|
|
|
|
/// The custom even `eframe` uses with the [`winit`] event loop.
|
|
#[derive(Debug)]
|
|
pub enum UserEvent {
|
|
/// A repaint is requested.
|
|
RequestRepaint {
|
|
/// What to repaint.
|
|
viewport_id: ViewportId,
|
|
|
|
/// When to repaint.
|
|
when: Instant,
|
|
|
|
/// What the cumulative pass number was when the repaint was _requested_.
|
|
cumulative_pass_nr: u64,
|
|
},
|
|
|
|
/// A request related to [`accesskit`](https://accesskit.dev/).
|
|
#[cfg(feature = "accesskit")]
|
|
AccessKitActionRequest(accesskit_winit::Event),
|
|
}
|
|
|
|
#[cfg(feature = "accesskit")]
|
|
impl From<accesskit_winit::Event> for UserEvent {
|
|
fn from(inner: accesskit_winit::Event) -> Self {
|
|
Self::AccessKitActionRequest(inner)
|
|
}
|
|
}
|
|
|
|
pub trait WinitApp {
|
|
fn egui_ctx(&self) -> Option<&egui::Context>;
|
|
|
|
fn window(&self, window_id: WindowId) -> Option<Arc<Window>>;
|
|
|
|
fn window_id_from_viewport_id(&self, id: ViewportId) -> Option<WindowId>;
|
|
|
|
fn save(&mut self);
|
|
|
|
fn save_and_destroy(&mut self);
|
|
|
|
fn run_ui_and_paint(
|
|
&mut self,
|
|
event_loop: &ActiveEventLoop,
|
|
window_id: WindowId,
|
|
) -> crate::Result<EventResult>;
|
|
|
|
fn suspended(&mut self, event_loop: &ActiveEventLoop) -> crate::Result<EventResult>;
|
|
|
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) -> crate::Result<EventResult>;
|
|
|
|
fn device_event(
|
|
&mut self,
|
|
event_loop: &ActiveEventLoop,
|
|
device_id: winit::event::DeviceId,
|
|
event: winit::event::DeviceEvent,
|
|
) -> crate::Result<EventResult>;
|
|
|
|
fn window_event(
|
|
&mut self,
|
|
event_loop: &ActiveEventLoop,
|
|
window_id: WindowId,
|
|
event: winit::event::WindowEvent,
|
|
) -> crate::Result<EventResult>;
|
|
|
|
#[cfg(feature = "accesskit")]
|
|
fn on_accesskit_event(&mut self, event: accesskit_winit::Event) -> crate::Result<EventResult>;
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum EventResult {
|
|
Wait,
|
|
|
|
/// Causes a synchronous repaint inside the event handler. This should only
|
|
/// be used in special situations if the window must be repainted while
|
|
/// handling a specific event. This occurs on Windows when handling resizes.
|
|
///
|
|
/// `RepaintNow` creates a new frame synchronously, and should therefore
|
|
/// only be used for extremely urgent repaints.
|
|
RepaintNow(WindowId),
|
|
|
|
/// Queues a repaint for once the event loop handles its next redraw. Exists
|
|
/// so that multiple input events can be handled in one frame. Does not
|
|
/// cause any delay like `RepaintNow`.
|
|
RepaintNext(WindowId),
|
|
|
|
RepaintAt(WindowId, Instant),
|
|
|
|
/// Causes a save of the client state when the persistence feature is enabled.
|
|
Save,
|
|
|
|
/// Starts the process of ending eframe execution whilst allowing for proper
|
|
/// clean up of resources.
|
|
///
|
|
/// # Warning
|
|
/// This event **must** occur before [`Exit`] to correctly exit eframe code.
|
|
/// If in doubt, return this event.
|
|
///
|
|
/// [`Exit`]: [EventResult::Exit]
|
|
CloseRequested,
|
|
|
|
/// The event loop will exit, now.
|
|
/// The correct circumstance to return this event is in response to a winit "Destroyed" event.
|
|
///
|
|
/// # Warning
|
|
/// The [`CloseRequested`] **must** occur before this event to ensure that winit
|
|
/// is able to remove any open windows. Otherwise the window(s) will remain open
|
|
/// until the program terminates.
|
|
///
|
|
/// [`CloseRequested`]: EventResult::CloseRequested
|
|
Exit,
|
|
}
|
|
|
|
#[cfg(feature = "accesskit")]
|
|
pub(crate) fn on_accesskit_window_event(
|
|
egui_winit: &mut egui_winit::State,
|
|
window_id: WindowId,
|
|
event: &accesskit_winit::WindowEvent,
|
|
) -> EventResult {
|
|
match event {
|
|
accesskit_winit::WindowEvent::InitialTreeRequested => {
|
|
egui_winit.egui_ctx().enable_accesskit();
|
|
// Because we can't provide the initial tree synchronously
|
|
// (because that would require the activation handler to access
|
|
// the same mutable state as the winit event handler), some
|
|
// AccessKit platform adapters will use a placeholder tree
|
|
// until we send the first tree update. To minimize the possible
|
|
// bad effects of that workaround, repaint and send the tree
|
|
// immediately.
|
|
EventResult::RepaintNow(window_id)
|
|
}
|
|
accesskit_winit::WindowEvent::ActionRequested(request) => {
|
|
egui_winit.on_accesskit_action_request(request.clone());
|
|
// As a form of user input, accessibility actions should cause
|
|
// a repaint, but not until the next regular frame.
|
|
EventResult::RepaintNext(window_id)
|
|
}
|
|
accesskit_winit::WindowEvent::AccessibilityDeactivated => {
|
|
egui_winit.egui_ctx().disable_accesskit();
|
|
// Disabling AccessKit support should have no visible effect,
|
|
// so there's no need to repaint.
|
|
EventResult::Wait
|
|
}
|
|
}
|
|
}
|