Skip to content
Closed
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
55 changes: 49 additions & 6 deletions openless-all/app/scripts/macos-capsule-spaces-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,73 @@ function assertMatch(source, pattern, name) {
const coordinatorRs = (
await readFile(new URL('../src-tauri/src/coordinator.rs', import.meta.url), 'utf-8')
).replace(/\r\n/g, '\n');
const libRs = (await readFile(new URL('../src-tauri/src/lib.rs', import.meta.url), 'utf-8')).replace(
/\r\n/g,
'\n',
);
const functionMatch = coordinatorRs.match(
/#\[cfg\(target_os = "macos"\)\]\s*fn show_capsule_window_no_activate[\s\S]*?\n}\n\n#\[cfg\(target_os = "linux"\)\]/,
);
const behaviorHelperMatch = coordinatorRs.match(
/#\[cfg\(target_os = "macos"\)\]\s*fn macos_capsule_collection_behavior[\s\S]*?\n}\n\n#\[cfg\(target_os = "macos"\)\]\s*fn macos_capsule_window_level/,
);
const styleHelperMatch = coordinatorRs.match(
/#\[cfg\(target_os = "macos"\)\]\s*fn macos_capsule_style_mask[\s\S]*?\n}\n\n#\[cfg\(target_os = "macos"\)\]\s*fn configure_macos_capsule_window_for_overlay/,
);
const hideHelperMatch = coordinatorRs.match(
/#\[cfg\(target_os = "macos"\)\]\s*pub\(crate\) fn hide_capsule_window_preserving_space[\s\S]*?\n}\n\n#\[cfg\(target_os = "macos"\)\]\s*fn show_capsule_window_no_activate/,
);

if (!functionMatch) {
throw new Error('macOS capsule no-activate function not found');
}
if (!behaviorHelperMatch) {
throw new Error('macOS capsule collection behavior helper not found');
}
if (!styleHelperMatch) {
throw new Error('macOS capsule style helper not found');
}
if (!hideHelperMatch) {
throw new Error('macOS capsule hide helper not found');
}

const macosNoActivateFunction = functionMatch[0];
const behaviorHelper = behaviorHelperMatch[0];
const styleHelper = styleHelperMatch[0];
const hideHelper = hideHelperMatch[0];
const executableMacosNoActivateFunction = macosNoActivateFunction.replace(/\/\/.*$/gm, '');

assertMatch(
macosNoActivateFunction,
/set_visible_on_all_workspaces\(true\)[\s\S]*?orderFrontRegardless/,
'macOS capsule should join all Spaces before showing without activation',
behaviorHelper,
/!MOVE_TO_ACTIVE_SPACE[\s\S]*?CAN_JOIN_ALL_SPACES[\s\S]*?TRANSIENT[\s\S]*?IGNORES_CYCLE[\s\S]*?FULL_SCREEN_AUXILIARY/,
'macOS capsule should clear MoveToActiveSpace and opt into the fullscreen HUD behaviors',
);

assertMatch(
styleHelper,
/NONACTIVATING_PANEL[\s\S]*?1 << 7/,
'macOS capsule should use the non-activating panel style bit',
);

assertMatch(
macosNoActivateFunction,
/FULL_SCREEN_AUXILIARY[\s\S]*?1 << 8[\s\S]*?setCollectionBehavior[\s\S]*?orderFrontRegardless/,
'macOS capsule should join fullscreen Spaces as an auxiliary window before showing without activation',
/configure_macos_capsule_window_for_overlay\(window\)[\s\S]*?orderFrontRegardless/,
'macOS capsule should configure overlay behavior before showing without activation',
);

assertMatch(
hideHelper,
/orderOut/,
'macOS capsule should hide with orderOut to preserve fullscreen Space association',
);

assertMatch(
libRs,
/prepare_capsule_window_for_overlay\(&capsule\)[\s\S]*?hide_capsule_window_preserving_space\(&capsule\)/,
'macOS startup should prepare and hide the capsule through the overlay-preserving path',
);

for (const forbidden of ['window.show()', 'set_focus', 'NSApp.activate', 'makeKeyAndOrderFront']) {
for (const forbidden of ['set_focus', 'NSApp.activate', 'makeKeyAndOrderFront']) {
if (executableMacosNoActivateFunction.includes(forbidden)) {
throw new Error(`macOS capsule no-activate path must not call ${forbidden}`);
}
Expand Down
170 changes: 152 additions & 18 deletions openless-all/app/src-tauri/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4536,6 +4536,48 @@ mod tests {
);
}

#[test]
#[cfg(target_os = "macos")]
fn macos_capsule_collection_behavior_joins_spaces_without_conflicts() {
let behavior = macos_capsule_collection_behavior(0);

assert!(
behavior & (1 << 0) != 0,
"capsule HUD should join all Spaces"
);
assert_eq!(
behavior & (1 << 1),
0,
"MoveToActiveSpace keeps the HUD out of another app's fullscreen Space"
);
assert!(behavior & (1 << 3) != 0, "must be transient overlay");
assert_eq!(
behavior & (1 << 4),
0,
"Stationary can conflict with independent display Spaces"
);
assert!(behavior & (1 << 6) != 0, "must stay out of window cycling");
assert!(
behavior & (1 << 8) != 0,
"must appear over fullscreen Spaces"
);
assert!(
macos_capsule_window_level() >= 1500,
"capsule must use AssistiveTechHigh-level overlay stacking"
);
}

#[test]
#[cfg(target_os = "macos")]
fn macos_capsule_style_mask_is_nonactivating_panel_like() {
let style_mask = macos_capsule_style_mask(0);

assert!(
style_mask & (1 << 7) != 0,
"capsule should stay non-activating"
);
}

#[test]
#[cfg(target_os = "windows")]
fn prepared_windows_ime_slot_is_taken_only_for_matching_session() {
Expand Down Expand Up @@ -5127,37 +5169,122 @@ fn show_capsule_window_no_activate<R: tauri::Runtime>(
}

#[cfg(target_os = "macos")]
fn show_capsule_window_no_activate<R: tauri::Runtime>(
_app: &AppHandle<R>,
fn macos_capsule_collection_behavior(current: usize) -> usize {
const CAN_JOIN_ALL_SPACES: usize = 1 << 0;
const MOVE_TO_ACTIVE_SPACE: usize = 1 << 1;
const TRANSIENT: usize = 1 << 3;
const STATIONARY: usize = 1 << 4;
const IGNORES_CYCLE: usize = 1 << 6;
const FULL_SCREEN_AUXILIARY: usize = 1 << 8;

// Mirror Electron's visibleOnAllWorkspaces/fullscreen auxiliary HUD path.
// `MoveToActiveSpace` keeps the ordinary Tauri NSWindow tied to the app's
// own Space instead of joining another app's native fullscreen Space.
let cleared = current & !MOVE_TO_ACTIVE_SPACE & !STATIONARY;
cleared | CAN_JOIN_ALL_SPACES | TRANSIENT | IGNORES_CYCLE | FULL_SCREEN_AUXILIARY
}

#[cfg(target_os = "macos")]
fn macos_capsule_window_level() -> isize {
const K_CG_ASSISTIVE_TECH_HIGH_WINDOW_LEVEL_KEY: i32 = 20;

#[link(name = "ApplicationServices", kind = "framework")]
extern "C" {
fn CGWindowLevelForKey(key: i32) -> i32;
}

// Status-level windows can still be composited behind another app's native
// fullscreen Space. AssistiveTechHigh is the system overlay level used by
// transient accessibility HUDs.
unsafe { CGWindowLevelForKey(K_CG_ASSISTIVE_TECH_HIGH_WINDOW_LEVEL_KEY) as isize }
}

#[cfg(target_os = "macos")]
fn macos_capsule_style_mask(current: usize) -> usize {
const NONACTIVATING_PANEL: usize = 1 << 7;

current | NONACTIVATING_PANEL
}

#[cfg(target_os = "macos")]
fn configure_macos_capsule_window_for_overlay<R: tauri::Runtime>(
window: &tauri::WebviewWindow<R>,
) -> bool {
) -> Option<*mut objc2::runtime::AnyObject> {
use objc2::msg_send;
use objc2::runtime::AnyObject;

let Ok(handle) = window.ns_window() else {
return false;
log::warn!("[capsule] macOS overlay configure failed: ns_window unavailable");
return None;
};
let ns_window = handle as *mut AnyObject;
if ns_window.is_null() {
return false;
log::warn!("[capsule] macOS overlay configure failed: ns_window null");
return None;
}

unsafe {
let before_behavior: usize = msg_send![ns_window, collectionBehavior];
let before_style_mask: usize = msg_send![ns_window, styleMask];
let after_behavior = macos_capsule_collection_behavior(before_behavior);
let after_style_mask = macos_capsule_style_mask(before_style_mask);
let level = macos_capsule_window_level();
let _: () = msg_send![ns_window, setStyleMask: after_style_mask];
let _: () = msg_send![ns_window, setCollectionBehavior: after_behavior];
let _: () = msg_send![ns_window, setLevel: level];
let _: () = msg_send![ns_window, setCanHide: false];
let _: () = msg_send![ns_window, setHidesOnDeactivate: false];
let _: () = msg_send![ns_window, setReleasedWhenClosed: false];
let _: () = msg_send![ns_window, setIgnoresMouseEvents: true];
Some(ns_window)
}
}

// emit_capsule 已经把窗口操作 marshal 到 Tauri 主线程;这里不能再调用
// window.show()/set_focus()/NSApp.activate,否则 AeroSpace 会把 workspace 切回
// OpenLess 主窗口所在空间。先让胶囊加入所有 Spaces,再用
// orderFrontRegardless 做无激活展示。
if let Err(e) = window.set_visible_on_all_workspaces(true) {
log::warn!("[capsule] set visible on all macOS Spaces failed: {e}");
#[cfg(target_os = "macos")]
pub(crate) fn prepare_capsule_window_for_overlay<R: tauri::Runtime>(
window: &tauri::WebviewWindow<R>,
) -> bool {
configure_macos_capsule_window_for_overlay(window).is_some()
}

#[cfg(target_os = "macos")]
pub(crate) fn hide_capsule_window_preserving_space<R: tauri::Runtime>(
window: &tauri::WebviewWindow<R>,
) -> bool {
use objc2::msg_send;

let Some(ns_window) = configure_macos_capsule_window_for_overlay(window) else {
return false;
};
unsafe {
let _: () = msg_send![ns_window, orderOut: std::ptr::null::<objc2::runtime::AnyObject>()];
}
true
}

#[cfg(target_os = "macos")]
fn show_capsule_window_no_activate<R: tauri::Runtime>(
_app: &AppHandle<R>,
window: &tauri::WebviewWindow<R>,
) -> bool {
use objc2::msg_send;

// emit_capsule 已经把窗口操作 marshal 到 Tauri 主线程;这里不能调用
// set_focus()/NSApp.activate,否则 AeroSpace 会把 workspace 切回 OpenLess
// 主窗口所在空间。胶囊是 HUD,不是主窗口:通过 all-Spaces +
// fullscreen auxiliary 行为挂到用户当前 fullscreen Space。
let was_visible = window.is_visible().unwrap_or(false);
let Some(ns_window) = configure_macos_capsule_window_for_overlay(window) else {
return false;
};

unsafe {
const NS_WINDOW_COLLECTION_BEHAVIOR_CAN_JOIN_ALL_SPACES: usize = 1 << 0;
const NS_WINDOW_COLLECTION_BEHAVIOR_FULL_SCREEN_AUXILIARY: usize = 1 << 8;
let behavior: usize = msg_send![ns_window, collectionBehavior];
let behavior = behavior
| NS_WINDOW_COLLECTION_BEHAVIOR_CAN_JOIN_ALL_SPACES
| NS_WINDOW_COLLECTION_BEHAVIOR_FULL_SCREEN_AUXILIARY;
let _: () = msg_send![ns_window, setCollectionBehavior: behavior];
if !was_visible {
if let Err(e) = window.show() {
log::warn!("[capsule] macOS no_activate pre-show failed: {e}");
}
}

let _: () = msg_send![ns_window, orderFrontRegardless];
}
true
Expand Down Expand Up @@ -5396,6 +5523,13 @@ fn emit_capsule(
);
}
hide_capsule_window_if_present();
#[cfg(target_os = "macos")]
{
if !hide_capsule_window_preserving_space(&window) {
let _ = window.hide();
}
}
#[cfg(not(target_os = "macos"))]
let _ = window.hide();
}
}
Expand Down
10 changes: 9 additions & 1 deletion openless-all/app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,15 @@ pub fn run() {
if let Err(e) = position_capsule_bottom_center(&capsule, false) {
log::warn!("[capsule] position failed: {e}");
}
let _ = capsule.hide();
#[cfg(target_os = "macos")]
{
crate::coordinator::prepare_capsule_window_for_overlay(&capsule);
let _ = crate::coordinator::hide_capsule_window_preserving_space(&capsule);
}
#[cfg(not(target_os = "macos"))]
{
let _ = capsule.hide();
}
}

// QA 浮窗(issue #118):紧贴胶囊上方 8pt、屏幕底部居中、380×440。
Expand Down