Fix Windows pet drag recovery
This commit is contained in:
parent
5ae609429c
commit
e3babcbbe0
4 changed files with 72 additions and 9 deletions
|
|
@ -144,9 +144,11 @@ assert.match(petWindowSource, /function installMousePassthroughAndDrag/, "pet wi
|
|||
assert.match(petWindowSource, /setIgnoreMouseEvents\(true, \{ forward: true \}\)/, "transparent pet window background must use OS-level mouse passthrough.");
|
||||
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, /function installMousePassthroughAndDrag[\s\S]*?const rearmPassthrough[\s\S]*?process\.platform !== "win32"[\s\S]*?rearmWindowsMouseForwarding\(reason\)/, "Windows pet reloads must toggle forwarded mouse passthrough to re-register hover and drag tracking.");
|
||||
assert.match(petWindowSource, /scheduleWindowsMouseForwardingRearm\(`\$\{reason\}\+75ms`, 75\);[\s\S]*?scheduleWindowsMouseForwardingRearm\(`\$\{reason\}\+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(petWindowSource, /export function recoverPetMouseInterop/, "pet windows must expose a controlled mouse interop recovery hook for OS display and resume events.");
|
||||
assert.match(petWindowSource, /petMouseInteropRecovery\.set\(window, scheduleMouseInteropRecovery\)/, "pet windows must register their mouse interop recovery callback.");
|
||||
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.");
|
||||
|
|
@ -163,6 +165,9 @@ assert.match(petWindowSource, /\.pet-shell[\s\S]*?-webkit-app-region: no-drag; c
|
|||
assert.match(petPreloadSource, /openpets:pet-hit-test/, "pet preload must report visible pet and bubble hit testing for passthrough.");
|
||||
assert.match(petPreloadSource, /openpets:pet-ready/, "pet preload must report readiness after installing mouse handlers.");
|
||||
assert.match(petPreloadSource, /openpets:pet-drag-start/, "pet preload must start controlled pet dragging from the sprite.");
|
||||
assert.match(defaultPetControllerSource, /powerMonitor\.on\("resume", recoverDefaultPetWindowAfterResume\)/, "default pet must recover mouse interop after Windows sleep or resume.");
|
||||
assert.match(defaultPetControllerSource, /recoverDefaultPetMouseInterop\("display-change"\)/, "default pet must recover mouse interop after monitor topology changes.");
|
||||
assert.match(windowsSource, /recoverDefaultPetMouseInterop\("default-pet-changed"\)/, "changing default pet must recover mouse interop for dragging without app restart.");
|
||||
assert.match(petWindowSource, /function installPetContextMenu/, "pet windows must install a native right-click context menu.");
|
||||
assert.match(petWindowSource, /webContents\.on\("context-menu"/, "pet context menu must be handled in the Electron main process.");
|
||||
assert.match(petWindowSource, /Menu\.buildFromTemplate/, "pet context menu must use a small native Electron menu.");
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { BrowserWindow, screen } from "electron";
|
||||
import { BrowserWindow, powerMonitor, screen } from "electron";
|
||||
|
||||
import { getAppStateSnapshot, getDefaultPetPosition, resetDefaultPetPosition, setDefaultPetPosition, updatePreferences } from "./app-state.js";
|
||||
import { defaultPetWindowSize, getDefaultPetInitialPosition } from "./display.js";
|
||||
import { debug, info } from "./logger.js";
|
||||
import { transientDisplayMs, type OpenPetsReaction } from "./local-ipc-protocol.js";
|
||||
import { clearTransientReaction, createDefaultPetWindow, getSafeDefaultPetPosition, getTransientDisplayDurationMs, getTransientReactionAnimationMs, loadDefaultPetContent, mergePetTransientDisplay, readWindowPosition, setPetReactionState, type PetStatusBadgeReaction, type PetTransientDisplay } from "./pet-window.js";
|
||||
import { clearTransientReaction, createDefaultPetWindow, getSafeDefaultPetPosition, getTransientDisplayDurationMs, getTransientReactionAnimationMs, loadDefaultPetContent, mergePetTransientDisplay, readWindowPosition, recoverPetMouseInterop, setPetReactionState, type PetStatusBadgeReaction, type PetTransientDisplay } from "./pet-window.js";
|
||||
|
||||
let defaultPetWindow: BrowserWindow | null = null;
|
||||
let paused = false;
|
||||
|
|
@ -70,6 +70,16 @@ export function refreshDefaultPetContent(): void {
|
|||
void loadDefaultPetContent(defaultPetWindow, paused, transientDisplay, statusBadge, getCurrentDismissToken());
|
||||
}
|
||||
|
||||
export function recoverDefaultPetMouseInterop(reason: string): void {
|
||||
if (!defaultPetWindow || defaultPetWindow.isDestroyed()) {
|
||||
debug("pet.default", "mouse interop recovery skipped", { reason, skippedReason: "no-window" });
|
||||
return;
|
||||
}
|
||||
|
||||
debug("pet.default", "mouse interop recovery requested", { windowId: defaultPetWindow.id, reason, petId: getAppStateSnapshot().preferences.defaultPetId });
|
||||
recoverPetMouseInterop(defaultPetWindow, reason);
|
||||
}
|
||||
|
||||
export function applyExternalPetReaction(reaction: OpenPetsReaction): { readonly shown: boolean; readonly reason?: string } {
|
||||
if (paused) {
|
||||
return { shown: false, reason: "paused" };
|
||||
|
|
@ -112,6 +122,7 @@ export function installDefaultPetDisplayHandlers(): void {
|
|||
screen.on("display-added", reclampDefaultPetWindow);
|
||||
screen.on("display-removed", reclampDefaultPetWindow);
|
||||
screen.on("display-metrics-changed", reclampDefaultPetWindow);
|
||||
powerMonitor.on("resume", recoverDefaultPetWindowAfterResume);
|
||||
}
|
||||
|
||||
function handleBubbleDismissed(dismissToken: string): void {
|
||||
|
|
@ -248,6 +259,12 @@ function reclampDefaultPetWindow(): void {
|
|||
info("pet.default", "reclamp position", { windowId: defaultPetWindow.id, position: safePosition });
|
||||
defaultPetWindow.setPosition(safePosition.x, safePosition.y, false);
|
||||
setDefaultPetPosition(safePosition);
|
||||
recoverDefaultPetMouseInterop("display-change");
|
||||
}
|
||||
|
||||
function recoverDefaultPetWindowAfterResume(): void {
|
||||
recoverDefaultPetMouseInterop("power-resume");
|
||||
setTimeout(() => recoverDefaultPetMouseInterop("power-resume+500ms"), 500).unref?.();
|
||||
}
|
||||
|
||||
export function shouldOpenDefaultPetOnLaunch(): boolean {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ const petWindowRenderCache = new WeakMap<BrowserWindow, string>();
|
|||
|
||||
const windowLoadChains = new WeakMap<BrowserWindow, Promise<void>>();
|
||||
const windowLoadSequences = new WeakMap<BrowserWindow, number>();
|
||||
const petMouseInteropRecovery = new WeakMap<BrowserWindow, (reason: string) => void>();
|
||||
|
||||
export function createDefaultPetWindow(options: DefaultPetWindowOptions, dismissToken?: string): BrowserWindow {
|
||||
const window = createBasePetWindow("OpenPets — Default Pet", options.position);
|
||||
|
|
@ -91,6 +92,17 @@ export function createAgentPetWindow(options: AgentPetWindowOptions, dismissToke
|
|||
return window;
|
||||
}
|
||||
|
||||
export function recoverPetMouseInterop(window: BrowserWindow, reason: string): void {
|
||||
if (window.isDestroyed()) return;
|
||||
const recover = petMouseInteropRecovery.get(window);
|
||||
if (recover) {
|
||||
recover(reason);
|
||||
return;
|
||||
}
|
||||
|
||||
debug("pet.window", "mouse interop recovery skipped", { windowId: window.id, reason, skippedReason: "unregistered-window" });
|
||||
}
|
||||
|
||||
function installPetContextMenu(window: BrowserWindow, action: { readonly label: string; readonly click: () => void }): void {
|
||||
const webContents = window.webContents;
|
||||
const handleContextMenu = (event: Electron.Event): void => {
|
||||
|
|
@ -114,6 +126,26 @@ function installMousePassthroughAndDrag(window: BrowserWindow, onBubbleDismissed
|
|||
const webContents = window.webContents;
|
||||
const canForwardMouseEvents = process.platform === "darwin" || process.platform === "win32";
|
||||
|
||||
const scheduleMouseInteropRecovery = (reason: string): void => {
|
||||
if (window.isDestroyed()) return;
|
||||
dragging = null;
|
||||
rendererReady = false;
|
||||
lastInteractive = false;
|
||||
clearRearmTimers();
|
||||
debug("pet.window", "mouse interop recovery", { windowId, reason });
|
||||
setPassthrough(false);
|
||||
if (process.platform === "win32") {
|
||||
requestCursorHitTestProbe(reason);
|
||||
scheduleWindowsMouseForwardingRearm(`${reason}+250ms`, 250);
|
||||
scheduleWindowsMouseForwardingRearm(`${reason}+500ms`, 500);
|
||||
scheduleWindowsMouseForwardingRearm(`${reason}+1000ms`, 1_000);
|
||||
scheduleWindowsMouseForwardingRearm(`${reason}+1500ms`, 1_500);
|
||||
return;
|
||||
}
|
||||
|
||||
rearmPassthrough(reason);
|
||||
};
|
||||
|
||||
const isFromWindow = (event: IpcMainEvent): boolean => event.sender === webContents;
|
||||
const setPassthrough = (passthrough: boolean): void => {
|
||||
if (window.isDestroyed()) return;
|
||||
|
|
@ -177,7 +209,7 @@ function installMousePassthroughAndDrag(window: BrowserWindow, onBubbleDismissed
|
|||
rearmTimers.add(timer);
|
||||
};
|
||||
|
||||
const rearmPassthroughAfterLoad = (): void => {
|
||||
const rearmPassthrough = (reason: string): void => {
|
||||
if (window.isDestroyed()) return;
|
||||
if (process.platform !== "win32") {
|
||||
setPassthrough(true);
|
||||
|
|
@ -189,9 +221,13 @@ function installMousePassthroughAndDrag(window: BrowserWindow, onBubbleDismissed
|
|||
// 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);
|
||||
rearmWindowsMouseForwarding(reason);
|
||||
scheduleWindowsMouseForwardingRearm(`${reason}+75ms`, 75);
|
||||
scheduleWindowsMouseForwardingRearm(`${reason}+175ms`, 175);
|
||||
};
|
||||
|
||||
const rearmPassthroughAfterLoad = (): void => {
|
||||
rearmPassthrough("did-finish-load");
|
||||
};
|
||||
|
||||
const handleHitTest = (event: IpcMainEvent, interactive: unknown, source: unknown): void => {
|
||||
|
|
@ -270,6 +306,7 @@ function installMousePassthroughAndDrag(window: BrowserWindow, onBubbleDismissed
|
|||
ipcMain.off("openpets:pet-drag-end", handleDragEnd);
|
||||
ipcMain.off("openpets:bubble-dismissed", handleBubbleDismissed);
|
||||
clearRearmTimers();
|
||||
petMouseInteropRecovery.delete(window);
|
||||
if (!webContents.isDestroyed()) {
|
||||
webContents.off("did-start-navigation", resetForNavigation);
|
||||
webContents.off("did-start-loading", resetForNavigation);
|
||||
|
|
@ -279,6 +316,8 @@ function installMousePassthroughAndDrag(window: BrowserWindow, onBubbleDismissed
|
|||
}
|
||||
};
|
||||
|
||||
petMouseInteropRecovery.set(window, scheduleMouseInteropRecovery);
|
||||
|
||||
ipcMain.on("openpets:pet-ready", handleReady);
|
||||
ipcMain.on("openpets:pet-hit-test", handleHitTest);
|
||||
ipcMain.on("openpets:pet-drag-start", handleDragStart);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { refreshAgentPetContent } from "./agent-pet-controller.js";
|
|||
import { completeOnboarding, getAppStateSnapshot, normalizePetScale, petScaleOptions, updatePreferences } from "./app-state.js";
|
||||
import { getCatalogPageUiState, getCatalogSearchUiState, getCatalogUiState } from "./catalog.js";
|
||||
import { getCodexPetsUiState, importCodexPet, readCodexPetSpritesheet } from "./codex-pets.js";
|
||||
import { refreshDefaultPetContent, resetDefaultPetToInitialPosition } from "./default-pet-controller.js";
|
||||
import { recoverDefaultPetMouseInterop, refreshDefaultPetContent, resetDefaultPetToInitialPosition } from "./default-pet-controller.js";
|
||||
import { installPet, removePet, setDefaultInstalledPet } from "./pet-installation.js";
|
||||
import { getInstalledPetDir } from "./pet-paths.js";
|
||||
import { defaultPetSprite, reactionAnimationMetadata, selectableAnimationMetadata, validateReactionAnimationOverrides } from "./reaction-animation-mapping.js";
|
||||
|
|
@ -175,6 +175,8 @@ export function installInternalUiHandlers(): void {
|
|||
|
||||
const state = await setDefaultInstalledPet(petId);
|
||||
refreshDefaultPetContent();
|
||||
recoverDefaultPetMouseInterop("default-pet-changed");
|
||||
setTimeout(() => recoverDefaultPetMouseInterop("default-pet-changed+500ms"), 500).unref?.();
|
||||
return state;
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue