From f89a2858773de4707b2b83db3625161e17c7c795 Mon Sep 17 00:00:00 2001 From: baiqing Date: Sun, 3 May 2026 09:29:59 +0800 Subject: [PATCH] fix(macos): restore native window chrome and capsule focus --- .../app/scripts/windows-ui-config.test.mjs | 14 +++- openless-all/app/src-tauri/src/coordinator.rs | 77 +++++++++++++------ openless-all/app/src-tauri/src/lib.rs | 18 +++-- openless-all/app/src-tauri/tauri.conf.json | 2 +- 4 files changed, 82 insertions(+), 29 deletions(-) diff --git a/openless-all/app/scripts/windows-ui-config.test.mjs b/openless-all/app/scripts/windows-ui-config.test.mjs index 633a7583..b6fcae40 100644 --- a/openless-all/app/scripts/windows-ui-config.test.mjs +++ b/openless-all/app/scripts/windows-ui-config.test.mjs @@ -33,9 +33,21 @@ assertEqual(capsuleWindow.width, 220, 'windows capsule config keeps translation- assertEqual(capsuleWindow.height, 110, 'windows capsule config keeps translation-capable height baseline'); assertEqual(capsuleWindow.transparent, true, 'capsule window should keep transparent visuals'); assertEqual(capsuleWindow.alwaysOnTop, true, 'capsule window should stay above the focused app while recording'); -assertEqual(mainWindow.decorations, false, 'windows main window should use only custom titlebar'); +assertEqual(mainWindow.decorations, true, 'shared main window config should keep native macOS traffic lights'); assertEqual(mainWindow.visible, false, 'windows main window should stay hidden until the intended first show point'); +assertMatch( + libRs, + /#\[cfg\(target_os = "windows"\)\][\s\S]*?main\.set_decorations\(false\)/, + 'windows runtime should disable native chrome before the first show', +); + +assertMatch( + coordinatorRs, + /#\[cfg\(target_os = "macos"\)\][\s\S]*?orderFrontRegardless/, + 'macOS capsule should show without taking the key window', +); + if (!/function WindowsResizeHandles\(\)/.test(windowChromeTsx)) { throw new Error('windows frameless shell should expose explicit resize handles'); } diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index 42eea2e8..36f035ba 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -8,7 +8,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc; use std::sync::Arc; -use std::time::Instant; +use std::time::{Duration, Instant}; use chrono::Utc; use parking_lot::Mutex; @@ -2520,24 +2520,58 @@ fn restore_focus_target_if_possible(_target: Option) -> bool { true } +#[cfg(target_os = "macos")] +fn show_capsule_window_no_activate( + app: &AppHandle, + window: &tauri::WebviewWindow, +) -> bool { + let window = window.clone(); + let (tx, rx) = mpsc::channel(); + let _ = app.run_on_main_thread(move || { + use objc2::msg_send; + use objc2::runtime::AnyObject; + + let ok = match window.ns_window() { + Ok(handle) => { + let ns = handle as *mut AnyObject; + if ns.is_null() { + false + } else { + unsafe { + let _: () = msg_send![ns, orderFrontRegardless]; + } + true + } + } + Err(e) => { + log::warn!("[capsule] ns_window unavailable for no-activate show: {e}"); + false + } + }; + let _ = tx.send(ok); + }); + rx.recv_timeout(Duration::from_millis(800)).unwrap_or(false) +} + #[cfg(target_os = "windows")] -fn show_capsule_window_no_activate() -> bool { - use std::iter::once; - use windows::core::PCWSTR; +fn show_capsule_window_no_activate( + _app: &AppHandle, + window: &tauri::WebviewWindow, +) -> bool { + use raw_window_handle::{HasWindowHandle, RawWindowHandle}; use windows::Win32::Foundation::HWND; use windows::Win32::UI::WindowsAndMessaging::{ - FindWindowW, SetWindowPos, ShowWindow, HWND_TOPMOST, SWP_NOACTIVATE, SWP_NOMOVE, - SWP_NOSIZE, SWP_SHOWWINDOW, SW_SHOWNOACTIVATE, + SetWindowPos, ShowWindow, HWND_TOPMOST, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, + SWP_SHOWWINDOW, SW_SHOWNOACTIVATE, }; - let title: Vec = "OpenLess Capsule".encode_utf16().chain(once(0)).collect(); - let hwnd = match unsafe { FindWindowW(PCWSTR::null(), PCWSTR(title.as_ptr())) } { - Ok(hwnd) => hwnd, - Err(_) => return false, - }; - if hwnd == HWND::default() || hwnd.0.is_null() { + let Ok(handle) = window.window_handle() else { return false; - } + }; + let RawWindowHandle::Win32(raw) = handle.as_raw() else { + return false; + }; + let hwnd = HWND(raw.hwnd.get() as *mut _); let _ = unsafe { ShowWindow(hwnd, SW_SHOWNOACTIVATE) }; let _ = unsafe { @@ -2554,8 +2588,11 @@ fn show_capsule_window_no_activate() -> bool { true } -#[cfg(not(target_os = "windows"))] -fn show_capsule_window_no_activate() -> bool { +#[cfg(all(not(target_os = "macos"), not(target_os = "windows")))] +fn show_capsule_window_no_activate( + _app: &AppHandle, + _window: &tauri::WebviewWindow, +) -> bool { false } @@ -2624,15 +2661,11 @@ fn emit_capsule( let visible = !matches!(state, CapsuleState::Idle); maybe_position_capsule_bottom_center(inner, &window, payload.translation); if show_capsule && visible { - if cfg!(target_os = "windows") { - if !show_capsule_window_no_activate() { - let _ = window.show(); - } - } else { + if !show_capsule_window_no_activate(&app, &window) { let _ = window.show(); } - // 胶囊 show() 在 macOS 会调 makeKeyAndOrderFront: 抢走主窗口焦点。 - // 若 OpenLess 已是前台 app,用 makeKeyWindow 还原主窗口(不激活 NSApp)。 + // macOS/Windows 优先走 no-activate show,避免录音胶囊抢走主窗口点击焦点。 + // 若 fallback 到 show(),OpenLess 已是前台 app 时再把 key window 还给 main。 #[cfg(target_os = "macos")] crate::restore_main_window_key_if_active(&app); } else { diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index 03c69213..fa478965 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -483,7 +483,8 @@ fn activate_app(_app: &AppHandle) {} /// 不调 NSApp.activate,不抢其他 app 焦点,符合 CLAUDE.md 约束。 #[cfg(target_os = "macos")] pub(crate) fn restore_main_window_key_if_active(app: &AppHandle) { - let _ = app.run_on_main_thread(|| { + let main = app.get_webview_window("main"); + let _ = app.run_on_main_thread(move || { use objc2::msg_send; use objc2::runtime::{AnyClass, AnyObject, Bool}; unsafe { @@ -498,11 +499,18 @@ pub(crate) fn restore_main_window_key_if_active(app: &AppHandle) if !is_active.as_bool() { return; } - let main_win: *mut AnyObject = msg_send![ns_app, mainWindow]; - if main_win.is_null() { + let Some(main) = main else { return; - } - let _: () = msg_send![main_win, makeKeyWindow]; + }; + match main.ns_window() { + Ok(handle) => { + let main_win = handle as *mut AnyObject; + if !main_win.is_null() { + let _: () = msg_send![main_win, makeKeyWindow]; + } + } + Err(e) => log::warn!("[main] ns_window unavailable for key restore: {e}"), + }; } }); } diff --git a/openless-all/app/src-tauri/tauri.conf.json b/openless-all/app/src-tauri/tauri.conf.json index fa959dd5..ac9c091a 100644 --- a/openless-all/app/src-tauri/tauri.conf.json +++ b/openless-all/app/src-tauri/tauri.conf.json @@ -21,7 +21,7 @@ "minWidth": 980, "minHeight": 640, "resizable": true, - "decorations": false, + "decorations": true, "transparent": true, "shadow": true, "hiddenTitle": true,