mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-10-27 12:16:46 +00:00
fix: bring hidden or covered bubbles back every minute
Another always-on-top window, or a shell action that hid ours, could leave a bubble covered or hidden until restart. The fullscreen check thread now shows every ready bubble again and puts it back at the top of the always-on-top band once a minute, unless the user or a fullscreen app has them hidden. Windows re-asserts HWND_TOPMOST without activating; other platforms drop and re-set always-on-top.
This commit is contained in:
1 parent
97100cfcc0
commit
5362e46be5
5 files changed
+82
-4
No files matched your search
@@ -45,6 +45,8 @@ self-updater are all written from scratch against the same public APIs
|
||||
expires and after an update was applied
|
||||
- Auto-hide when a fullscreen app is in the foreground (games, video,
|
||||
presentations) — reappears when you leave fullscreen
|
||||
- Every minute, shown bubbles are put back on top, so another
|
||||
always-on-top window can't keep them covered or hidden
|
||||
|
||||
## Who this is for
|
||||
|
||||
|
||||
@@ -489,6 +489,45 @@ pub fn apply(app: &AppHandle, action: Action) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Show every ready bubble again and put it back on top, unless the
|
||||
/// bubbles are meant to be hidden. Another always-on-top window, or a shell
|
||||
/// action that hid ours, can otherwise leave a bubble covered or hidden for
|
||||
/// good. A bubble still waiting for its page is left to the ready handshake.
|
||||
/// Visibility is read on the main thread, after any hide queued before.
|
||||
pub fn bring_back(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) {
|
||||
if with_pending(|p| p.contains_key(window.label())) {
|
||||
continue;
|
||||
}
|
||||
show(&window);
|
||||
raise(&window);
|
||||
}
|
||||
});
|
||||
if let Err(err) = queued {
|
||||
log::warn!("bubble bring-back could not reach the main thread: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn raise(window: &WebviewWindow) {
|
||||
#[cfg(windows)]
|
||||
let result = platform::windows::raise_topmost(window).map_err(|e| e.to_string());
|
||||
// Re-setting the same level is a no-op, so drop it and set it again.
|
||||
#[cfg(not(windows))]
|
||||
let result = window
|
||||
.set_always_on_top(false)
|
||||
.and_then(|()| window.set_always_on_top(true))
|
||||
.map_err(|e| e.to_string());
|
||||
if let Err(err) = result {
|
||||
log::warn!("{} not raised: {err}", window.label());
|
||||
}
|
||||
}
|
||||
|
||||
/// Make every bubble ignore the mouse, or take it again.
|
||||
pub fn set_click_through(app: &AppHandle, ignore: bool) {
|
||||
let handle = app.clone();
|
||||
|
||||
+12
-1
@@ -2,10 +2,13 @@
|
||||
//! fullscreen window of another process is in the foreground, and hides the
|
||||
//! bubbles while one is. They come back without activation when it leaves,
|
||||
//! unless the user hid them in the meantime.
|
||||
//!
|
||||
//! The same thread brings the bubbles back every minute while they should
|
||||
//! show, in case another window covered or hid them.
|
||||
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::core::visibility::Event;
|
||||
use crate::platform::{self, FullscreenCheck};
|
||||
@@ -14,13 +17,21 @@ use crate::runtime::Runtime;
|
||||
/// How often the foreground window is checked.
|
||||
pub const CHECK_INTERVAL: Duration = Duration::from_millis(1500);
|
||||
|
||||
/// How often shown bubbles are brought back on top.
|
||||
pub const BRING_BACK_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
/// Start the check thread. It runs until the process starts exiting.
|
||||
pub fn spawn(runtime: Arc<Runtime>) -> io::Result<()> {
|
||||
std::thread::Builder::new()
|
||||
.name("fullscreen".into())
|
||||
.spawn(move || {
|
||||
let mut last_bring_back = Instant::now();
|
||||
while !runtime.is_exiting() {
|
||||
tick(&runtime);
|
||||
if last_bring_back.elapsed() >= BRING_BACK_INTERVAL {
|
||||
last_bring_back = Instant::now();
|
||||
runtime.bring_back_bubbles();
|
||||
}
|
||||
std::thread::sleep(CHECK_INTERVAL);
|
||||
}
|
||||
})
|
||||
|
||||
+23
-3
@@ -28,9 +28,9 @@ use windows::Win32::UI::Shell::{
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
GetClassNameW, GetForegroundWindow, GetWindowLongPtrW, GetWindowRect, GetWindowThreadProcessId,
|
||||
IsIconic, IsWindowVisible, MessageBoxW, SetForegroundWindow, SetWindowLongPtrW, SetWindowPos,
|
||||
ShowWindow, GWL_EXSTYLE, GWL_STYLE, MA_NOACTIVATE, MB_ICONERROR, MB_OK, MB_SETFOREGROUND,
|
||||
SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, SW_HIDE,
|
||||
SW_SHOWNOACTIVATE, WM_DISPLAYCHANGE, WM_EXITSIZEMOVE, WM_MOUSEACTIVATE, WM_NCDESTROY,
|
||||
ShowWindow, GWL_EXSTYLE, GWL_STYLE, HWND_TOPMOST, MA_NOACTIVATE, MB_ICONERROR, MB_OK,
|
||||
MB_SETFOREGROUND, SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER,
|
||||
SW_HIDE, SW_SHOWNOACTIVATE, WM_DISPLAYCHANGE, WM_EXITSIZEMOVE, WM_MOUSEACTIVATE, WM_NCDESTROY,
|
||||
WM_SETTINGCHANGE, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW,
|
||||
};
|
||||
|
||||
@@ -293,6 +293,26 @@ pub fn show_no_activate(window: &tauri::WebviewWindow) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Put a bubble back at the top of the always-on-top band, above any
|
||||
/// topmost window that came after it, without activating it.
|
||||
pub fn raise_topmost(window: &tauri::WebviewWindow) -> io::Result<()> {
|
||||
let hwnd = hwnd_of(window)?;
|
||||
// SAFETY: `hwnd` is a live window of this process; nothing moves,
|
||||
// resizes or activates.
|
||||
unsafe {
|
||||
SetWindowPos(
|
||||
hwnd,
|
||||
Some(HWND_TOPMOST),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE,
|
||||
)
|
||||
}
|
||||
.map_err(io::Error::from)
|
||||
}
|
||||
|
||||
/// Hide a bubble (see [`show_no_activate`]).
|
||||
pub fn hide(window: &tauri::WebviewWindow) -> io::Result<()> {
|
||||
let hwnd = hwnd_of(window)?;
|
||||
|
||||
@@ -506,6 +506,12 @@ impl Runtime {
|
||||
bubble::set_click_through(&self.ui.app, ignore);
|
||||
}
|
||||
|
||||
/// Show the bubbles again and put them back on top, unless they are
|
||||
/// meant to be hidden.
|
||||
pub fn bring_back_bubbles(&self) {
|
||||
bubble::bring_back(&self.ui.app);
|
||||
}
|
||||
|
||||
/// Apply a visibility event to every bubble. A user show or hide is
|
||||
/// also saved as `widget_visible`.
|
||||
pub fn apply_visibility(&self, event: visibility::Event) {
|
||||
|
||||
Reference in new issue
Block a user