fix(macos): restore native window chrome and capsule focus
This commit is contained in:
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');
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}"),
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"minWidth": 980,
|
||||
"minHeight": 640,
|
||||
"resizable": true,
|
||||
"decorations": false,
|
||||
"decorations": true,
|
||||
"transparent": true,
|
||||
"shadow": true,
|
||||
"hiddenTitle": true,
|
||||
|
||||
Reference in new issue
Block a user