@gjsify/gamepad 0.3.12 → 0.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,35 +1,41 @@
1
+ //#region src/axis-mapping.ts
2
+ /**
3
+ * SDL logical axis indices as reported by Manette.Event.get_absolute().
4
+ * These are the SDL gamepad mapping indices, NOT Linux ABS_* hardware codes.
5
+ */
1
6
  const ManetteAxis = {
2
- LEFT_X: 0,
3
- // SDL leftx
4
- LEFT_Y: 1,
5
- // SDL lefty
6
- RIGHT_X: 2,
7
- // SDL rightx
8
- RIGHT_Y: 3,
9
- // SDL righty
10
- LEFT_TRIGGER: 4,
11
- // SDL lefttrigger
12
- RIGHT_TRIGGER: 5
13
- // SDL righttrigger
7
+ LEFT_X: 0,
8
+ LEFT_Y: 1,
9
+ RIGHT_X: 2,
10
+ RIGHT_Y: 3,
11
+ LEFT_TRIGGER: 4,
12
+ RIGHT_TRIGGER: 5
14
13
  };
14
+ /**
15
+ * W3C standard gamepad axis indices.
16
+ * https://w3c.github.io/gamepad/#remapping
17
+ */
15
18
  const W3CAxis = {
16
- LEFT_STICK_X: 0,
17
- LEFT_STICK_Y: 1,
18
- RIGHT_STICK_X: 2,
19
- RIGHT_STICK_Y: 3
19
+ LEFT_STICK_X: 0,
20
+ LEFT_STICK_Y: 1,
21
+ RIGHT_STICK_X: 2,
22
+ RIGHT_STICK_Y: 3
20
23
  };
24
+ /** Total number of axes in the W3C standard mapping. */
21
25
  const W3C_AXIS_COUNT = 4;
