diff --git a/src/bubble.rs b/src/bubble.rs index b2b7d46..68e280f 100644 --- a/src/bubble.rs +++ b/src/bubble.rs @@ -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()); } } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index f848248..064b399 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -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 {