fix(macos): restore native window chrome and capsule focus

This commit is contained in:
baiqing committed 2026-05-03 09:29:59 +08:00
1 parent 89dd4e49bb
commit f89a285877
4 files changed
+82 -29

No files matched your search

@@ -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');
}
+55 -22
View File
@@ -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<usize>) -> bool {
true
}
#[cfg(target_os = "macos")]
fn show_capsule_window_no_activate<R: tauri::Runtime>(
app: &AppHandle<R>,
window: &tauri::WebviewWindow<R>,
) -> 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<R: tauri::Runtime>(
_app: &AppHandle<R>,
window: &tauri::WebviewWindow<R>,
) -> 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<u16> = "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<R: tauri::Runtime>(
_app: &AppHandle<R>,
_window: &tauri::WebviewWindow<R>,
) -> 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 {
+13 -5
View File
@@ -483,7 +483,8 @@ fn activate_app<R: Runtime>(_app: &AppHandle<R>) {}
/// 不调 NSApp.activate,不抢其他 app 焦点,符合 CLAUDE.md 约束。
#[cfg(target_os = "macos")]
pub(crate) fn restore_main_window_key_if_active<R: Runtime>(app: &AppHandle<R>) {
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<R: Runtime>(app: &AppHandle<R>)
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}"),
};
}
});
}
+1 -1
View File
@@ -21,7 +21,7 @@
"minWidth": 980,
"minHeight": 640,
"resizable": true,
"decorations": false,
"decorations": true,
"transparent": true,
"shadow": true,
"hiddenTitle": true,