22
- const MANETTE_TO_W3C_AXIS = /* @__PURE__ */ new Map([
23
- [ManetteAxis.LEFT_X, W3CAxis.LEFT_STICK_X],
24
- [ManetteAxis.LEFT_Y, W3CAxis.LEFT_STICK_Y],
25
- [ManetteAxis.RIGHT_X, W3CAxis.RIGHT_STICK_X],
26
- [ManetteAxis.RIGHT_Y, W3CAxis.RIGHT_STICK_Y]
26
+ /**
27
+ * Maps SDL logical axis → W3C axis index.
28
+ * Stick axes (0–3) map 1:1. Trigger axes (4–5) are NOT in this map —
29
+ * they are handled separately in gamepad-manager as buttons[6]/buttons[7].
30
+ */
31
+ const MANETTE_TO_W3C_AXIS = new Map([
32
+ [ManetteAxis.LEFT_X, W3CAxis.LEFT_STICK_X],
33
+ [ManetteAxis.LEFT_Y, W3CAxis.LEFT_STICK_Y],
34
+ [ManetteAxis.RIGHT_X, W3CAxis.RIGHT_STICK_X],
35
+ [ManetteAxis.RIGHT_Y, W3CAxis.RIGHT_STICK_Y]
27
36
  ]);
28
- const TRIGGER_PRESS_THRESHOLD = 0.5;
29
- export {
30
- MANETTE_TO_W3C_AXIS,
31
- ManetteAxis,
32
- TRIGGER_PRESS_THRESHOLD,
33
- W3CAxis,
34
- W3C_AXIS_COUNT
35
- };
37
+ /** Threshold above which an analog trigger is considered "pressed". */
38
+ const TRIGGER_PRESS_THRESHOLD = .5;
39
+
40
+ //#endregion
41
+ export { MANETTE_TO_W3C_AXIS, ManetteAxis, TRIGGER_PRESS_THRESHOLD, W3CAxis, W3C_AXIS_COUNT };
@@ -1,92 +1,76 @@
1
+ //#region src/button-mapping.ts
2
+ /**
3
+ * Linux BTN_* input event codes as reported by Manette.Event.get_button().
4
+ * These are the actual values libmanette 0.2 passes in button-press/release signals.
5
+ */
1
6
  const LinuxButton = {
2
- BTN_SOUTH: 304,
3
- // 0x130 — A (Xbox), B (Nintendo), Cross (PlayStation)
4
- BTN_EAST: 305,
5
- // 0x131 — B (Xbox), A (Nintendo), Circle (PlayStation)
6
- BTN_C: 306,
7
- // 0x132
8
- BTN_NORTH: 307,
9
- // 0x133 — Y (Xbox), X (Nintendo), Triangle (PlayStation)
10
- BTN_WEST: 308,
11
- // 0x134 — X (Xbox), Y (Nintendo), Square (PlayStation)
12
- BTN_Z: 309,
13
- // 0x135
14
- BTN_TL: 310,
15
- // 0x136 — Left shoulder (L, LB)
16
- BTN_TR: 311,
17
- // 0x137 — Right shoulder (R, RB)
18
- BTN_TL2: 312,
19
- // 0x138 — Left trigger (LT, L2)
20
- BTN_TR2: 313,
21
- // 0x139 — Right trigger (RT, R2)
22
- BTN_SELECT: 314,
23
- // 0x13a — Select / Back / Share
24
- BTN_START: 315,
25
- // 0x13b — Start / Menu / Options
26
- BTN_MODE: 316,
27
- // 0x13c — Home / Guide / PS
28
- BTN_THUMBL: 317,
29
- // 0x13d — Left stick click (L3)
30
- BTN_THUMBR: 318,
31
- // 0x13e — Right stick click (R3)
32
- BTN_DPAD_UP: 544,
33
- // 0x220
34
- BTN_DPAD_DOWN: 545,
35
- // 0x221
36
- BTN_DPAD_LEFT: 546,
37
- // 0x222
38
- BTN_DPAD_RIGHT: 547
39
- // 0x223
7
+ BTN_SOUTH: 304,
8
+ BTN_EAST: 305,
9
+ BTN_C: 306,
10
+ BTN_NORTH: 307,
11
+ BTN_WEST: 308,
12
+ BTN_Z: 309,
13
+ BTN_TL: 310,
14
+ BTN_TR: 311,
15
+ BTN_TL2: 312,
16
+ BTN_TR2: 313,
17
+ BTN_SELECT: 314,
18
+ BTN_START: 315,
19
+ BTN_MODE: 316,
20
+ BTN_THUMBL: 317,
21
+ BTN_THUMBR: 318,
22
+ BTN_DPAD_UP: 544,
23
+ BTN_DPAD_DOWN: 545,
24
+ BTN_DPAD_LEFT: 546,
25
+ BTN_DPAD_RIGHT: 547
40
26
  };
27
+ /**
28
+ * W3C standard gamepad button indices.
29
+ * https://w3c.github.io/gamepad/#remapping
30
+ */
41
31
  const W3CButton = {
42
- FACE_1: 0,
43
- // A (Xbox), B (Nintendo), Cross (PlayStation)
44
- FACE_2: 1,
45
- // B (Xbox), A (Nintendo), Circle (PlayStation)
46
- FACE_3: 2,
47
- // X (Xbox), Y (Nintendo), Square (PlayStation)
48
- FACE_4: 3,
49
- // Y (Xbox), X (Nintendo), Triangle (PlayStation)
50
- LEFT_BUMPER: 4,
51
- RIGHT_BUMPER: 5,
52
- LEFT_TRIGGER: 6,
53
- // Analog trigger — also populated from axis events
54
- RIGHT_TRIGGER: 7,
55
- // Analog trigger — also populated from axis events
56
- SELECT: 8,
57
- START: 9,
58
- LEFT_STICK: 10,
59
- RIGHT_STICK: 11,
60
- DPAD_UP: 12,
61
- DPAD_DOWN: 13,
62
- DPAD_LEFT: 14,
63
- DPAD_RIGHT: 15,
64
- HOME: 16
32
+ FACE_1: 0,
33
+ FACE_2: 1,
34
+ FACE_3: 2,
35
+ FACE_4: 3,
36
+ LEFT_BUMPER: 4,
37
+ RIGHT_BUMPER: 5,
38
+ LEFT_TRIGGER: 6,
39
+ RIGHT_TRIGGER: 7,
40
+ SELECT: 8,
41
+ START: 9,
42
+ LEFT_STICK: 10,
43
+ RIGHT_STICK: 11,
44
+ DPAD_UP: 12,
45
+ DPAD_DOWN: 13,
46
+ DPAD_LEFT: 14,
47
+ DPAD_RIGHT: 15,
48
+ HOME: 16
65
49
  };
50
+ /** Total number of buttons in the W3C standard mapping. */
66
51
  const W3C_BUTTON_COUNT = 17;
67
- const MANETTE_TO_W3C_BUTTON = /* @__PURE__ */ new Map([
68
- [LinuxButton.BTN_SOUTH, W3CButton.FACE_1],
69
- [LinuxButton.BTN_EAST, W3CButton.FACE_2],
70
- [LinuxButton.BTN_WEST, W3CButton.FACE_3],
71
- [LinuxButton.BTN_NORTH, W3CButton.FACE_4],
72
- [LinuxButton.BTN_TL, W3CButton.LEFT_BUMPER],
73
- [LinuxButton.BTN_TR, W3CButton.RIGHT_BUMPER],
74
- [LinuxButton.BTN_TL2, W3CButton.LEFT_TRIGGER],
75
- [LinuxButton.BTN_TR2, W3CButton.RIGHT_TRIGGER],
76
- [LinuxButton.BTN_SELECT, W3CButton.SELECT],
77
- [LinuxButton.BTN_START, W3CButton.START],
78
- [LinuxButton.BTN_THUMBL, W3CButton.LEFT_STICK],
79
- [LinuxButton.BTN_THUMBR, W3CButton.RIGHT_STICK],
80
- [LinuxButton.BTN_DPAD_UP, W3CButton.DPAD_UP],
81
- [LinuxButton.BTN_DPAD_DOWN, W3CButton.DPAD_DOWN],
82
- [LinuxButton.BTN_DPAD_LEFT, W3CButton.DPAD_LEFT],
83
- [LinuxButton.BTN_DPAD_RIGHT, W3CButton.DPAD_RIGHT],
84
- [LinuxButton.BTN_MODE, W3CButton.HOME]
52
+ /**
53
+ * Maps Linux BTN_* code → W3C standard button index.
54
+ */
55
+ const MANETTE_TO_W3C_BUTTON = new Map([
56
+ [LinuxButton.BTN_SOUTH, W3CButton.FACE_1],
57
+ [LinuxButton.BTN_EAST, W3CButton.FACE_2],
58
+ [LinuxButton.BTN_WEST, W3CButton.FACE_3],
59
+ [LinuxButton.BTN_NORTH, W3CButton.FACE_4],
60
+ [LinuxButton.BTN_TL, W3CButton.LEFT_BUMPER],
61
+ [LinuxButton.BTN_TR, W3CButton.RIGHT_BUMPER],
62
+ [LinuxButton.BTN_TL2, W3CButton.LEFT_TRIGGER],
63
+ [LinuxButton.BTN_TR2, W3CButton.RIGHT_TRIGGER],
64
+ [LinuxButton.BTN_SELECT, W3CButton.SELECT],
65
+ [LinuxButton.BTN_START, W3CButton.START],
66
+ [LinuxButton.BTN_THUMBL, W3CButton.LEFT_STICK],
67
+ [LinuxButton.BTN_THUMBR, W3CButton.RIGHT_STICK],
68
+ [LinuxButton.BTN_DPAD_UP, W3CButton.DPAD_UP],
69
+ [LinuxButton.BTN_DPAD_DOWN, W3CButton.DPAD_DOWN],
70
+ [LinuxButton.BTN_DPAD_LEFT, W3CButton.DPAD_LEFT],
71
+ [LinuxButton.BTN_DPAD_RIGHT, W3CButton.DPAD_RIGHT],
72
+ [LinuxButton.BTN_MODE, W3CButton.HOME]
85
73
  ]);
86
- export {
87
- LinuxButton,
88
- MANETTE_TO_W3C_BUTTON,
89
- LinuxButton as ManetteButton,
90
- W3CButton,
91
- W3C_BUTTON_COUNT
92
- };
74
+
75
+ //#endregion
76
+ export { LinuxButton, LinuxButton as ManetteButton, MANETTE_TO_W3C_BUTTON, W3CButton, W3C_BUTTON_COUNT };
@@ -1,16 +1,21 @@
1
- class GamepadButton {
2
- pressed;
3
- touched;
4
- value;
5
- constructor(pressed = false, touched = false, value = 0) {
6
- this.pressed = pressed;
7
- this.touched = touched;
8
- this.value = value;
9
- }
10
- get [Symbol.toStringTag]() {
11
- return "GamepadButton";
12
- }
13
- }
14
- export {
15
- GamepadButton
1
+ //#region src/gamepad-button.ts
2
+ /**
3
+ * Represents the state of a single button on a gamepad.
4
+ * https://w3c.github.io/gamepad/#dom-gamepadbutton
5
+ */
6
+ var GamepadButton = class {
7
+ pressed;
8
+ touched;
9
+ value;
10
+ constructor(pressed = false, touched = false, value = 0) {
11
+ this.pressed = pressed;
12
+ this.touched = touched;
13
+ this.value = value;
14
+ }
15
+ get [Symbol.toStringTag]() {
16
+ return "GamepadButton";
17
+ }
16
18
  };
19
+
20
+ //#endregion
21
+ export { GamepadButton };
@@ -1,14 +1,20 @@
1
1
  import { Event } from "@gjsify/dom-events";
2
- class GamepadEvent extends Event {
3
- gamepad;
4
- constructor(type, eventInitDict) {
5
- super(type, eventInitDict);
6
- this.gamepad = eventInitDict.gamepad;
7
- }
8
- get [Symbol.toStringTag]() {
9
- return "GamepadEvent";
10
- }
11
- }
12
- export {
13
- GamepadEvent
2
+
3
+ //#region src/gamepad-event.ts
4
+ /**
5
+ * Fired on the Window when a gamepad is connected or disconnected.
6
+ * https://w3c.github.io/gamepad/#dom-gamepadevent
7
+ */
8
+ var GamepadEvent = class extends Event {
9
+ gamepad;
10
+ constructor(type, eventInitDict) {
11
+ super(type, eventInitDict);
12
+ this.gamepad = eventInitDict.gamepad;
13
+ }
14
+ get [Symbol.toStringTag]() {
15
+ return "GamepadEvent";
16
+ }
14
17
  };
18
+
19
+ //#endregion
20
+ export { GamepadEvent };
@@ -1,223 +1,218 @@
1
+ import { MANETTE_TO_W3C_AXIS, ManetteAxis, TRIGGER_PRESS_THRESHOLD, W3C_AXIS_COUNT } from "./axis-mapping.js";
2
+ import { MANETTE_TO_W3C_BUTTON, W3CButton, W3C_BUTTON_COUNT } from "./button-mapping.js";
1
3
  import { GamepadButton } from "./gamepad-button.js";
2
- import { Gamepad } from "./gamepad.js";
3
4
  import { GamepadEvent } from "./gamepad-event.js";
5
+ import { Gamepad } from "./gamepad.js";
4
6
  import { ManetteHapticActuator } from "./haptic-actuator.js";
5
- import { MANETTE_TO_W3C_BUTTON, W3C_BUTTON_COUNT } from "./button-mapping.js";
6
- import { MANETTE_TO_W3C_AXIS, ManetteAxis, W3C_AXIS_COUNT, TRIGGER_PRESS_THRESHOLD } from "./axis-mapping.js";
7
- import { W3CButton } from "./button-mapping.js";
7
+
8
+ //#region src/gamepad-manager.ts
8
9
  const MAX_GAMEPADS = 4;
9
- class GamepadManager {
10
- _monitor = null;
11
- _slots = new Array(MAX_GAMEPADS).fill(null);
12
- _monitorSignalIds = [];
13
- _ManetteModule = null;
14
- _initPromise = null;
15
- _initialized = false;
16
- /**
17
- * Lazily initialize the Manette.Monitor.
18
- * Called on first `getGamepads()` invocation.
19
- */
20
- _ensureInit() {
21
- if (this._initialized) return;
22
- if (this._initPromise) return;
23
- this._initPromise = this._init();
24
- }
25
- async _init() {
26
- try {
27
- const mod = await import("gi://Manette");
28
- this._ManetteModule = mod.default;
29
- } catch {
30
- this._initialized = true;
31
- return;
32
- }
33
- const monitor = new this._ManetteModule.Monitor();
34
- this._monitor = monitor;
35
- const iter = monitor.iterate();
36
- let result = iter.next();
37
- while (result[0]) {
38
- const device = result[1];
39
- if (device) {
40
- this._onDeviceConnected(device);
41
- }
42
- result = iter.next();
43
- }
44
- this._monitorSignalIds.push(
45
- monitor.connect("device-connected", (_monitor, device) => {
46
- this._onDeviceConnected(device);
47
- }),
48
- monitor.connect("device-disconnected", (_monitor, device) => {
49
- this._onDeviceDisconnected(device);
50
- })
51
- );
52
- this._initialized = true;
53
- }
54
- _onDeviceConnected(device) {
55
- let slotIndex = -1;
56
- for (let i = 0; i < MAX_GAMEPADS; i++) {
57
- if (this._slots[i] === null) {
58
- slotIndex = i;
59
- break;
60
- }
61
- }
62
- if (slotIndex === -1) return;
63
- const state = {
64
- device,
65
- index: slotIndex,
66
- connected: true,
67
- timestamp: performance.now(),
68
- buttons: new Float64Array(W3C_BUTTON_COUNT),
69
- buttonsPressed: new Array(W3C_BUTTON_COUNT).fill(false),
70
- axes: new Float64Array(W3C_AXIS_COUNT),
71
- hapticActuator: new ManetteHapticActuator(device),
72
- signalIds: []
73
- };
74
- state.signalIds.push(
75
- device.connect("button-press-event", (_device, event) => {
76
- this._onButtonPress(state, event);
77
- }),
78
- device.connect("button-release-event", (_device, event) => {
79
- this._onButtonRelease(state, event);
80
- }),
81
- device.connect("absolute-axis-event", (_device, event) => {
82
- this._onAxisChange(state, event);
83
- }),
84
- device.connect("hat-axis-event", (_device, event) => {
85
- this._onHatChange(state, event);
86
- }),
87
- device.connect("disconnected", () => {
88
- this._onDeviceDisconnected(device);
89
- })
90
- );
91
- this._slots[slotIndex] = state;
92
- const snapshot = this._createSnapshot(state);
93
- if (snapshot) {
94
- globalThis.dispatchEvent?.(new GamepadEvent("gamepadconnected", { gamepad: snapshot }));
95
- }
96
- }
97
- _onDeviceDisconnected(device) {
98
- const state = this._findStateByDevice(device);
99
- if (!state) return;
100
- for (const id of state.signalIds) {
101
- device.disconnect(id);
102
- }
103
- state.connected = false;
104
- const snapshot = this._createSnapshot(state);
105
- this._slots[state.index] = null;
106
- if (snapshot) {
107
- globalThis.dispatchEvent?.(new GamepadEvent("gamepaddisconnected", { gamepad: snapshot }));
108
- }
109
- }
110
- _onButtonPress(state, event) {
111
- const [ok, button] = event.get_button();
112
- if (!ok) return;
113
- const w3cIdx = MANETTE_TO_W3C_BUTTON.get(button);
114
- if (w3cIdx === void 0) return;
115
- state.buttons[w3cIdx] = 1;
116
- state.buttonsPressed[w3cIdx] = true;
117
- state.timestamp = performance.now();
118
- }
119
- _onButtonRelease(state, event) {
120
- const [ok, button] = event.get_button();
121
- if (!ok) return;
122
- const w3cIdx = MANETTE_TO_W3C_BUTTON.get(button);
123
- if (w3cIdx === void 0) return;
124
- state.buttons[w3cIdx] = 0;
125
- state.buttonsPressed[w3cIdx] = false;
126
- state.timestamp = performance.now();
127
- }
128
- _onAxisChange(state, event) {
129
- const [ok, axis, value] = event.get_absolute();
130
- if (!ok) return;
131
- const w3cAxisIdx = MANETTE_TO_W3C_AXIS.get(axis);
132
- if (w3cAxisIdx !== void 0) {
133
- state.axes[w3cAxisIdx] = value;
134
- } else if (axis === ManetteAxis.LEFT_TRIGGER) {
135
- const normalized = (value + 1) / 2;
136
- state.buttons[W3CButton.LEFT_TRIGGER] = normalized;
137
- state.buttonsPressed[W3CButton.LEFT_TRIGGER] = normalized > TRIGGER_PRESS_THRESHOLD;
138
- } else if (axis === ManetteAxis.RIGHT_TRIGGER) {
139
- const normalized = (value + 1) / 2;
140
- state.buttons[W3CButton.RIGHT_TRIGGER] = normalized;
141
- state.buttonsPressed[W3CButton.RIGHT_TRIGGER] = normalized > TRIGGER_PRESS_THRESHOLD;
142
- }
143
- state.timestamp = performance.now();
144
- }
145
- _onHatChange(state, event) {
146
- const [ok, hatAxis, hatValue] = event.get_hat();
147
- if (!ok) return;
148
- if (hatAxis === 0) {
149
- state.buttonsPressed[W3CButton.DPAD_LEFT] = hatValue < 0;
150
- state.buttons[W3CButton.DPAD_LEFT] = hatValue < 0 ? 1 : 0;
151
- state.buttonsPressed[W3CButton.DPAD_RIGHT] = hatValue > 0;
152
- state.buttons[W3CButton.DPAD_RIGHT] = hatValue > 0 ? 1 : 0;
153
- } else if (hatAxis === 1) {
154
- state.buttonsPressed[W3CButton.DPAD_UP] = hatValue < 0;
155
- state.buttons[W3CButton.DPAD_UP] = hatValue < 0 ? 1 : 0;
156
- state.buttonsPressed[W3CButton.DPAD_DOWN] = hatValue > 0;
157
- state.buttons[W3CButton.DPAD_DOWN] = hatValue > 0 ? 1 : 0;
158
- }
159
- state.timestamp = performance.now();
160
- }
161
- _findStateByDevice(device) {
162
- for (const state of this._slots) {
163
- if (state && state.device === device) return state;
164
- }
165
- return null;
166
- }
167
- _createSnapshot(state) {
168
- const buttons = [];
169
- for (let i = 0; i < W3C_BUTTON_COUNT; i++) {
170
- buttons.push(new GamepadButton(
171
- state.buttonsPressed[i],
172
- state.buttonsPressed[i] || state.buttons[i] > 0,
173
- state.buttons[i]
174
- ));
175
- }
176
- return new Gamepad({
177
- id: state.device.get_name() ?? `Gamepad (${state.device.get_guid()})`,
178
- index: state.index,
179
- connected: state.connected,
180
- timestamp: state.timestamp,
181
- mapping: "standard",
182
- axes: Array.from(state.axes),
183
- buttons,
184
- vibrationActuator: state.hapticActuator
185
- });
186
- }
187
- /**
188
- * Returns a snapshot array matching the W3C `navigator.getGamepads()` contract.
189
- * Each non-null entry is a frozen Gamepad object with current state.
190
- */
191
- getGamepads() {
192
- this._ensureInit();
193
- const result = [];
194
- for (let i = 0; i < MAX_GAMEPADS; i++) {
195
- const state = this._slots[i];
196
- result.push(state ? this._createSnapshot(state) : null);
197
- }
198
- return result;
199
- }
200
- /** Cleanup — disconnect all signal handlers. */
201
- dispose() {
202
- for (const state of this._slots) {
203
- if (state) {
204
- for (const id of state.signalIds) {
205
- state.device.disconnect(id);
206
- }
207
- }
208
- }
209
- this._slots.fill(null);
210
- if (this._monitor) {
211
- for (const id of this._monitorSignalIds) {
212
- this._monitor.disconnect(id);
213
- }
214
- this._monitorSignalIds = [];
215
- this._monitor = null;
216
- }
217
- this._initialized = false;
218
- this._initPromise = null;
219
- }
220
- }
221
- export {
222
- GamepadManager
10
+ /**
11
+ * Singleton manager that wraps Manette.Monitor and maintains gamepad state.
12
+ *
13
+ * libmanette fires GObject signals on button/axis changes. This manager
14
+ * caches the latest state so that `getGamepads()` can return a snapshot
15
+ * matching the W3C Gamepad API's polling model.
16
+ */
17
+ var GamepadManager = class {
18
+ _monitor = null;
19
+ _slots = new Array(MAX_GAMEPADS).fill(null);
20
+ _monitorSignalIds = [];
21
+ _ManetteModule = null;
22
+ _initPromise = null;
23
+ _initialized = false;
24
+ /**
25
+ * Lazily initialize the Manette.Monitor.
26
+ * Called on first `getGamepads()` invocation.
27
+ */
28
+ _ensureInit() {
29
+ if (this._initialized) return;
30
+ if (this._initPromise) return;
31
+ this._initPromise = this._init();
32
+ }
33
+ async _init() {
34
+ try {
35
+ const mod = await import("gi://Manette");
36
+ this._ManetteModule = mod.default;
37
+ } catch {
38
+ this._initialized = true;
39
+ return;
40
+ }
41
+ const monitor = new this._ManetteModule.Monitor();
42
+ this._monitor = monitor;
43
+ const iter = monitor.iterate();
44
+ let result = iter.next();
45
+ while (result[0]) {
46
+ const device = result[1];
47
+ if (device) {
48
+ this._onDeviceConnected(device);
49
+ }
50
+ result = iter.next();
51
+ }
52
+ this._monitorSignalIds.push(monitor.connect("device-connected", (_monitor, device) => {
53
+ this._onDeviceConnected(device);
54
+ }), monitor.connect("device-disconnected", (_monitor, device) => {
55
+ this._onDeviceDisconnected(device);
56
+ }));
57
+ this._initialized = true;
58
+ }
59
+ _onDeviceConnected(device) {
60
+ let slotIndex = -1;
61
+ for (let i = 0; i < MAX_GAMEPADS; i++) {
62
+ if (this._slots[i] === null) {
63
+ slotIndex = i;
64
+ break;
65
+ }
66
+ }
67
+ if (slotIndex === -1) return;
68
+ const state = {
69
+ device,
70
+ index: slotIndex,
71
+ connected: true,
72
+ timestamp: performance.now(),
73
+ buttons: new Float64Array(17),
74
+ buttonsPressed: new Array(17).fill(false),
75
+ axes: new Float64Array(4),
76
+ hapticActuator: new ManetteHapticActuator(device),
77
+ signalIds: []
78
+ };
79
+ state.signalIds.push(device.connect("button-press-event", (_device, event) => {
80
+ this._onButtonPress(state, event);
81
+ }), device.connect("button-release-event", (_device, event) => {
82
+ this._onButtonRelease(state, event);
83
+ }), device.connect("absolute-axis-event", (_device, event) => {
84
+ this._onAxisChange(state, event);
85
+ }), device.connect("hat-axis-event", (_device, event) => {
86
+ this._onHatChange(state, event);
87
+ }), device.connect("disconnected", () => {
88
+ this._onDeviceDisconnected(device);
89
+ }));
90
+ this._slots[slotIndex] = state;
91
+ const snapshot = this._createSnapshot(state);
92
+ if (snapshot) {
93
+ globalThis.dispatchEvent?.(new GamepadEvent("gamepadconnected", { gamepad: snapshot }));
94
+ }
95
+ }
96
+ _onDeviceDisconnected(device) {
97
+ const state = this._findStateByDevice(device);
98
+ if (!state) return;
99
+ for (const id of state.signalIds) {
100
+ device.disconnect(id);
101
+ }
102
+ state.connected = false;
103
+ const snapshot = this._createSnapshot(state);
104
+ this._slots[state.index] = null;
105
+ if (snapshot) {
106
+ globalThis.dispatchEvent?.(new GamepadEvent("gamepaddisconnected", { gamepad: snapshot }));
107
+ }
108
+ }
109
+ _onButtonPress(state, event) {
110
+ const [ok, button] = event.get_button();
111
+ if (!ok) return;
112
+ const w3cIdx = MANETTE_TO_W3C_BUTTON.get(button);
113
+ if (w3cIdx === undefined) return;
114
+ state.buttons[w3cIdx] = 1;
115
+ state.buttonsPressed[w3cIdx] = true;
116
+ state.timestamp = performance.now();
117
+ }
118
+ _onButtonRelease(state, event) {
119
+ const [ok, button] = event.get_button();
120
+ if (!ok) return;
121
+ const w3cIdx = MANETTE_TO_W3C_BUTTON.get(button);
122
+ if (w3cIdx === undefined) return;
123
+ state.buttons[w3cIdx] = 0;
124
+ state.buttonsPressed[w3cIdx] = false;
125
+ state.timestamp = performance.now();
126
+ }
127
+ _onAxisChange(state, event) {
128
+ const [ok, axis, value] = event.get_absolute();
129
+ if (!ok) return;
130
+ const w3cAxisIdx = MANETTE_TO_W3C_AXIS.get(axis);
131
+ if (w3cAxisIdx !== undefined) {
132
+ state.axes[w3cAxisIdx] = value;
133
+ } else if (axis === ManetteAxis.LEFT_TRIGGER) {
134
+ const normalized = (value + 1) / 2;
135
+ state.buttons[W3CButton.LEFT_TRIGGER] = normalized;
136
+ state.buttonsPressed[W3CButton.LEFT_TRIGGER] = normalized > TRIGGER_PRESS_THRESHOLD;
137
+ } else if (axis === ManetteAxis.RIGHT_TRIGGER) {
138
+ const normalized = (value + 1) / 2;
139
+ state.buttons[W3CButton.RIGHT_TRIGGER] = normalized;
140
+ state.buttonsPressed[W3CButton.RIGHT_TRIGGER] = normalized > TRIGGER_PRESS_THRESHOLD;
141
+ }
142
+ state.timestamp = performance.now();
143
+ }
144
+ _onHatChange(state, event) {
145
+ const [ok, hatAxis, hatValue] = event.get_hat();
146
+ if (!ok) return;
147
+ if (hatAxis === 0) {
148
+ state.buttonsPressed[W3CButton.DPAD_LEFT] = hatValue < 0;
149
+ state.buttons[W3CButton.DPAD_LEFT] = hatValue < 0 ? 1 : 0;
150
+ state.buttonsPressed[W3CButton.DPAD_RIGHT] = hatValue > 0;
151
+ state.buttons[W3CButton.DPAD_RIGHT] = hatValue > 0 ? 1 : 0;
152
+ } else if (hatAxis === 1) {
153
+ state.buttonsPressed[W3CButton.DPAD_UP] = hatValue < 0;
154
+ state.buttons[W3CButton.DPAD_UP] = hatValue < 0 ? 1 : 0;
155
+ state.buttonsPressed[W3CButton.DPAD_DOWN] = hatValue > 0;
156
+ state.buttons[W3CButton.DPAD_DOWN] = hatValue > 0 ? 1 : 0;
157
+ }
158
+ state.timestamp = performance.now();
159
+ }
160
+ _findStateByDevice(device) {
161
+ for (const state of this._slots) {
162
+ if (state && state.device === device) return state;
163
+ }
164
+ return null;
165
+ }
166
+ _createSnapshot(state) {
167
+ const buttons = [];
168
+ for (let i = 0; i < 17; i++) {
169
+ buttons.push(new GamepadButton(state.buttonsPressed[i], state.buttonsPressed[i] || state.buttons[i] > 0, state.buttons[i]));
170
+ }
171
+ return new Gamepad({
172
+ id: state.device.get_name() ?? `Gamepad (${state.device.get_guid()})`,
173
+ index: state.index,
174
+ connected: state.connected,
175
+ timestamp: state.timestamp,
176
+ mapping: "standard",
177
+ axes: Array.from(state.axes),
178
+ buttons,
179
+ vibrationActuator: state.hapticActuator
180
+ });
181
+ }
182
+ /**
183
+ * Returns a snapshot array matching the W3C `navigator.getGamepads()` contract.
184
+ * Each non-null entry is a frozen Gamepad object with current state.
185
+ */
186
+ getGamepads() {
187
+ this._ensureInit();
188
+ const result = [];
189
+ for (let i = 0; i < MAX_GAMEPADS; i++) {
190
+ const state = this._slots[i];
191
+ result.push(state ? this._createSnapshot(state) : null);
192
+ }
193
+ return result;
194
+ }
195
+ /** Cleanup disconnect all signal handlers. */
196
+ dispose() {
197
+ for (const state of this._slots) {
198
+ if (state) {
199
+ for (const id of state.signalIds) {
200
+ state.device.disconnect(id);
201
+ }
202
+ }
203
+ }
204
+ this._slots.fill(null);
205
+ if (this._monitor) {
206
+ for (const id of this._monitorSignalIds) {
207
+ this._monitor.disconnect(id);
208
+ }
209
+ this._monitorSignalIds = [];
210
+ this._monitor = null;
211
+ }
212
+ this._initialized = false;
213
+ this._initPromise = null;
214
+ }
223
215
  };
216
+
217
+ //#endregion
218
+ export { GamepadManager };
@@ -1,26 +1,32 @@
1
- class Gamepad {
2
- id;
3
- index;
4
- connected;
5
- timestamp;
6
- mapping;
7
- axes;
8
- buttons;
9
- vibrationActuator;
10
- constructor(init) {
11
- this.id = init.id;
12
- this.index = init.index;
13
- this.connected = init.connected;
14
- this.timestamp = init.timestamp;
15
- this.mapping = init.mapping;
16
- this.axes = Object.freeze([...init.axes]);
17
- this.buttons = Object.freeze(init.buttons);
18
- this.vibrationActuator = init.vibrationActuator ?? null;
19
- }
20
- get [Symbol.toStringTag]() {
21
- return "Gamepad";
22
- }
23
- }
24
- export {
25
- Gamepad
1
+ //#region src/gamepad.ts
2
+ /**
3
+ * Represents a single gamepad device.
4
+ * Instances are snapshots — they are not live-updating.
5
+ * https://w3c.github.io/gamepad/#dom-gamepad
6
+ */
7
+ var Gamepad = class {
8
+ id;
9
+ index;
10
+ connected;
11
+ timestamp;
12
+ mapping;
13
+ axes;
14
+ buttons;
15
+ vibrationActuator;
16
+ constructor(init) {
17
+ this.id = init.id;
18
+ this.index = init.index;
19
+ this.connected = init.connected;
20
+ this.timestamp = init.timestamp;
21
+ this.mapping = init.mapping;
22
+ this.axes = Object.freeze([...init.axes]);
23
+ this.buttons = Object.freeze(init.buttons);
24
+ this.vibrationActuator = init.vibrationActuator ?? null;
25
+ }
26
+ get [Symbol.toStringTag]() {
27
+ return "Gamepad";
28
+ }
26
29
  };
30
+
31
+ //#endregion
32
+ export { Gamepad };
@@ -1,27 +1,35 @@
1
- class ManetteHapticActuator {
2
- effects;
3
- _device;
4
- constructor(device) {
5
- this._device = device;
6
- this.effects = device.has_rumble() ? ["dual-rumble"] : [];
7
- }
8
- playEffect(type, params) {
9
- if (type !== "dual-rumble" || !this._device.has_rumble()) {
10
- return Promise.resolve("complete");
11
- }
12
- const duration = params?.duration ?? 200;
13
- const strong = Math.round((params?.strongMagnitude ?? 1) * 65535);
14
- const weak = Math.round((params?.weakMagnitude ?? 1) * 65535);
15
- this._device.rumble(strong, weak, Math.min(duration, 32767));
16
- return Promise.resolve("complete");
17
- }
18
- reset() {
19
- if (this._device.has_rumble()) {
20
- this._device.rumble(0, 0, 0);
21
- }
22
- return Promise.resolve("complete");
23
- }
24
- }
25
- export {
26
- ManetteHapticActuator
1
+ //#region src/haptic-actuator.ts
2
+ /**
3
+ * Wraps libmanette's rumble support as a W3C GamepadHapticActuator.
4
+ *
5
+ * libmanette supports dual-rumble with strong/weak magnitude control
6
+ * via `Device.rumble(strong_magnitude, weak_magnitude, milliseconds)`.
7
+ * Magnitudes are in the range 0–65535 (uint16).
8
+ */
9
+ var ManetteHapticActuator = class {
10
+ effects;
11
+ _device;
12
+ constructor(device) {
13
+ this._device = device;
14
+ this.effects = device.has_rumble() ? ["dual-rumble"] : [];
15
+ }
16
+ playEffect(type, params) {
17
+ if (type !== "dual-rumble" || !this._device.has_rumble()) {
18
+ return Promise.resolve("complete");
19
+ }
20
+ const duration = params?.duration ?? 200;
21
+ const strong = Math.round((params?.strongMagnitude ?? 1) * 65535);
22
+ const weak = Math.round((params?.weakMagnitude ?? 1) * 65535);
23
+ this._device.rumble(strong, weak, Math.min(duration, 32767));
24
+ return Promise.resolve("complete");
25
+ }
26
+ reset() {
27
+ if (this._device.has_rumble()) {
28
+ this._device.rumble(0, 0, 0);
29
+ }
30
+ return Promise.resolve("complete");
31
+ }
27
32
  };
33
+
34
+ //#endregion
35
+ export { ManetteHapticActuator };
package/lib/esm/index.js CHANGED
@@ -1,23 +1,9 @@
1
+ import { MANETTE_TO_W3C_AXIS, ManetteAxis, TRIGGER_PRESS_THRESHOLD, W3CAxis, W3C_AXIS_COUNT } from "./axis-mapping.js";
2
+ import { LinuxButton, MANETTE_TO_W3C_BUTTON, W3CButton, W3C_BUTTON_COUNT } from "./button-mapping.js";
1
3
  import { GamepadButton } from "./gamepad-button.js";
2
- import { Gamepad } from "./gamepad.js";
3
4
  import { GamepadEvent } from "./gamepad-event.js";
4
- import { GamepadManager } from "./gamepad-manager.js";
5
+ import { Gamepad } from "./gamepad.js";
5
6
  import { ManetteHapticActuator } from "./haptic-actuator.js";
6
- import { MANETTE_TO_W3C_BUTTON, ManetteButton, W3CButton, W3C_BUTTON_COUNT } from "./button-mapping.js";
7
- import { MANETTE_TO_W3C_AXIS, ManetteAxis, W3CAxis, W3C_AXIS_COUNT, TRIGGER_PRESS_THRESHOLD } from "./axis-mapping.js";
8
- export {
9
- Gamepad,
10
- GamepadButton,
11
- GamepadEvent,
12
- GamepadManager,
13
- MANETTE_TO_W3C_AXIS,
14
- MANETTE_TO_W3C_BUTTON,
15
- ManetteAxis,
16
- ManetteButton,
17
- ManetteHapticActuator,
18
- TRIGGER_PRESS_THRESHOLD,
19
- W3CAxis,
20
- W3CButton,
21
- W3C_AXIS_COUNT,
22
- W3C_BUTTON_COUNT
23
- };
7
+ import { GamepadManager } from "./gamepad-manager.js";
8
+
9
+ export { Gamepad, GamepadButton, GamepadEvent, GamepadManager, MANETTE_TO_W3C_AXIS, MANETTE_TO_W3C_BUTTON, ManetteAxis, LinuxButton as ManetteButton, ManetteHapticActuator, TRIGGER_PRESS_THRESHOLD, W3CAxis, W3CButton, W3C_AXIS_COUNT, W3C_BUTTON_COUNT };
@@ -1,10 +1,14 @@
1
1
  import { GamepadEvent } from "./gamepad-event.js";
2
2
  import { GamepadManager } from "./gamepad-manager.js";
3
+
4
+ //#region src/register.ts
3
5
  const manager = new GamepadManager();
4
6
  if (typeof globalThis.navigator === "undefined") {
5
- globalThis.navigator = {};
7
+ globalThis.navigator = {};
6
8
  }
7
9
  globalThis.navigator.getGamepads = () => manager.getGamepads();
8
10
  if (typeof globalThis.GamepadEvent === "undefined") {
9
- globalThis.GamepadEvent = GamepadEvent;
11
+ globalThis.GamepadEvent = GamepadEvent;
10
12
  }
13
+
14
+ //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/gamepad",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "description": "Gamepad Web API for GJS using libmanette as backend",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -36,14 +36,14 @@
36
36
  "controller"
37
37
  ],
38
38
  "dependencies": {
39
- "@gjsify/dom-events": "^0.3.12"
39
+ "@gjsify/dom-events": "^0.3.14"
40
40
  },
41
41
  "devDependencies": {
42
- "@girs/gjs": "^4.0.0-rc.9",
43
- "@girs/glib-2.0": "^2.88.0-4.0.0-rc.9",
44
- "@girs/manette-0.2": "^0.2.13-4.0.0-rc.9",
45
- "@gjsify/cli": "^0.3.12",
46
- "@gjsify/unit": "^0.3.12",
42
+ "@girs/gjs": "4.0.0-rc.9",
43
+ "@girs/glib-2.0": "2.88.0-4.0.0-rc.9",
44
+ "@girs/manette-0.2": "0.2.13-4.0.0-rc.9",
45
+ "@gjsify/cli": "^0.3.14",
46
+ "@gjsify/unit": "^0.3.14",
47
47
  "@types/node": "^25.6.0",
48
48
  "typescript": "^6.0.3"
49
49
  }