diff --git a/desktop/src-tauri/src/dictation.rs b/desktop/src-tauri/src/dictation.rs index ef7a6af..31cf9e4 100644 --- a/desktop/src-tauri/src/dictation.rs +++ b/desktop/src-tauri/src/dictation.rs @@ -331,32 +331,61 @@ fn set_tray_tooltip(app: &AppHandle, text: &str) { } /// 让识别浮层能浮在全屏 App 之上(macOS):给 NSWindow 追加 -/// CanJoinAllSpaces | Stationary | FullScreenAuxiliary 集合行为。 +/// CanJoinAllSpaces | Stationary | FullScreenAuxiliary 集合行为,并抬高窗口层级。 /// 仅 alwaysOnTop 不够——全屏 App 独占 Space,普通置顶窗口不会出现在其中, /// FullScreenAuxiliary 才让浮层作为辅助窗叠加到全屏 Space 上。 -/// 必须在主线程调用(Tauri setup 闭包即主线程),窗口创建后调用一次即可。 +/// +/// 强制派发到主线程执行(设 NSWindow 属性必须主线程);每次显示浮层都重设一遍, +/// 规避「setup 时隐藏窗口的原生窗口尚未就绪 / 属性被 show 重置」。 #[cfg(target_os = "macos")] pub fn configure_overlay_spaces(app: &AppHandle) { - use objc2::{msg_send, runtime::AnyObject}; - // NSWindowCollectionBehavior 位标志 - const CAN_JOIN_ALL_SPACES: usize = 1 << 0; - const STATIONARY: usize = 1 << 4; - const FULL_SCREEN_AUXILIARY: usize = 1 << 8; - if let Some(w) = app.get_webview_window(OVERLAY) { - if let Ok(ptr) = w.ns_window() { - let ns = ptr as *mut AnyObject; - unsafe { - let cur: usize = msg_send![ns, collectionBehavior]; - let next = cur | CAN_JOIN_ALL_SPACES | STATIONARY | FULL_SCREEN_AUXILIARY; - let _: () = msg_send![ns, setCollectionBehavior: next]; + let app2 = app.clone(); + let _ = app.run_on_main_thread(move || { + use objc2::{msg_send, runtime::AnyObject}; + const CAN_JOIN_ALL_SPACES: usize = 1 << 0; + const STATIONARY: usize = 1 << 4; + const FULL_SCREEN_AUXILIARY: usize = 1 << 8; + // NSStatusWindowLevel(25):浮在普通/全屏窗口之上、菜单栏之下。 + const STATUS_LEVEL: isize = 25; + let Some(w) = app2.get_webview_window(OVERLAY) else { + overlay_diag("configure: overlay window 不存在"); + return; + }; + match w.ns_window() { + Ok(ptr) => { + let ns = ptr as *mut AnyObject; + unsafe { + let before: usize = msg_send![ns, collectionBehavior]; + let next = before | CAN_JOIN_ALL_SPACES | STATIONARY | FULL_SCREEN_AUXILIARY; + let _: () = msg_send![ns, setCollectionBehavior: next]; + let _: () = msg_send![ns, setLevel: STATUS_LEVEL]; + let after: usize = msg_send![ns, collectionBehavior]; + overlay_diag(&format!( + "configure: ns_window OK behavior {before:#x} → {after:#x} level={STATUS_LEVEL}" + )); + } } + Err(e) => overlay_diag(&format!("configure: ns_window 失败: {e}")), } - } + }); } #[cfg(not(target_os = "macos"))] pub fn configure_overlay_spaces(_app: &AppHandle) {} +// TODO(诊断): 全屏浮层排查——写文件便于 open 启动时读取,定位后删除。 +#[cfg(target_os = "macos")] +fn overlay_diag(msg: &str) { + use std::io::Write; + if let Ok(mut f) = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open("/tmp/dudu-overlay-diag.log") + { + let _ = writeln!(f, "{msg}"); + } +} + fn show_overlay(app: &AppHandle) { if let Some(w) = app.get_webview_window(OVERLAY) { // 跟随光标:浮层出现在指针下方 24px,水平居中 @@ -365,6 +394,8 @@ fn show_overlay(app: &AppHandle) { } let _ = w.show(); } + // 每次显示都重设全屏叠加行为(主线程派发;见 configure_overlay_spaces)。 + configure_overlay_spaces(app); } fn hide_overlay(app: &AppHandle) { diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 9aabdf3..d57cde4 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -98,6 +98,12 @@ pub fn run() { }) .setup(|app| { let handle = app.handle().clone(); + // dudu 是托盘应用(无主窗口):设为 Accessory 激活策略——菜单栏辅助 App, + // 不占 Dock。这也是识别浮层能浮在其他 App 全屏之上的前提:标准 NSWindow + // 靠 collectionBehavior 无法叠加到别的 App 全屏 Space(macOS 设计限制, + // 见 Apple 论坛 26677 / tauri#11488),Accessory 策略 + CanJoinAllSpaces 才生效。 + #[cfg(target_os = "macos")] + let _ = app.set_activation_policy(tauri::ActivationPolicy::Accessory); // 设置 → 管理状态 app.manage(settings::SettingsStore::load(&handle)); app.manage(dictation::DictationState::default());