fix(capsule): 胶囊跟随鼠标光标所在屏,修复多屏/多 Space 只在第一块屏显示#714
Conversation
多屏 + 多桌面下胶囊只出现在第一块屏、其他屏闪一下就消失。根因:定位 (position_capsule_bottom_center) 用 AX caret 选屏,多数 App 取不到 caret 矩形就 跌回胶囊自己的 current_monitor(第一块屏);而去重缓存 capsule_layout_snapshot 也 用 current_monitor,与定位看的不是同一块屏 → 胶囊一落第一块屏即被判“无变化”跳过 重定位,被锁死。两条路都没跟踪鼠标。 - 新增 mouse_cursor_point():CoreGraphics CGEventGetLocation 取光标,坐标系与 AX 一致、零权限、永远可用 - focused_input_target_monitor → capsule_target_monitor,选屏改“光标优先”(caret 仅兜底),选屏循环抽成 monitor_for_anchor_point - 定位与去重快照统一走 capsule_target_monitor,彻底消除不一致 - 保留 CAN_JOIN_ALL_SPACES(Spaces 契约强制且本就正确:帧落在光标屏→只那块屏显示) - 扩展 macos-capsule-spaces 契约测试,grep 守护“光标优先 + 两路共用同一函数” Windows 路径未动(本就跟随输入 App 屏且快照一致)。 验证:cargo check + lib 纯函数单测 + check:macos-capsule-spaces 全过。
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
|
@claude 请审核(多屏胶囊定位):本 PR 修复「胶囊只在第一块屏/多 Space 只显示一次」。根因是定位用 AX caret 选屏、但去重缓存 重点请确认:
|
|
Claude finished @appergb's task in 2m 42s —— View job 代码审核结果(多屏胶囊定位)审核清单:
确认 1:坐标系一致性 ✅结论: 两者都使用 Quartz 全局显示坐标系(左上原点、Y 向下增大、单位 points):
坐标系的结合点在
#[repr(C)]
struct CGPoint { x: f64, y: f64 }与 Apple SDK 的 确认 2:
|
| 路径 | 旧代码 |
|---|---|
定位 position_capsule_bottom_center |
focused_input_target_monitor(AX caret) |
去重缓存 capsule_layout_snapshot |
window.current_monitor()(胶囊上次停留的屏) |
修复后的统一:
// lib.rs:2539 — 定位
if let Some(mon) = capsule_target_monitor(window) { ... }
// capsule_focus.rs:618 — 去重缓存
if let Some(mon) = crate::capsule_target_monitor(window) {
return Some(CapsuleLayoutState { ... });
}两处调用同一个函数 capsule_target_monitor,返回值包含 monitor_x/y/width/height/scale_bits,作为 CapsuleLayoutState 的 key。光标移到另一块屏时,key 必然变化 → 不再误判「无变化」→ 胶囊正常重定位 ✓
capsule_focus.rs 的 #[cfg(target_os = "macos")] 块在 Windows 的 foreground_window_monitor 块之后、current_monitor fallback 之前插入,控制流顺序正确:macOS 分支命中即直接返回,不会落到 current_monitor ✓
确认 4:CGEventGetLocation 零权限(TCC)✅
结论:不触发 TCC,零权限成立。
CGEventCreate(source: NULL) 创建的是一个自己构造的事件对象,不是截获系统事件流。CGEventGetLocation 从这个对象读取当前光标位置——这是 macOS 公开的合法 API,等价于「查询当前鼠标坐标」,不涉及:
- Accessibility(
kTCCServiceAccessibility) - Screen Recording(
kTCCServiceScreenCapture) - Input Monitoring(
kTCCServiceListenEvent/kTCCServicePostEvent)
需要 TCC 的是:CGEventTapCreate(监听/注入全局事件流)、AXUIElement 系列(无障碍访问)。本 PR 新增的只是 CGEventCreate + CGEventGetLocation,不在 TCC 管控范围内。
内存管理亦正确:CGEventCreate 遵循 Create Rule,返回 retain count +1 的对象,用 CFRelease 释放 ✓(CGEventRelease 与 CFRelease 等价,两者皆可接受)
确认 5:平台隔离 ✅
结论:所有修改仅影响 macOS,其他平台零改动。
capsule_target_monitor、macos_mouse_cursor_point、monitor_for_anchor_point、macos_capsule_ax::mouse_cursor_point均有#[cfg(target_os = "macos")]✓capsule_focus.rs新增块被#[cfg(target_os = "macos")]包裹,Windows 继续走foreground_window_monitor,其他平台走window.current_monitor()✓- Windows 的
foreground_window_monitor(跟随正在输入 App 所在屏)未改动 ✓ - 契约测试
macos-capsule-spaces-contract.test.mjs是独立脚本,不进入 Android/Windows CI ✓
综合结论
5 个核心确认点全部通过,可合并。 根因分析准确,修法直接消除了不一致来源,契约测试有效守护不变量。
唯一可选改进(不阻塞):
- 契约测试正则依赖中文注释(如
跟随鼠标光标所在显示器)——若注释被重写,grep 会误报。可以考虑改为对函数调用模式的纯 Rust 语法匹配,但当前行为对这个 repo 是有意为之(注释即约定),可以接受。
User description
问题
多屏 + 多桌面(Spaces)下,听写胶囊只出现在第一块屏,其他屏只闪一下就消失。期望:胶囊出现在鼠标当前所在的屏/桌面,其他屏不显示。
根因(macOS)
胶囊「该摆哪块屏」用了两套互相矛盾的判断:
position_capsule_bottom_center用focused_input_target_monitor(AX 输入框 caret 位置)。多数 App 取不到可用的 AX 矩形 → 返回None→ 跌落window.current_monitor()(胶囊自己上次停留的屏 = 第一块屏)。capsule_layout_snapshot在 macOS 也用window.current_monitor(),和定位看的不是同一块屏。两者不一致 → 胶囊一旦落到第一块屏,之后每次状态变化都判snapshot == last→ 跳过重新定位,被锁死在第一块屏;切换时在别屏短暂出现 = 那个「闪一下」。两条路都没有跟踪鼠标光标——而这正是用户要的、也是唯一「永远可用、零权限」的信号。
capsule_focus.rs自己的注释(590–596 行)早就警告过这两者必须一致。修法
macos_capsule_ax::mouse_cursor_point():CoreGraphicsCGEventCreate+CGEventGetLocation取光标位置,与 AX caret 同属 Quartz 全局显示坐标系(左上原点、points),可直接复用现有logical_frame/frame_contains_point选屏;零权限、始终可用。focused_input_target_monitor→capsule_target_monitor,选屏改光标优先(mouse_cursor_point().or_else(focused_input_anchor_point)),选屏循环抽成纯函数monitor_for_anchor_point。capsule_target_monitor,彻底消除不一致 → 不再锁死第一块屏、不再闪现,胶囊跟随鼠标所在的屏/桌面。CAN_JOIN_ALL_SPACES:Spaces 契约测试强制,且本就正确——窗口帧落在光标那块屏 → 只那块屏显示,同时跨 Space 跟随(满足「鼠标所在桌面才显示」)。macos-capsule-spaces契约测试,新增 3 条源码 grep 守护本次不变量(光标优先、定位走capsule_target_monitor、快照复用同一函数),防止回归成「测试绿、线上坏」。坐标系坑:
NSEvent.mouseLocation是左下原点 Cocoa 系(需翻转,未采用);CGEventGetLocation是左上原点、免翻转,与 AX 对齐。Windows 路径未改动——它本就走
foreground_window_monitor(跟随正在输入的 App 所在屏),且定位与快照一致。测试
cargo check --manifest-path src-tauri/Cargo.toml(仅既有 warning)npm run check:macos-capsule-spaces(含新增 3 条契约)通过🤖 Generated with Claude Code
PR Type
Bug fix, Enhancement
Description
Capsule now follows mouse cursor across multi-screen/Spaces
Unified monitor detection: both positioning and layout cache use same function
Added contract tests to guard invariants
Diagram Walkthrough
File Walkthrough
capsule_focus.rs
Add macOS monitor detection to layout cacheopenless-all/app/src-tauri/src/coordinator/capsule_focus.rs
capsule_layout_snapshotto usecapsule_target_monitorlib.rs
Introduce mouse cursor based monitor selectionopenless-all/app/src-tauri/src/lib.rs
focused_input_target_monitortocapsule_target_monitormacos_mouse_cursor_point()using CoreGraphics (zero-permission)macos-capsule-spaces-contract.test.mjs
Add contract tests for capsule monitor selectionopenless-all/app/scripts/macos-capsule-spaces-contract.test.mjs
function