Skip to content

Commit bfc73e1

Browse files
Fix arrow keys on safari + release keys and mouse on blur
1 parent b1e6016 commit bfc73e1

3 files changed

Lines changed: 157 additions & 13 deletions

File tree

GDJS/Runtime/inputmanager.ts

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ namespace gdjs {
2525
* if location is not specified.
2626
*/
2727
private static _DEFAULT_LEFT_VARIANT_KEYS: integer[] = [16, 17, 18, 91];
28+
29+
/**
30+
* The `KeyboardEvent.location` of keys on the numeric keypad.
31+
*/
32+
private static _NUMPAD_LOCATION: integer = 3;
2833
private _pressedKeys: Hashtable<boolean>;
2934
private _justPressedKeys: Hashtable<boolean>;
3035
private _releasedKeys: Hashtable<boolean>;
@@ -111,11 +116,24 @@ namespace gdjs {
111116
*
112117
* @param keyCode The raw key code
113118
* @param location The location
119+
* @param code The KeyboardEvent.code, used to know if the key really is on
120+
* the numpad when a numpad location is reported.
114121
*/
115122
static getLocationAwareKeyCode(
116123
keyCode: number,
117-
location: number | null | undefined
124+
location: number | null | undefined,
125+
code?: string | null
118126
): integer {
127+
if (
128+
location === InputManager._NUMPAD_LOCATION &&
129+
code &&
130+
!code.startsWith('Numpad')
131+
) {
132+
// macOS sets the "numeric pad" modifier flag for the arrow keys, which makes
133+
// WebKit (Safari) report them with a numpad location. Only `code` tells the
134+
// numpad keys apart from the arrow keys, so trust it over the location.
135+
location = 0;
136+
}
119137
if (location) {
120138
// If it is a numpad number, do not modify it.
121139
if (96 <= keyCode && keyCode <= 105) {
@@ -135,11 +153,13 @@ namespace gdjs {
135153
* 2 for right keys, and 3 for numpad keys.
136154
* @param keyCode The raw key code associated to the key press.
137155
* @param location The location of the event.
156+
* @param code The KeyboardEvent.code of the event.
138157
*/
139-
onKeyPressed(keyCode: number, location?: number): void {
158+
onKeyPressed(keyCode: number, location?: number, code?: string): void {
140159
const locationAwareKeyCode = InputManager.getLocationAwareKeyCode(
141160
keyCode,
142-
location
161+
location,
162+
code
143163
);
144164
this._pressedKeys.put(locationAwareKeyCode, true);
145165
this._justPressedKeys.put(locationAwareKeyCode, true);
@@ -152,11 +172,13 @@ namespace gdjs {
152172
* 2 for right keys, and 3 for numpad keys.
153173
* @param keyCode The raw key code associated to the key release.
154174
* @param location The location of the event.
175+
* @param code The KeyboardEvent.code of the event.
155176
*/
156-
onKeyReleased(keyCode: number, location?: number): void {
177+
onKeyReleased(keyCode: number, location?: number, code?: string): void {
157178
const locationAwareKeyCode = InputManager.getLocationAwareKeyCode(
158179
keyCode,
159-
location
180+
location,
181+
code
160182
);
161183
this._pressedKeys.put(locationAwareKeyCode, false);
162184
this._justPressedKeys.put(locationAwareKeyCode, false);
@@ -170,6 +192,11 @@ namespace gdjs {
170192
*/
171193
releaseAllPressedKeys(): void {
172194
for (const locationAwareKeyCode in this._pressedKeys.items) {
195+
// Keys keep an entry once released, so only the keys actually held down
196+
// must be made to go through the release state.
197+
if (!this._pressedKeys.items[locationAwareKeyCode]) {
198+
continue;
199+
}
173200
this._pressedKeys.put(locationAwareKeyCode, false);
174201
this._justPressedKeys.put(locationAwareKeyCode, false);
175202
this._releasedKeys.put(locationAwareKeyCode, true);
@@ -427,6 +454,23 @@ namespace gdjs {
427454
this._releasedMouseButtons[buttonCode] = true;
428455
}
429456

457+
/**
458+
* Release all the mouse buttons that are currently pressed.
459+
*/
460+
releaseAllPressedMouseButtons(): void {
461+
for (
462+
let buttonCode = 0;
463+
buttonCode < this._pressedMouseButtons.length;
464+
buttonCode++
465+
) {
466+
if (this._pressedMouseButtons[buttonCode]) {
467+
// Go through `onMouseButtonReleased` so that the touch simulated by
468+
// the left button is ended too.
469+
this.onMouseButtonReleased(buttonCode);
470+
}
471+
}
472+
}
473+
430474
/**
431475
* Return true if the mouse button corresponding to buttonCode is pressed.
432476
* @param buttonCode The mouse button code (0: Left button, 1: Right button).

GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ namespace gdjs {
6262

6363
_wasDisposed: boolean = false;
6464

65+
_unregisterFocusListeners: (() => void) | null = null;
66+
6567
/**
6668
* @param game The game that is being rendered
6769
* @param forceFullscreen If fullscreen should be always activated
@@ -753,7 +755,7 @@ namespace gdjs {
753755
return;
754756
}
755757

756-
manager.onKeyPressed(e.keyCode, e.location);
758+
manager.onKeyPressed(e.keyCode, e.location, e.code);
757759
};
758760
document.onkeyup = (e) => {
759761
if (isFocusingDomElement()) {
@@ -770,11 +772,11 @@ namespace gdjs {
770772
// This means the key would be considered as "stuck" from the game's perspective
771773
// it would never be released unless it's pressed and released again (without meta).
772774
// Out of caution, we simulate a release of the key that were pressed with meta key.
773-
for (const {
774-
location,
775-
keyCode,
776-
} of keysPressedWithMetaPressedByCode.values()) {
777-
manager.onKeyReleased(keyCode, location);
775+
for (const [
776+
code,
777+
{ location, keyCode },
778+
] of keysPressedWithMetaPressedByCode) {
779+
manager.onKeyReleased(keyCode, location, code);
778780
}
779781
keysPressedWithMetaPressedByCode.clear();
780782
}
@@ -792,7 +794,32 @@ namespace gdjs {
792794
e.preventDefault();
793795
}
794796

795-
manager.onKeyReleased(e.keyCode, e.location);
797+
manager.onKeyReleased(e.keyCode, e.location, e.code);
798+
};
799+
800+
// No "keyup" or "mouseup" is received for the keys and mouse buttons that are
801+
// still held down when the game loses the focus (when switching to another
802+
// window or tab), which would leave them stuck in a pressed state.
803+
// Release them all instead.
804+
const releaseAllPressedInputs = () => {
805+
keysPressedWithMetaPressedByCode.clear();
806+
manager.releaseAllPressedKeys();
807+
manager.releaseAllPressedMouseButtons();
808+
};
809+
const onBlur = () => {
810+
releaseAllPressedInputs();
811+
};
812+
const onVisibilityChange = () => {
813+
// On mobile, switching to another app can hide the game without blurring it.
814+
if (document.visibilityState === 'hidden') {
815+
releaseAllPressedInputs();
816+
}
817+
};
818+
window.addEventListener('blur', onBlur);
819+
document.addEventListener('visibilitychange', onVisibilityChange);
820+
this._unregisterFocusListeners = () => {
821+
window.removeEventListener('blur', onBlur);
822+
document.removeEventListener('visibilitychange', onVisibilityChange);
796823
};
797824

798825
// Mouse:
@@ -1099,11 +1126,16 @@ namespace gdjs {
10991126
/**
11001127
* Dispose the renderers (PixiJS and/or Three.js) as well as DOM elements
11011128
* used for the game (the canvas, if specified, and the additional DOM container
1102-
* created on top of it to allow display HTML elements, for example for text inputs).
1129+
* created on top of it to allow display HTML elements, for example for text inputs),
1130+
* and the events listeners registered on the window and document.
11031131
*
11041132
* @param removeCanvas If true, the canvas will be removed from the DOM.
11051133
*/
11061134
dispose(removeCanvas?: boolean) {
1135+
if (this._unregisterFocusListeners) {
1136+
this._unregisterFocusListeners();
1137+
this._unregisterFocusListeners = null;
1138+
}
11071139
this._pixiRenderer?.destroy();
11081140
this._threeRenderer?.dispose();
11091141
this._pixiRenderer = null;

GDJS/tests/tests/inputmanager.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,74 @@ describe('gdjs.InputManager', () => {
9191
inputManager.onKeyReleased(17);
9292
});
9393

94+
it('should ignore a numpad location that is contradicted by the code', () => {
95+
// Safari on macOS reports the arrow keys with a numpad location, because macOS
96+
// sets the "numeric pad" modifier flag for them.
97+
inputManager.onKeyPressed(38, 3, 'ArrowUp');
98+
expect(inputManager.getLastPressedKey()).to.be(38);
99+
expect(inputManager.isKeyPressed(38)).to.be(true);
100+
expect(inputManager.isKeyPressed(3038)).to.be(false);
101+
inputManager.onKeyReleased(38, 3, 'ArrowUp');
102+
expect(inputManager.wasKeyReleased(38)).to.be(true);
103+
expect(inputManager.wasKeyReleased(3038)).to.be(false);
104+
inputManager.onFrameEnded();
105+
106+
// A numpad key with NumLock off keeps its numpad location.
107+
inputManager.onKeyPressed(38, 3, 'Numpad8');
108+
expect(inputManager.getLastPressedKey()).to.be(3038);
109+
expect(inputManager.isKeyPressed(3038)).to.be(true);
110+
expect(inputManager.isKeyPressed(38)).to.be(false);
111+
inputManager.onKeyReleased(38, 3, 'Numpad8');
112+
expect(inputManager.wasKeyReleased(3038)).to.be(true);
113+
inputManager.onFrameEnded();
114+
115+
// Without a code, the location is trusted as before.
116+
inputManager.onKeyPressed(38, 3);
117+
expect(inputManager.getLastPressedKey()).to.be(3038);
118+
inputManager.onKeyReleased(38, 3);
119+
});
120+
121+
it('should release the held keys and mouse buttons when the game loses the focus', () => {
122+
// A key that was already pressed and released before losing the focus.
123+
inputManager.onKeyPressed(65, 0, 'KeyA');
124+
inputManager.onKeyReleased(65, 0, 'KeyA');
125+
inputManager.onFrameEnded();
126+
127+
// Keys and mouse buttons still held down when the focus is lost.
128+
inputManager.onKeyPressed(87, 0, 'KeyW');
129+
inputManager.onKeyPressed(16, 1, 'ShiftLeft');
130+
inputManager.onMouseButtonPressed(gdjs.InputManager.MOUSE_LEFT_BUTTON);
131+
expect(inputManager.isKeyPressed(87)).to.be(true);
132+
expect(inputManager.isKeyPressed(1016)).to.be(true);
133+
expect(
134+
inputManager.isMouseButtonPressed(gdjs.InputManager.MOUSE_LEFT_BUTTON)
135+
).to.be(true);
136+
inputManager.onFrameEnded();
137+
138+
inputManager.releaseAllPressedKeys();
139+
inputManager.releaseAllPressedMouseButtons();
140+
expect(inputManager.isKeyPressed(87)).to.be(false);
141+
expect(inputManager.isKeyPressed(1016)).to.be(false);
142+
expect(inputManager.wasKeyReleased(87)).to.be(true);
143+
expect(inputManager.wasKeyReleased(1016)).to.be(true);
144+
expect(inputManager.anyKeyPressed()).to.be(false);
145+
expect(
146+
inputManager.isMouseButtonPressed(gdjs.InputManager.MOUSE_LEFT_BUTTON)
147+
).to.be(false);
148+
expect(
149+
inputManager.isMouseButtonReleased(gdjs.InputManager.MOUSE_LEFT_BUTTON)
150+
).to.be(true);
151+
expect(inputManager.anyMouseButtonPressed()).to.be(false);
152+
// The touch simulated by the left button must have ended too.
153+
expect(
154+
inputManager.hasTouchEnded(gdjs.InputManager.MOUSE_TOUCH_ID)
155+
).to.be(true);
156+
157+
// The key released before the focus loss must not be released a second time.
158+
expect(inputManager.wasKeyReleased(65)).to.be(false);
159+
inputManager.onFrameEnded();
160+
});
161+
94162
it('should handle mouse events', () => {
95163
inputManager.onMouseMove(500, 600);
96164
expect(inputManager.getCursorX()).to.be(500);

0 commit comments

Comments
 (0)