Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ So your change should look like:
by CSS and wasn't rendered at its natural size (#1096) - @Stanko
- Modified `pos`, `skew` and `scale` components to make operations like
`obj.pos.x += 1` work again (#1109) - @ErikGXDev
- Fixed `isKeyDown` and `isButtonDown` getting stuck on game loosing focus
(#1101) - @Stanko

## [4000.0.0-alpha.27.1] - 2026-05-12

Expand Down
41 changes: 41 additions & 0 deletions src/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ export class ButtonState<T = string, A = never> {
state.events.trigger(this._releaseEv as any, btn, this._arg);
}
}
releaseAll(state: AppState) {
for (const btn of this.down) {
this.release(btn, state);
}
}
}

class GamepadState {
Expand Down Expand Up @@ -978,8 +983,39 @@ export const initApp = (
const docEvents: EventList<DocumentEventMap> = {};
const winEvents: EventList<WindowEventMap> = {};

let releaseHeldInputsQueued = false;

function releaseHeldInputs() {
state.buttonHandler.releaseKeyboardMouse(state);
state.keyState.releaseAll(state);
state.mouseState.releaseAll(state);
}

function queueReleaseHeldInputs() {
if (releaseHeldInputsQueued) {
return;
}
releaseHeldInputsQueued = true;
state.events.onOnce("input", () => {
releaseHeldInputsQueued = false;
releaseHeldInputs();
});
}

function releaseHeldInputsOnFocusLoss() {
// Release all inputs immediately when blur/hide happens,
// and then queue it to to catch any queued events on the next input tick,
// that wouldn't be processed otherwise
releaseHeldInputs();
queueReleaseHeldInputs();
Comment thread
imaginarny marked this conversation as resolved.
}

const pd = opt.pixelDensity || 1;

canvasEvents.blur = () => {
releaseHeldInputsOnFocusLoss();
};

canvasEvents.mousemove = (e) => {
// 🍝 Here we depend of GFX Context even if initGfx needs initApp for being used
// Letterbox creates some black bars so we need to remove that for calculating
Expand Down Expand Up @@ -1259,11 +1295,16 @@ export const initApp = (
state.events.trigger("show");
}
else {
releaseHeldInputsOnFocusLoss();
state.isHidden = true;
state.events.trigger("hide");
}
};

winEvents.blur = () => {
releaseHeldInputsOnFocusLoss();
};

winEvents.gamepadconnected = (e) => {
const kbGamepad = registerGamepad(e.gamepad);
state.events.onOnce("input", () => {
Expand Down
29 changes: 29 additions & 0 deletions src/app/inputBindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,15 @@ class ChordedButtonDetector<T extends string = string> {
}
return canceledButtons;
}
releaseAll(): string[] {
const buttons = [...this.buttonsUsed];
this.buttonsUsed.clear();
this.mods.forEach((_, mod) => this.mods.set(mod, false));
return buttons;
}
isButtonUsed(button: string): boolean {
return this.buttonsUsed.has(button);
}
}

export class ButtonProcessor {
Expand Down Expand Up @@ -176,6 +185,16 @@ export class ButtonProcessor {
this.state.release(button, state);
}
}
private _releaseKeyboardMouse(buttons: string[], state: AppState) {
for (let button of buttons) {
if (
this.state.down.has(button)
&& !this.byGamepad.isButtonUsed(button)
) {
this.state.release(button, state);
}
}
}
processKeydown(key: Key, keyCode: string, state: AppState) {
this._maybePress(this.byKey.handleDown(key), state);
this._maybePress(this.byKeyCode.handleDown(keyCode), state);
Expand All @@ -196,6 +215,16 @@ export class ButtonProcessor {
processGamepadButtonUp(gb: KGamepadButton, state: AppState) {
this._maybeRelease(this.byGamepad.handleUp(gb), state);
}
releaseKeyboardMouse(state: AppState) {
this._releaseKeyboardMouse(
[
...this.byKey.releaseAll(),
...this.byKeyCode.releaseAll(),
...this.byMouse.releaseAll(),
],
state,
);
}
update() {
this.state.update();
}
Expand Down