fix(island): 修好"收起后再次点击灵动岛没反应"(假失焦缩回 + 命中状态互相覆盖)
根因一(假失焦):展开时 setFocusable(true)+focus() 会引发一次与用户无关的失焦 (实测 70~520ms 后到达)。原来的失焦处理器在 500ms 宽限期之后就把任何 blur 当成 "用户点了别处",于是刚展开的岛立刻缩回胶囊 —— 看起来就是"点了没反应"。 现在只在**指针确实离开岛体**时才认为用户点了别处(新增 isCursorOnIsland)。 根因二(命中抖动):渲染进程与主进程各自下发 setInteractive(true/false),而两边几何 逐帧不一致(弹簧形变 + 岛体矩形上报限流),窗口就在"接收鼠标 / 穿透"之间几毫秒翻转一次, 点击撞上穿透态即"点不动"(日志实测 true/false 成对翻转)。现在命中判定单一权威: 渲染进程只负责"立刻打开命中",关闭一律由主进程按真实光标轮询(120ms→60ms)决定; 同时去掉"窗口一可见就整块 476×282 包围盒接收鼠标"(会在屏幕上方留一块看不见却点不动的 区域)、淡出期间不接收鼠标、岛体矩形上报限流 100→50ms。 验证(真机真实点击,非合成事件):SetCursorPos + mouse_event 连续 4 轮 "点开 → 点卡片收起 → 再点胶囊"全部正常,展开后保持展开不再被假失焦缩回 (修复前 0.5~2s 内必缩回);pnpm verify:desktop 78/78(新增回归用例 「假失焦(指针仍在岛上)不回缩:展开后必须保持展开」)。 顺带修掉既有测试缺陷:通知菜单项带未读徽标时整项 textContent 变成 "2通知", 用 startsWith 匹配会误报"菜单项缺失",改为按文案层 span 精确匹配。
This commit is contained in:
1 parent
70250151d0
commit
a7b0e9c2b3
6 files changed
+173
-55
No files matched your search
@@ -341,7 +341,12 @@ let lastPointer: { x: number; y: number } | null = null;
|
||||
/** 最近一次上报给主进程的岛体矩形(去重 + 限流,避免形变期间刷屏 IPC 拖慢渲染) */
|
||||
let lastHitRectKey: string | null = null;
|
||||
let lastHitRectAt = 0;
|
||||
const HIT_RECT_MIN_INTERVAL_MS = 100;
|
||||
/**
|
||||
* 岛体矩形上报的最小间隔:主进程按它做命中判定,限流太大就会在形变结束后留下
|
||||
* 一段"矩形比实际卡片大"的死区(那段时间窗口会吃掉看不见区域的点击)。
|
||||
* 50ms 既不会刷爆 IPC,也能让死区短到无感。
|
||||
*/
|
||||
const HIT_RECT_MIN_INTERVAL_MS = 50;
|
||||
let hitRectTrailingTimer: number | null = null;
|
||||
|
||||
/** 把岛体矩形上报给主进程:主进程按光标位置兜底校正命中(不依赖 mousemove 转发) */
|
||||
@@ -378,6 +383,14 @@ function reportHitRect(rect: DOMRect | null): void {
|
||||
}, HIT_RECT_MIN_INTERVAL_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 命中测试:把"指针在岛上"告诉主进程(照搬 WinIsland 的 set_cursor_hittest 思路)。
|
||||
*
|
||||
* **只上报 true**:窗口关闭命中一律由主进程按真实光标位置(60ms 轮询)决定。
|
||||
* 为什么不能两边都下发:渲染进程与主进程的几何不可能逐帧一致(弹簧形变 + 岛体矩形上报限流),
|
||||
* 两边都下发 true/false 时会互相覆盖,窗口就在"接收鼠标 / 穿透"之间来回抖动 ——
|
||||
* 用户点下去时窗口恰好处于穿透态,表现就是**点了没反应**(实测日志里 true/false 每几毫秒翻转一次)。
|
||||
*/
|
||||
function updateInteractive(clientX: number, clientY: number): void {
|
||||
lastPointer = { x: clientX, y: clientY };
|
||||
const node = document.querySelector('.island-card') as HTMLElement | null;
|
||||
@@ -391,9 +404,15 @@ function updateInteractive(clientX: number, clientY: number): void {
|
||||
clientY >= rect.top - margin &&
|
||||
clientY <= rect.bottom + margin;
|
||||
}
|
||||
if (inside === lastInteractive) return;
|
||||
lastInteractive = inside;
|
||||
bridge?.setInteractive?.(inside);
|
||||
if (!inside) {
|
||||
// 离开岛体不做任何下发:交给主进程轮询关闭;这里只复位本地标记,
|
||||
// 保证下次指针进入时能立刻重新打开命中(不必等下一次轮询)。
|
||||
lastInteractive = false;
|
||||
return;
|
||||
}
|
||||
if (lastInteractive === true) return;
|
||||
lastInteractive = true;
|
||||
bridge?.setInteractive?.(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -406,16 +425,15 @@ function updateInteractive(clientX: number, clientY: number): void {
|
||||
*
|
||||
* 另外这里会把当前岛体矩形上报主进程(`setHitRect`):即使 Windows 下 mousemove 转发丢失,
|
||||
* 主进程也能按光标位置把命中校正回来 —— 这是"点开→收起→再也点不开"的兜底修复。
|
||||
* 注意:关闭命中(穿透)不由这里下发,见 updateInteractive 的说明。
|
||||
*/
|
||||
function refreshInteractive(): void {
|
||||
const node = document.querySelector('.island-card') as HTMLElement | null;
|
||||
const fullyHidden = state.value?.mode === 'hidden' && !appearance.value.idleSliver;
|
||||
reportHitRect(node && !fullyHidden ? node.getBoundingClientRect() : null);
|
||||
if (fullyHidden) {
|
||||
if (lastInteractive !== false) {
|
||||
lastInteractive = false;
|
||||
bridge?.setInteractive?.(false);
|
||||
}
|
||||
// 完全隐藏:本地复位即可(不下发关闭,窗口隐藏后由主进程轮询兜底关掉命中)
|
||||
lastInteractive = false;
|
||||
return;
|
||||
}
|
||||
if (!lastPointer) return;
|
||||
|
||||
Reference in new issue
Block a user