diff --git a/Cargo.lock b/Cargo.lock index 9140f2160..eb9a385b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4303,6 +4303,14 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test_background_logic" +version = "0.1.0" +dependencies = [ + "eframe", + "env_logger", +] + [[package]] name = "test_egui_extras_compilation" version = "0.1.0" diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 48557675c..4cda3e762 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -822,6 +822,14 @@ impl GlowWinitRunning<'_> { } } + winit::event::WindowEvent::Occluded(is_occluded) => { + if let Some(viewport_id) = viewport_id + && let Some(viewport) = glutin.viewports.get_mut(&viewport_id) + { + viewport.info.occluded = Some(*is_occluded); + } + } + winit::event::WindowEvent::CloseRequested => { if viewport_id == Some(ViewportId::ROOT) && self.integration.should_close() { log::debug!( diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index 7cfdab148..d4dfeb45d 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -852,6 +852,14 @@ impl WgpuWinitRunning<'_> { } } + winit::event::WindowEvent::Occluded(is_occluded) => { + if let Some(viewport_id) = viewport_id + && let Some(viewport) = shared.viewports.get_mut(&viewport_id) + { + viewport.info.occluded = Some(*is_occluded); + } + } + winit::event::WindowEvent::CloseRequested => { if viewport_id == Some(ViewportId::ROOT) && integration.should_close() { log::debug!( diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index 4814fa99b..e2724fc49 100644 --- a/crates/eframe/src/web/backend.rs +++ b/crates/eframe/src/web/backend.rs @@ -31,11 +31,18 @@ impl WebInput { time: Some(super::now_sec()), ..self.raw.take() }; - raw_input + let viewport = raw_input .viewports .entry(egui::ViewportId::ROOT) - .or_default() - .native_pixels_per_point = Some(super::native_pixels_per_point()); + .or_default(); + viewport.native_pixels_per_point = Some(super::native_pixels_per_point()); + + // A hidden browser tab is effectively occluded. + let hidden = web_sys::window() + .and_then(|w| w.document()) + .is_some_and(|doc| doc.hidden()); + viewport.occluded = Some(hidden); + raw_input } diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 787867569..a52d40233 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -253,9 +253,28 @@ pub struct ViewportInfo { /// /// This should be the same as [`RawInput::focused`]. pub focused: Option, + + /// Is the window fully occluded (completely covered) by another window? + /// + /// Not all platforms support this. + /// On platforms that don't, this will be `None` or `Some(false)`. + pub occluded: Option, } impl ViewportInfo { + /// Is the window considered visible for rendering purposes? + /// + /// A window is not visible if it is minimized or occluded. + /// When not visible, the UI is not painted and rendering is skipped, + /// but application logic may still be executed by some integrations. + pub fn visible(&self) -> Option { + match (self.minimized, self.occluded) { + (Some(true), _) | (_, Some(true)) => Some(false), + (Some(false), Some(false)) => Some(true), + (_, None) | (None, _) => None, + } + } + /// This viewport has been told to close. /// /// If this is the root viewport, the application will exit @@ -282,6 +301,7 @@ impl ViewportInfo { maximized: self.maximized, fullscreen: self.fullscreen, focused: self.focused, + occluded: self.occluded, } } @@ -298,6 +318,7 @@ impl ViewportInfo { maximized, fullscreen, focused, + occluded, } = self; crate::Grid::new("viewport_info").show(ui, |ui| { @@ -345,6 +366,16 @@ impl ViewportInfo { ui.label(opt_as_str(focused)); ui.end_row(); + ui.label("Occluded:"); + ui.label(opt_as_str(occluded)); + ui.end_row(); + + let visible = self.visible(); + + ui.label("Visible:"); + ui.label(opt_as_str(&visible)); + ui.end_row(); + fn opt_rect_as_string(v: &Option) -> String { v.as_ref().map_or(String::new(), |r| { format!("Pos: {:?}, size: {:?}", r.min, r.size()) diff --git a/tests/test_background_logic/Cargo.toml b/tests/test_background_logic/Cargo.toml new file mode 100644 index 000000000..92985d5ee --- /dev/null +++ b/tests/test_background_logic/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "test_background_logic" +version = "0.1.0" +license = "MIT OR Apache-2.0" +edition = "2024" +rust-version = "1.92" +publish = false + +[lints] +workspace = true + +[dependencies] +eframe = { workspace = true, features = ["default"] } +env_logger = { workspace = true, features = ["auto-color", "humantime"] } diff --git a/tests/test_background_logic/src/main.rs b/tests/test_background_logic/src/main.rs new file mode 100644 index 000000000..ea80cfa9e --- /dev/null +++ b/tests/test_background_logic/src/main.rs @@ -0,0 +1,64 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +#![expect(rustdoc::missing_crate_level_docs)] +#![allow(clippy::print_stderr)] + +use std::time::Duration; + +use eframe::egui::{self, ViewportInfo}; + +fn main() { + env_logger::init(); + + let _ = eframe::run_native( + "Background Logic Test", + eframe::NativeOptions { + viewport: egui::ViewportBuilder::default().with_inner_size([400.0, 200.0]), + ..Default::default() + }, + Box::new(|_cc| Ok(Box::new(App))), + ); +} + +struct App; + +impl eframe::App for App { + fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + eprintln!("App::logic called {}", viewport_info(ctx)); + ctx.request_repaint_after(Duration::from_secs(1)); + } + + fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { + eprintln!("App::ui called {}", viewport_info(ui.ctx())); + ui.centered_and_justified(|ui| { + ui.heading("Minimize this window"); + }); + } +} + +fn viewport_info(ctx: &egui::Context) -> String { + ctx.input(|i| { + let ViewportInfo { + minimized, + focused, + occluded, + .. + } = i.viewport(); + + let visible = i.viewport().visible(); + + let mut s = String::new(); + + let flags = [ + ("focused", focused), + ("occluded", occluded), + ("minimized", minimized), + ("visible", &visible), + ]; + for (name, value) in flags { + if let Some(value) = value { + s += &format!(" {name}={value}"); + } + } + s + }) +}