Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions apps/desktop-tauri/src-tauri/src/floatbar/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@ use tauri::{Emitter, Manager};
/// Install the native z-order guard and reopen the floating bar on app start
/// if it was enabled previously.
///
/// Called once from `main.rs::setup` so the Windows event hooks are registered
/// on the thread that owns Tauri's message loop.
/// Called once from `main.rs::setup` so the guard has the app handle before
/// the floatbar is shown.
pub fn install(app: &tauri::AppHandle) {
// Install the Windows shell hooks from Tauri's setup thread, which owns a
// message loop and therefore can receive out-of-context WinEvents.
topmost_guard::install(app);
let persisted = Settings::load();
if persisted.float_bar_enabled {
Expand All @@ -47,8 +45,8 @@ pub fn handle_window_event(window: &tauri::Window, event: &tauri::WindowEvent) -
match event {
tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => {
// Probe first (no I/O). Load style only when recovery runs.
// Always reassert topmost on geometry change; shell-event path
// in topmost_guard stays overlap-gated.
// Always reassert topmost on geometry change; the visibility-scoped
// topmost_guard timer stays overlap-gated.
let relocated = if window::is_off_all_monitors(window) {
let style = Settings::load().float_bar_style;
window::recover_onto_primary(window, &style)
Expand All @@ -64,6 +62,9 @@ pub fn handle_window_event(window: &tauri::Window, event: &tauri::WindowEvent) -
}
}
tauri::WindowEvent::CloseRequested { .. } => window::remember_geometry(window),
tauri::WindowEvent::Destroyed => {
topmost_guard::set_active(false);
}
_ => {}
}
true
Expand Down
153 changes: 51 additions & 102 deletions apps/desktop-tauri/src-tauri/src/floatbar/topmost_guard.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
//! Event-driven Windows z-order repair for a floatbar placed over the taskbar.
//! Keep a taskbar-overlapping floatbar above the Windows taskbar.
//!
//! The Windows taskbar is itself a topmost shell window. Activating or
//! reordering it can therefore put it in front of another topmost window
//! without removing that other window's `WS_EX_TOPMOST` style. We listen for
//! the two shell events that can produce that ordering change and re-assert the
//! floatbar only when it actually overlaps a taskbar.

#[cfg(any(windows, test))]
const EVENT_SYSTEM_FOREGROUND: u32 = 0x0003;
#[cfg(any(windows, test))]
const EVENT_OBJECT_REORDER: u32 = 0x8004;
//! The taskbar is itself topmost. Activating it can reorder the topmost band
//! without clearing `WS_EX_TOPMOST` on the floatbar. While the floatbar is
//! shown we run a short visibility-scoped timer and reassert topmost only when
//! the bar is visible and overlaps a taskbar. Move/resize always reasserts in
//! `floatbar::handle_window_event` (not overlap-gated).

#[cfg(any(windows, test))]
#[repr(C)]
Expand All @@ -26,110 +21,82 @@ fn rects_overlap(a: Rect, b: Rect) -> bool {
a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top
}

#[cfg(any(windows, test))]
fn is_relevant_event(event: u32) -> bool {
event == EVENT_SYSTEM_FOREGROUND || event == EVENT_OBJECT_REORDER
}

#[cfg(windows)]
mod platform {
use super::{
EVENT_OBJECT_REORDER, EVENT_SYSTEM_FOREGROUND, Rect, is_relevant_event, rects_overlap,
};
use super::{Rect, rects_overlap};
use raw_window_handle::HasWindowHandle;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, OnceLock};
use std::time::Duration;
use tauri::Manager;

use super::super::window::{self, FLOATBAR_LABEL};

const WINEVENT_OUTOFCONTEXT: u32 = 0x0000;
const WINEVENT_SKIPOWNPROCESS: u32 = 0x0002;
const REASSERT_DELAY: Duration = Duration::from_millis(40);
/// Interval while the floatbar is active. Matches the ~150ms recovery
/// budget validated for the original hook-based fix.
const POLL_INTERVAL: Duration = Duration::from_millis(120);

static APP_HANDLE: OnceLock<tauri::AppHandle> = OnceLock::new();
static HOOKS: OnceLock<Mutex<Vec<isize>>> = OnceLock::new();
static FLOATBAR_ACTIVE: AtomicBool = AtomicBool::new(false);
static REASSERT_PENDING: AtomicBool = AtomicBool::new(false);
static LOOP_RUNNING: AtomicBool = AtomicBool::new(false);

static PRIMARY_CLASS: OnceLock<Vec<u16>> = OnceLock::new();
static SECONDARY_CLASS: OnceLock<Vec<u16>> = OnceLock::new();

pub fn install(app: &tauri::AppHandle) {
let _ = APP_HANDLE.set(app.clone());
let hooks = HOOKS.get_or_init(|| Mutex::new(Vec::new()));
let mut hooks = hooks.lock().unwrap();
if !hooks.is_empty() {
return;
}

for event in [EVENT_SYSTEM_FOREGROUND, EVENT_OBJECT_REORDER] {
let hook = unsafe {
SetWinEventHook(
event,
event,
0,
Some(win_event_proc),
0,
0,
WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS,
)
};
if hook == 0 {
tracing::warn!(
event,
error = %std::io::Error::last_os_error(),
"failed to install floatbar z-order event hook"
);
} else {
hooks.push(hook);
}
}
}

unsafe extern "system" fn win_event_proc(
_hook: isize,
event: u32,
_hwnd: isize,
_object_id: i32,
_child_id: i32,
_event_thread: u32,
_event_time: u32,
) {
if FLOATBAR_ACTIVE.load(Ordering::Acquire) && is_relevant_event(event) {
schedule_reassert();
}
}

pub fn set_active(active: bool) {
FLOATBAR_ACTIVE.store(active, Ordering::Release);
if active {
ensure_loop();
}
}

fn schedule_reassert() {
if REASSERT_PENDING.swap(true, Ordering::AcqRel) {
fn ensure_loop() {
if LOOP_RUNNING.swap(true, Ordering::AcqRel) {
return;
}
let Some(app) = APP_HANDLE.get().cloned() else {
REASSERT_PENDING.store(false, Ordering::Release);
LOOP_RUNNING.store(false, Ordering::Release);
return;
};

tauri::async_runtime::spawn(async move {
tokio::time::sleep(REASSERT_DELAY).await;
let dispatcher = app.clone();
let result = dispatcher.run_on_main_thread(move || {
if let Some(floatbar) = app.get_webview_window(FLOATBAR_LABEL)
&& floatbar.is_visible().unwrap_or(false)
&& overlaps_taskbar(&floatbar)
{
window::apply_always_on_top(&floatbar);
while FLOATBAR_ACTIVE.load(Ordering::Acquire) {
tokio::time::sleep(POLL_INTERVAL).await;
if !FLOATBAR_ACTIVE.load(Ordering::Acquire) {
break;
}
REASSERT_PENDING.store(false, Ordering::Release);
});
if result.is_err() {
REASSERT_PENDING.store(false, Ordering::Release);
let dispatcher = app.clone();
let app_for_work = app.clone();
let _ = dispatcher.run_on_main_thread(move || {
reassert_if_needed(&app_for_work);
});
}
LOOP_RUNNING.store(false, Ordering::Release);
// If set_active(true) raced with loop exit, start again.
if FLOATBAR_ACTIVE.load(Ordering::Acquire) {
ensure_loop();
}
});
}

fn reassert_if_needed(app: &tauri::AppHandle) {
let Some(floatbar) = app.get_webview_window(FLOATBAR_LABEL) else {
// Webview gone without hide — stop the guard.
FLOATBAR_ACTIVE.store(false, Ordering::Release);
return;
};
if !floatbar.is_visible().unwrap_or(false) {
return;
}
if overlaps_taskbar(&floatbar) {
window::apply_always_on_top(&floatbar);
}
}

fn overlaps_taskbar(window: &tauri::WebviewWindow) -> bool {
let Ok(handle) = window.window_handle() else {
return false;
Expand All @@ -149,8 +116,8 @@ mod platform {
}

fn taskbar_rects() -> Vec<Rect> {
let primary_class = wide("Shell_TrayWnd");
let secondary_class = wide("Shell_SecondaryTrayWnd");
let primary_class = PRIMARY_CLASS.get_or_init(|| wide("Shell_TrayWnd"));
let secondary_class = SECONDARY_CLASS.get_or_init(|| wide("Shell_SecondaryTrayWnd"));
let mut rects = Vec::new();

let primary = unsafe { FindWindowW(primary_class.as_ptr(), std::ptr::null()) };
Expand Down Expand Up @@ -184,19 +151,8 @@ mod platform {
value.encode_utf16().chain(std::iter::once(0)).collect()
}

type WinEventProc = unsafe extern "system" fn(isize, u32, isize, i32, i32, u32, u32);

#[link(name = "user32")]
unsafe extern "system" {
fn SetWinEventHook(
event_min: u32,
event_max: u32,
module: isize,
callback: Option<WinEventProc>,
process_id: u32,
thread_id: u32,
flags: u32,
) -> isize;
fn FindWindowW(class_name: *const u16, window_name: *const u16) -> isize;
fn FindWindowExW(
parent: isize,
Expand Down Expand Up @@ -256,11 +212,4 @@ mod tests {
taskbar
));
}

#[test]
fn foreground_and_window_reorder_events_are_relevant() {
assert!(is_relevant_event(EVENT_SYSTEM_FOREGROUND));
assert!(is_relevant_event(EVENT_OBJECT_REORDER));
assert!(!is_relevant_event(0x800B));
}
}
Loading
Loading