diff --git a/Cargo.toml b/Cargo.toml
index be6f9fb..ae316db 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -56,6 +56,7 @@ windows = { version = "0.62", features = [
"Win32_Foundation",
"Win32_System_Registry",
"Win32_System_Threading",
+ "Win32_UI_Accessibility",
"Win32_UI_WindowsAndMessaging",
"Win32_UI_Shell",
"Win32_Graphics_Dwm",
diff --git a/README.md b/README.md
index e704993..dbf6a94 100644
--- a/README.md
+++ b/README.md
@@ -46,9 +46,9 @@ self-updater are all written from scratch against the same public APIs
- Auto-hide when a fullscreen app is in the foreground (games, video,
presentations) — reappears when you leave fullscreen
- Bubbles stay above other always-on-top windows, such as chat
- notifications: on Windows a covered bubble is raised again within about
- 1.5 s, and on every platform shown bubbles are put back on top every
- minute, so nothing can keep them covered or hidden
+ notifications: on Windows a bubble goes back on top the moment another
+ app shows such a window over it, and on every platform shown bubbles are
+ put back on top every minute, so nothing can keep them covered or hidden
## Who this is for
diff --git a/src/bubble.rs b/src/bubble.rs
index 68e280f..bbb320d 100644
--- a/src/bubble.rs
+++ b/src/bubble.rs
@@ -15,9 +15,7 @@
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, PoisonError};
-use std::time::Duration;
-#[cfg(not(windows))]
-use std::time::Instant;
+use std::time::{Duration, Instant};
use tauri::{
AppHandle, Manager, Monitor, PhysicalPosition, PhysicalSize, WebviewUrl, WebviewWindow,
@@ -416,6 +414,7 @@ fn prepare_native(
if click_through {
set_ignore_cursor(&window, true);
}
+ watch_z_order(&hook_app);
let hooked = platform::windows::install_bubble_hook(&window, move |event| {
on_hook_event(&hook_app, provider, event);
});
@@ -536,24 +535,69 @@ pub fn bring_back(app: &AppHandle) {
#[cfg(windows)]
pub fn keep_on_top(app: &AppHandle) {
let handle = app.clone();
- let queued = app.run_on_main_thread(move || {
- let shows = runtime_of(&handle).is_some_and(|r| r.with_state(|s| s.visibility.shows()));
- if !shows {
- return;
- }
- for (_, window) in windows(&handle) {
- match platform::windows::covered_by_other_topmost(&window) {
- Ok(true) => raise(&window),
- Ok(false) => {}
- Err(err) => log::warn!("{} on-top check failed: {err}", window.label()),
- }
- }
- });
+ let queued = app.run_on_main_thread(move || raise_covered(&handle));
if let Err(err) = queued {
log::warn!("bubble on-top check could not reach the main thread: {err}");
}
}
+/// Most raises in [`RAISE_WINDOW`] before raising pauses until it ends, so
+/// another app that also fights for the top cannot keep both busy; the
+/// fullscreen check carries on at its own pace.
+#[cfg(windows)]
+const MAX_RAISES: u32 = 20;
+#[cfg(windows)]
+const RAISE_WINDOW: Duration = Duration::from_secs(1);
+
+/// Body of [`keep_on_top`]; runs on the main thread.
+#[cfg(windows)]
+fn raise_covered(app: &AppHandle) {
+ static RAISES: Mutex