Improve Windows pet input rearm
This commit is contained in:
parent
67f645b47b
commit
cc675fe898
3 changed files with 85 additions and 12 deletions
|
|
@ -42,16 +42,29 @@ const getInteractiveTarget = (event) => {
|
|||
return target && target.closest(".pet-shell, .bubble");
|
||||
};
|
||||
|
||||
const setInteractiveHit = (interactive) => {
|
||||
if (lastInteractiveHit === interactive) return;
|
||||
const reportInteractiveHit = (interactive, source, force = false) => {
|
||||
if (!force && lastInteractiveHit === interactive) return;
|
||||
lastInteractiveHit = interactive;
|
||||
ipcRenderer.send("openpets:pet-hit-test", interactive);
|
||||
ipcRenderer.send("openpets:pet-hit-test", interactive, source);
|
||||
};
|
||||
|
||||
const setInteractiveHit = (interactive, source = "mouse") => {
|
||||
if (lastInteractiveHit === interactive) return;
|
||||
reportInteractiveHit(interactive, source);
|
||||
};
|
||||
|
||||
const updateInteractiveHit = (event) => {
|
||||
setInteractiveHit(Boolean(getInteractiveTarget(event)) || dragging);
|
||||
};
|
||||
|
||||
ipcRenderer.on("openpets:pet-probe-hit-test", (_event, point) => {
|
||||
if (!point || typeof point.clientX !== "number" || typeof point.clientY !== "number" || !Number.isFinite(point.clientX) || !Number.isFinite(point.clientY)) return;
|
||||
const clientX = point.clientX;
|
||||
const clientY = point.clientY;
|
||||
const target = document.elementFromPoint(clientX, clientY);
|
||||
reportInteractiveHit(Boolean(target && target.closest(".pet-shell, .bubble")) || dragging, typeof point.reason === "string" ? point.reason.slice(0, 80) : "probe", true);
|
||||
});
|
||||
|
||||
const installMouseInterop = () => {
|
||||
lastInteractiveHit = null;
|
||||
dragging = false;
|
||||
|
|
@ -81,7 +94,7 @@ const installMouseInterop = () => {
|
|||
if (!dragging) setInteractiveHit(false);
|
||||
}, { passive: true });
|
||||
|
||||
setInteractiveHit(false);
|
||||
setInteractiveHit(false, "ready");
|
||||
ipcRenderer.send("openpets:pet-ready");
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -131,6 +131,9 @@ assert.match(petWindowSource, /setIgnoreMouseEvents\(true, \{ forward: true \}\)
|
|||
assert.match(petWindowSource, /setIgnoreMouseEvents\(false\)/, "visible pet and bubble hit targets must re-enable mouse handling.");
|
||||
assert.match(petWindowSource, /openpets:pet-ready/, "pet windows must resync passthrough after each renderer reload.");
|
||||
assert.match(petWindowSource, /function installMousePassthroughAndDrag[\s\S]*?const rearmPassthroughAfterLoad[\s\S]*?process\.platform !== "win32"[\s\S]*?window\.setIgnoreMouseEvents\(false\);[\s\S]*?window\.setIgnoreMouseEvents\(true, \{ forward: true \}\);/, "Windows pet reloads must toggle forwarded mouse passthrough to re-register hover and drag tracking.");
|
||||
assert.match(petWindowSource, /scheduleWindowsMouseForwardingRearm\("did-finish-load\+75ms", 75\);[\s\S]*?scheduleWindowsMouseForwardingRearm\("did-finish-load\+175ms", 175\);/, "Windows pet reloads must retry mouse forwarding rearm after load settles.");
|
||||
assert.match(petWindowSource, /openpets:pet-probe-hit-test/, "Windows pet reloads must probe current cursor hit target when mousemove forwarding is stale.");
|
||||
assert.match(petPreloadSource, /openpets:pet-probe-hit-test[\s\S]*?elementFromPoint\(clientX, clientY\)[\s\S]*?reportInteractiveHit/, "pet preload must answer main-process cursor hit-test probes.");
|
||||
assert.match(petWindowSource, /did-finish-load", rearmAfterLoad/, "pet windows must re-arm mouse passthrough after every content load.");
|
||||
assert.match(petWindowSource, /did-fail-load", handleLoadFailure/, "pet windows must restore passthrough after failed content loads.");
|
||||
assert.match(petWindowSource, /window\.setIgnoreMouseEvents\(false\);[\s\S]*?await window\.loadFile/, "pet reloads must reset OS mouse passthrough before navigation.");
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { app, BrowserWindow, ipcMain, Menu, type IpcMainEvent } from "electron";
|
||||
import { app, BrowserWindow, ipcMain, Menu, screen, type IpcMainEvent } from "electron";
|
||||
import { mkdir, stat, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
|
@ -136,6 +136,8 @@ function installMousePassthroughAndDrag(window: BrowserWindow): void {
|
|||
let dragging: { readonly startScreenX: number; readonly startScreenY: number; readonly startWindowX: number; readonly startWindowY: number } | null = null;
|
||||
let rendererReady = false;
|
||||
let listenersRemoved = false;
|
||||
let lastInteractive = false;
|
||||
const rearmTimers = new Set<NodeJS.Timeout>();
|
||||
const windowId = window.id;
|
||||
const webContents = window.webContents;
|
||||
const canForwardMouseEvents = process.platform === "darwin" || process.platform === "win32";
|
||||
|
|
@ -156,6 +158,53 @@ function installMousePassthroughAndDrag(window: BrowserWindow): void {
|
|||
else window.setIgnoreMouseEvents(false);
|
||||
};
|
||||
|
||||
const clearRearmTimers = (): void => {
|
||||
for (const timer of rearmTimers) clearTimeout(timer);
|
||||
rearmTimers.clear();
|
||||
};
|
||||
|
||||
const getCursorProbe = (): { readonly inside: boolean; readonly cursor: Point; readonly bounds: Electron.Rectangle; readonly clientX: number; readonly clientY: number } => {
|
||||
const cursor = screen.getCursorScreenPoint();
|
||||
const bounds = window.getContentBounds();
|
||||
const clientX = cursor.x - bounds.x;
|
||||
const clientY = cursor.y - bounds.y;
|
||||
return {
|
||||
cursor,
|
||||
bounds,
|
||||
clientX,
|
||||
clientY,
|
||||
inside: clientX >= 0 && clientX < bounds.width && clientY >= 0 && clientY < bounds.height,
|
||||
};
|
||||
};
|
||||
|
||||
const requestCursorHitTestProbe = (reason: string): void => {
|
||||
if (window.isDestroyed() || webContents.isDestroyed()) return;
|
||||
const probe = getCursorProbe();
|
||||
debug("pet.window", "cursor hit-test probe", { windowId, reason, inside: probe.inside, cursor: probe.cursor, bounds: probe.bounds });
|
||||
if (!probe.inside) return;
|
||||
webContents.send("openpets:pet-probe-hit-test", { clientX: probe.clientX, clientY: probe.clientY, reason });
|
||||
};
|
||||
|
||||
const rearmWindowsMouseForwarding = (reason: string): void => {
|
||||
if (window.isDestroyed()) return;
|
||||
if (dragging || lastInteractive) {
|
||||
debug("pet.window", "windows mouse forwarding rearm skipped", { windowId, reason, dragging: Boolean(dragging), interactive: lastInteractive });
|
||||
return;
|
||||
}
|
||||
debug("pet.window", "windows mouse forwarding rearm", { windowId, reason });
|
||||
window.setIgnoreMouseEvents(false);
|
||||
window.setIgnoreMouseEvents(true, { forward: true });
|
||||
requestCursorHitTestProbe(reason);
|
||||
};
|
||||
|
||||
const scheduleWindowsMouseForwardingRearm = (reason: string, delayMs: number): void => {
|
||||
const timer = setTimeout(() => {
|
||||
rearmTimers.delete(timer);
|
||||
rearmWindowsMouseForwarding(reason);
|
||||
}, delayMs);
|
||||
rearmTimers.add(timer);
|
||||
};
|
||||
|
||||
const rearmPassthroughAfterLoad = (): void => {
|
||||
if (window.isDestroyed()) return;
|
||||
if (process.platform !== "win32") {
|
||||
|
|
@ -165,17 +214,20 @@ function installMousePassthroughAndDrag(window: BrowserWindow): void {
|
|||
|
||||
// On Windows, rapid pet HTML reloads can leave Chromium's forwarded mouse
|
||||
// tracking stale while the cursor is already over the transparent window.
|
||||
// Toggle passthrough after each completed reload so hover/drag mousemove
|
||||
// forwarding is re-registered without requiring the cursor to leave/re-enter.
|
||||
window.setIgnoreMouseEvents(false);
|
||||
window.setIgnoreMouseEvents(true, { forward: true });
|
||||
// Toggle immediately, probe the current cursor hit target, then repeat the
|
||||
// toggle shortly after load because Windows sometimes re-registers mouse
|
||||
// forwarding after Chromium finishes late compositing work.
|
||||
rearmWindowsMouseForwarding("did-finish-load");
|
||||
scheduleWindowsMouseForwardingRearm("did-finish-load+75ms", 75);
|
||||
scheduleWindowsMouseForwardingRearm("did-finish-load+175ms", 175);
|
||||
};
|
||||
|
||||
const handleHitTest = (event: IpcMainEvent, interactive: unknown): void => {
|
||||
const handleHitTest = (event: IpcMainEvent, interactive: unknown, source: unknown): void => {
|
||||
if (!isFromWindow(event)) return;
|
||||
rendererReady = true;
|
||||
debug("pet.window", "hit test", { windowId, interactive: Boolean(interactive), dragging });
|
||||
setPassthrough(!interactive && !dragging);
|
||||
lastInteractive = Boolean(interactive);
|
||||
debug("pet.window", "hit test", { windowId, interactive: lastInteractive, dragging, source: typeof source === "string" ? source : undefined });
|
||||
setPassthrough(!lastInteractive && !dragging);
|
||||
};
|
||||
|
||||
const handleReady = (event: IpcMainEvent): void => {
|
||||
|
|
@ -206,12 +258,15 @@ function installMousePassthroughAndDrag(window: BrowserWindow): void {
|
|||
const resetForNavigation = (): void => {
|
||||
dragging = null;
|
||||
rendererReady = false;
|
||||
lastInteractive = false;
|
||||
clearRearmTimers();
|
||||
debug("pet.window", "navigation reset passthrough", { windowId });
|
||||
setPassthrough(false);
|
||||
};
|
||||
|
||||
const rearmAfterLoad = (): void => {
|
||||
dragging = null;
|
||||
lastInteractive = false;
|
||||
debug("pet.window", "load rearm passthrough", { windowId });
|
||||
rearmPassthroughAfterLoad();
|
||||
};
|
||||
|
|
@ -222,6 +277,7 @@ function installMousePassthroughAndDrag(window: BrowserWindow): void {
|
|||
|
||||
const handleLoadFailure = (): void => {
|
||||
dragging = null;
|
||||
lastInteractive = false;
|
||||
debug("pet.window", "load failure rearm passthrough", { windowId });
|
||||
setPassthrough(true);
|
||||
};
|
||||
|
|
@ -234,6 +290,7 @@ function installMousePassthroughAndDrag(window: BrowserWindow): void {
|
|||
ipcMain.off("openpets:pet-drag-start", handleDragStart);
|
||||
ipcMain.off("openpets:pet-drag-move", handleDragMove);
|
||||
ipcMain.off("openpets:pet-drag-end", handleDragEnd);
|
||||
clearRearmTimers();
|
||||
if (!webContents.isDestroyed()) {
|
||||
webContents.off("did-start-navigation", resetForNavigation);
|
||||
webContents.off("did-start-loading", resetForNavigation);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue