mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-10-27 12:16:46 +00:00
fix: keep the bubble visible when click-through is toggled
On Windows, tao's set_ignore_cursor_events re-applies all its window flags. Bubbles are shown through ShowWindow, not tao, so tao believes them hidden: it hid the bubble and rewrote the extended style, dropping its no-activate and tool-window bits. The transparent and layered bits are now set directly on the main thread.
This commit is contained in:
1 parent
fd5c2ba910
commit
4e2ecbf588
2 files changed
+50
-9
No files matched your search
+30
-4
@@ -351,6 +351,8 @@ pub fn create(app: &AppHandle, provider: ProviderId) -> tauri::Result<()> {
|
||||
set_position(&window, placement.pos);
|
||||
}
|
||||
set_size(&window, placement.size);
|
||||
// Windows applies it on the main thread, in `prepare_native`.
|
||||
#[cfg(not(windows))]
|
||||
if click_through {
|
||||
set_ignore_cursor(&window, true);
|
||||
}
|
||||
@@ -363,7 +365,7 @@ pub fn create(app: &AppHandle, provider: ProviderId) -> tauri::Result<()> {
|
||||
placement.dpi
|
||||
);
|
||||
|
||||
prepare_native(app, &window, provider);
|
||||
prepare_native(app, &window, provider, click_through);
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(is_dark) = platform::linux::portal_prefers_dark() {
|
||||
runtime.set_dark(is_dark);
|
||||
@@ -399,13 +401,21 @@ pub fn create(app: &AppHandle, provider: ProviderId) -> tauri::Result<()> {
|
||||
/// move-end, setting-change and display-change messages. Both must run on
|
||||
/// the window's own (main) thread before the first show.
|
||||
#[cfg(windows)]
|
||||
fn prepare_native(app: &AppHandle, window: &WebviewWindow, provider: ProviderId) {
|
||||
fn prepare_native(
|
||||
app: &AppHandle,
|
||||
window: &WebviewWindow,
|
||||
provider: ProviderId,
|
||||
click_through: bool,
|
||||
) {
|
||||
let window = window.clone();
|
||||
let hook_app = app.clone();
|
||||
let queued = app.run_on_main_thread(move || {
|
||||
if let Err(err) = platform::windows::apply_no_activate(&window) {
|
||||
log::warn!("{} may take focus: {err}", window.label());
|
||||
}
|
||||
if click_through {
|
||||
set_ignore_cursor(&window, true);
|
||||
}
|
||||
let hooked = platform::windows::install_bubble_hook(&window, move |event| {
|
||||
on_hook_event(&hook_app, provider, event);
|
||||
});
|
||||
@@ -419,7 +429,13 @@ fn prepare_native(app: &AppHandle, window: &WebviewWindow, provider: ProviderId)
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn prepare_native(_app: &AppHandle, _window: &WebviewWindow, _provider: ProviderId) {}
|
||||
fn prepare_native(
|
||||
_app: &AppHandle,
|
||||
_window: &WebviewWindow,
|
||||
_provider: ProviderId,
|
||||
_click_through: bool,
|
||||
) {
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn on_hook_event(app: &AppHandle, provider: ProviderId, event: platform::windows::BubbleHookEvent) {
|
||||
@@ -565,8 +581,18 @@ pub fn set_click_through(app: &AppHandle, ignore: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
/// On Windows the style bits are set directly: tao's
|
||||
/// `set_ignore_cursor_events` re-applies all its window flags, and since
|
||||
/// bubbles are shown through [`show`], not tao, it believes them hidden and
|
||||
/// hides them, also dropping their no-activate and tool-window styles.
|
||||
fn set_ignore_cursor(window: &WebviewWindow, ignore: bool) {
|
||||
if let Err(err) = window.set_ignore_cursor_events(ignore) {
|
||||
#[cfg(windows)]
|
||||
let result = platform::windows::set_click_through(window, ignore).map_err(|e| e.to_string());
|
||||
#[cfg(not(windows))]
|
||||
let result = window
|
||||
.set_ignore_cursor_events(ignore)
|
||||
.map_err(|e| e.to_string());
|
||||
if let Err(err) = result {
|
||||
log::warn!("{} click-through not set: {err}", window.label());
|
||||
}
|
||||
}
|
||||
|
||||
+20
-5
@@ -31,8 +31,8 @@ use windows::Win32::UI::WindowsAndMessaging::{
|
||||
SetWindowLongPtrW, SetWindowPos, ShowWindow, GWL_EXSTYLE, GWL_STYLE, GW_HWNDPREV, 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, WS_EX_TOPMOST,
|
||||
WM_EXITSIZEMOVE, WM_MOUSEACTIVATE, WM_NCDESTROY, WM_SETTINGCHANGE, WS_EX_LAYERED,
|
||||
WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TOPMOST, WS_EX_TRANSPARENT,
|
||||
};
|
||||
|
||||
use crate::core::geom::{self, Edge, Rect};
|
||||
@@ -203,19 +203,34 @@ pub fn apply_toolwindow(window: &tauri::WebviewWindow) -> io::Result<()> {
|
||||
add_ex_style(window, WS_EX_TOOLWINDOW.0)
|
||||
}
|
||||
|
||||
/// Let mouse input pass through a bubble to the windows below, or take it
|
||||
/// again. These are the bits tao's `set_ignore_cursor_events` sets, without
|
||||
/// its re-applying every window flag, which hides a bubble shown through
|
||||
/// [`show_no_activate`]. Run it on the window's own (main) thread.
|
||||
pub fn set_click_through(window: &tauri::WebviewWindow, ignore: bool) -> io::Result<()> {
|
||||
set_ex_style(window, (WS_EX_TRANSPARENT | WS_EX_LAYERED).0, ignore)
|
||||
}
|
||||
|
||||
/// OR `bits` into a window's extended style and apply the frame change.
|
||||
fn add_ex_style(window: &tauri::WebviewWindow, bits: u32) -> io::Result<()> {
|
||||
set_ex_style(window, bits, true)
|
||||
}
|
||||
|
||||
/// Set or clear `bits` in a window's extended style and apply the frame
|
||||
/// change.
|
||||
fn set_ex_style(window: &tauri::WebviewWindow, bits: u32, on: bool) -> io::Result<()> {
|
||||
let hwnd = hwnd_of(window)?;
|
||||
let wanted = bits as isize;
|
||||
let bits = bits as isize;
|
||||
// SAFETY: `hwnd` is a live top-level window of this process.
|
||||
let current = unsafe { GetWindowLongPtrW(hwnd, GWL_EXSTYLE) };
|
||||
if current & wanted == wanted {
|
||||
let wanted = if on { current | bits } else { current & !bits };
|
||||
if wanted == current {
|
||||
return Ok(());
|
||||
}
|
||||
// SAFETY: as above. A zero return is only an error when the last error
|
||||
// is set, so it is cleared first.
|
||||
unsafe { SetLastError(WIN32_ERROR(0)) };
|
||||
let previous = unsafe { SetWindowLongPtrW(hwnd, GWL_EXSTYLE, current | wanted) };
|
||||
let previous = unsafe { SetWindowLongPtrW(hwnd, GWL_EXSTYLE, wanted) };
|
||||
if previous == 0 {
|
||||
let err = io::Error::last_os_error();
|
||||
if err.raw_os_error().unwrap_or(0) != 0 {
|
||||
|
||||
Reference in new issue
Block a user