@easywasm/gl 0.0.2 → 0.0.3
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.
- package/package.json +4 -2
- package/web/gl.js +1060 -0
- package/web/glfw.js +931 -0
- package/src/example.c +0 -41
- package/src/gl.h +0 -569
- package/src/glfw3.h +0 -6577
- package/src/platform.h +0 -28
package/web/glfw.js
ADDED
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
// Web host implementation of the GLFW 3.5 API for WebAssembly modules compiled
|
|
2
|
+
// with wasi-sdk. wasi-sdk puts undefined symbols in the "env" import namespace
|
|
3
|
+
// by default, so pass the Glfw instance as the env import:
|
|
4
|
+
//
|
|
5
|
+
// const glfw = new Glfw({ canvas })
|
|
6
|
+
// const wasi = new WasiPreview1()
|
|
7
|
+
// const { instance } = await WebAssembly.instantiateStreaming(fetch('app.wasm'), {
|
|
8
|
+
// env: glfw,
|
|
9
|
+
// wasi_snapshot_preview1: wasi,
|
|
10
|
+
// })
|
|
11
|
+
// glfw.setInstance(instance)
|
|
12
|
+
//
|
|
13
|
+
// If _start runs during instantiation (standard C main) and any GLFW function
|
|
14
|
+
// called at startup needs memory access, pre-create a WebAssembly.Memory and
|
|
15
|
+
// pass it to both Glfw({ memory }) and env: { ...glfw, memory }:
|
|
16
|
+
//
|
|
17
|
+
// const memory = new WebAssembly.Memory({ initial: 256 })
|
|
18
|
+
// const glfw = new Glfw({ canvas, memory })
|
|
19
|
+
// ...{ env: { ...glfw, memory }, ... }
|
|
20
|
+
//
|
|
21
|
+
// Blocking while(!glfwWindowShouldClose) loops need asyncify or JSPI.
|
|
22
|
+
// Alternative: export a frame() function and drive it from requestAnimationFrame.
|
|
23
|
+
|
|
24
|
+
const enc = new TextEncoder()
|
|
25
|
+
const dec = new TextDecoder()
|
|
26
|
+
|
|
27
|
+
// DOM event.code → GLFW key constant (US layout)
|
|
28
|
+
const KEY_MAP = {
|
|
29
|
+
Space: 32, Quote: 39, Comma: 44, Minus: 45, Period: 46, Slash: 47,
|
|
30
|
+
Digit0: 48, Digit1: 49, Digit2: 50, Digit3: 51, Digit4: 52,
|
|
31
|
+
Digit5: 53, Digit6: 54, Digit7: 55, Digit8: 56, Digit9: 57,
|
|
32
|
+
Semicolon: 59, Equal: 61,
|
|
33
|
+
KeyA: 65, KeyB: 66, KeyC: 67, KeyD: 68, KeyE: 69, KeyF: 70,
|
|
34
|
+
KeyG: 71, KeyH: 72, KeyI: 73, KeyJ: 74, KeyK: 75, KeyL: 76,
|
|
35
|
+
KeyM: 77, KeyN: 78, KeyO: 79, KeyP: 80, KeyQ: 81, KeyR: 82,
|
|
36
|
+
KeyS: 83, KeyT: 84, KeyU: 85, KeyV: 86, KeyW: 87, KeyX: 88,
|
|
37
|
+
KeyY: 89, KeyZ: 90,
|
|
38
|
+
BracketLeft: 91, Backslash: 92, BracketRight: 93, Backquote: 96,
|
|
39
|
+
Escape: 256, Enter: 257, Tab: 258, Backspace: 259,
|
|
40
|
+
Insert: 260, Delete: 261,
|
|
41
|
+
ArrowRight: 262, ArrowLeft: 263, ArrowDown: 264, ArrowUp: 265,
|
|
42
|
+
PageUp: 266, PageDown: 267, Home: 268, End: 269,
|
|
43
|
+
CapsLock: 280, ScrollLock: 281, NumLock: 282, PrintScreen: 283, Pause: 284,
|
|
44
|
+
F1: 290, F2: 291, F3: 292, F4: 293, F5: 294, F6: 295, F7: 296,
|
|
45
|
+
F8: 297, F9: 298, F10: 299, F11: 300, F12: 301, F13: 302, F14: 303,
|
|
46
|
+
F15: 304, F16: 305, F17: 306, F18: 307, F19: 308, F20: 309, F21: 310,
|
|
47
|
+
F22: 311, F23: 312, F24: 313, F25: 314,
|
|
48
|
+
Numpad0: 320, Numpad1: 321, Numpad2: 322, Numpad3: 323, Numpad4: 324,
|
|
49
|
+
Numpad5: 325, Numpad6: 326, Numpad7: 327, Numpad8: 328, Numpad9: 329,
|
|
50
|
+
NumpadDecimal: 330, NumpadDivide: 331, NumpadMultiply: 332,
|
|
51
|
+
NumpadSubtract: 333, NumpadAdd: 334, NumpadEnter: 335, NumpadEqual: 336,
|
|
52
|
+
ShiftLeft: 340, ControlLeft: 341, AltLeft: 342, MetaLeft: 343,
|
|
53
|
+
ShiftRight: 344, ControlRight: 345, AltRight: 346, MetaRight: 347,
|
|
54
|
+
ContextMenu: 348,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// GLFW key → printable name used by glfwGetKeyName
|
|
58
|
+
const KEY_NAMES = {
|
|
59
|
+
32: 'SPACE', 39: "'", 44: ',', 45: '-', 46: '.', 47: '/',
|
|
60
|
+
48: '0', 49: '1', 50: '2', 51: '3', 52: '4',
|
|
61
|
+
53: '5', 54: '6', 55: '7', 56: '8', 57: '9',
|
|
62
|
+
59: ';', 61: '=',
|
|
63
|
+
65: 'A', 66: 'B', 67: 'C', 68: 'D', 69: 'E', 70: 'F',
|
|
64
|
+
71: 'G', 72: 'H', 73: 'I', 74: 'J', 75: 'K', 76: 'L',
|
|
65
|
+
77: 'M', 78: 'N', 79: 'O', 80: 'P', 81: 'Q', 82: 'R',
|
|
66
|
+
83: 'S', 84: 'T', 85: 'U', 86: 'V', 87: 'W', 88: 'X',
|
|
67
|
+
89: 'Y', 90: 'Z',
|
|
68
|
+
91: '[', 92: '\\', 93: ']', 96: '`',
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// DOM MouseEvent.button → GLFW mouse button (left=0, right=1, middle=2)
|
|
72
|
+
const BTN_MAP = [0, 2, 1]
|
|
73
|
+
|
|
74
|
+
function getMods(e) {
|
|
75
|
+
return (e.shiftKey ? 0x0001 : 0) |
|
|
76
|
+
(e.ctrlKey ? 0x0002 : 0) |
|
|
77
|
+
(e.altKey ? 0x0004 : 0) |
|
|
78
|
+
(e.metaKey ? 0x0008 : 0) |
|
|
79
|
+
(e.getModifierState?.('CapsLock') ? 0x0010 : 0) |
|
|
80
|
+
(e.getModifierState?.('NumLock') ? 0x0020 : 0)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Standard cursor shape → CSS cursor value
|
|
84
|
+
const CURSOR_CSS = {
|
|
85
|
+
0x00036001: 'default',
|
|
86
|
+
0x00036002: 'text',
|
|
87
|
+
0x00036003: 'crosshair',
|
|
88
|
+
0x00036004: 'pointer',
|
|
89
|
+
0x00036005: 'ew-resize',
|
|
90
|
+
0x00036006: 'ns-resize',
|
|
91
|
+
0x00036007: 'nwse-resize',
|
|
92
|
+
0x00036008: 'nesw-resize',
|
|
93
|
+
0x00036009: 'move',
|
|
94
|
+
0x0003600A: 'not-allowed',
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class Glfw {
|
|
98
|
+
constructor({ memory, canvas } = {}) {
|
|
99
|
+
this._memory = memory || null
|
|
100
|
+
this._canvas = canvas || null
|
|
101
|
+
this._instance = null
|
|
102
|
+
|
|
103
|
+
this._windows = new Map() // handle (i32) → WindowState
|
|
104
|
+
this._monitors = new Map() // handle (i32) → MonitorState
|
|
105
|
+
this._cursors = new Map() // handle (i32) → { css }
|
|
106
|
+
this._nextHandle = 1
|
|
107
|
+
|
|
108
|
+
this._currentContext = 0
|
|
109
|
+
this._hints = {}
|
|
110
|
+
|
|
111
|
+
this._errorCb = 0
|
|
112
|
+
this._monitorCb = 0
|
|
113
|
+
this._joystickCb = 0
|
|
114
|
+
|
|
115
|
+
this._timeBase = performance.now() / 1000
|
|
116
|
+
this._timeOffset = 0
|
|
117
|
+
|
|
118
|
+
// malloc'd string ptrs keyed by string value, never freed (static lifetime)
|
|
119
|
+
this._strCache = new Map()
|
|
120
|
+
|
|
121
|
+
// malloc'd struct ptrs for vidmode etc.
|
|
122
|
+
this._vidmodePtr = 0
|
|
123
|
+
this._monitorsPtr = 0
|
|
124
|
+
|
|
125
|
+
// Bind all methods so wasm can call them as plain function references
|
|
126
|
+
const proto = Object.getPrototypeOf(this)
|
|
127
|
+
for (const key of Object.getOwnPropertyNames(proto)) {
|
|
128
|
+
if (key === 'constructor') continue
|
|
129
|
+
const desc = Object.getOwnPropertyDescriptor(proto, key)
|
|
130
|
+
if (typeof desc.value === 'function') this[key] = desc.value.bind(this)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Returns the WebGL2 context for a window handle
|
|
135
|
+
getGL(winId) {
|
|
136
|
+
return this._windows.get(winId)?.gl ?? null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Returns the WebGL2 context for whichever window is current context
|
|
140
|
+
getContextGL() {
|
|
141
|
+
return this._windows.get(this._currentContext)?.gl ?? null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Call after instantiation with the wasm exports object (mirrors WasiPreview1.start).
|
|
145
|
+
start(exports) {
|
|
146
|
+
this._instance = { exports }
|
|
147
|
+
if (!this._memory && exports.memory) this._memory = exports.memory
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Legacy: call with the full WebAssembly.Instance object.
|
|
151
|
+
setInstance(instance) {
|
|
152
|
+
this._instance = instance
|
|
153
|
+
if (!this._memory && instance.exports.memory) {
|
|
154
|
+
this._memory = instance.exports.memory
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Memory helpers ──────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
get _view() { return new DataView(this._memory.buffer) }
|
|
161
|
+
|
|
162
|
+
_readStr(ptr) {
|
|
163
|
+
if (!ptr) return ''
|
|
164
|
+
const buf = new Uint8Array(this._memory.buffer)
|
|
165
|
+
let end = ptr
|
|
166
|
+
while (buf[end]) end++
|
|
167
|
+
return dec.decode(buf.subarray(ptr, end))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
_wi32(ptr, v) { if (ptr) this._view.setInt32(ptr, v | 0, true) }
|
|
171
|
+
_wf32(ptr, v) { if (ptr) this._view.setFloat32(ptr, v, true) }
|
|
172
|
+
_wf64(ptr, v) { if (ptr) this._view.setFloat64(ptr, v, true) }
|
|
173
|
+
|
|
174
|
+
_malloc(size) {
|
|
175
|
+
const fn = this._instance?.exports?.malloc
|
|
176
|
+
if (!fn) {
|
|
177
|
+
console.warn('[glfw] malloc not exported — add -Wl,--export=malloc to link flags')
|
|
178
|
+
return 0
|
|
179
|
+
}
|
|
180
|
+
return fn(size)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_allocStr(str) {
|
|
184
|
+
if (!str) return 0
|
|
185
|
+
if (this._strCache.has(str)) return this._strCache.get(str)
|
|
186
|
+
const bytes = enc.encode(str + '\0')
|
|
187
|
+
const ptr = this._malloc(bytes.length)
|
|
188
|
+
if (!ptr) return 0
|
|
189
|
+
new Uint8Array(this._memory.buffer, ptr, bytes.length).set(bytes)
|
|
190
|
+
this._strCache.set(str, ptr)
|
|
191
|
+
return ptr
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Call a wasm function pointer via the indirect function table
|
|
195
|
+
_callFn(fnPtr, ...args) {
|
|
196
|
+
if (!fnPtr) return
|
|
197
|
+
this._instance?.exports?.__indirect_function_table?.get(fnPtr)?.(...args)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Init ────────────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
glfwInit() {
|
|
203
|
+
const id = this._nextHandle++
|
|
204
|
+
this._monitors.set(id, {
|
|
205
|
+
id,
|
|
206
|
+
name: 'Web Display',
|
|
207
|
+
x: 0, y: 0,
|
|
208
|
+
width: screen.width,
|
|
209
|
+
height: screen.height,
|
|
210
|
+
workX: 0, workY: 0,
|
|
211
|
+
workWidth: screen.availWidth,
|
|
212
|
+
workHeight: screen.availHeight,
|
|
213
|
+
widthMM: Math.round(screen.width / 96 * 25.4),
|
|
214
|
+
heightMM: Math.round(screen.height / 96 * 25.4),
|
|
215
|
+
userPointer: 0,
|
|
216
|
+
})
|
|
217
|
+
this._primaryMonitor = id
|
|
218
|
+
return 1 // GLFW_TRUE
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
glfwTerminate() {
|
|
222
|
+
for (const [id] of this._windows) this.glfwDestroyWindow(id)
|
|
223
|
+
this._windows.clear()
|
|
224
|
+
this._monitors.clear()
|
|
225
|
+
this._cursors.clear()
|
|
226
|
+
this._currentContext = 0
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
glfwInitHint() {}
|
|
230
|
+
glfwInitAllocator() {}
|
|
231
|
+
glfwInitVulkanLoader() {}
|
|
232
|
+
|
|
233
|
+
glfwGetVersion(majPtr, minPtr, revPtr) {
|
|
234
|
+
this._wi32(majPtr, 3)
|
|
235
|
+
this._wi32(minPtr, 5)
|
|
236
|
+
this._wi32(revPtr, 0)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
glfwGetVersionString() { return this._allocStr('3.5.0 WASM') }
|
|
240
|
+
|
|
241
|
+
glfwGetError(descPtrPtr) {
|
|
242
|
+
this._wi32(descPtrPtr, 0)
|
|
243
|
+
return 0 // GLFW_NO_ERROR
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
glfwSetErrorCallback(cb) {
|
|
247
|
+
const prev = this._errorCb
|
|
248
|
+
this._errorCb = cb
|
|
249
|
+
return prev
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
glfwGetPlatform() { return 0x00060005 } // GLFW_PLATFORM_NULL
|
|
253
|
+
glfwPlatformSupported(p) { return p === 0x00060005 ? 1 : 0 }
|
|
254
|
+
|
|
255
|
+
// ── Monitors ────────────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
glfwGetMonitors(countPtr) {
|
|
258
|
+
const count = this._monitors.size
|
|
259
|
+
this._wi32(countPtr, count)
|
|
260
|
+
if (!count) return 0
|
|
261
|
+
// Allocate / refresh array of i32 handles
|
|
262
|
+
if (!this._monitorsPtr) this._monitorsPtr = this._malloc(count * 4)
|
|
263
|
+
if (!this._monitorsPtr) return 0
|
|
264
|
+
let i = 0
|
|
265
|
+
for (const id of this._monitors.keys()) {
|
|
266
|
+
this._view.setInt32(this._monitorsPtr + i * 4, id, true)
|
|
267
|
+
i++
|
|
268
|
+
}
|
|
269
|
+
return this._monitorsPtr
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
glfwGetPrimaryMonitor() { return this._primaryMonitor || 0 }
|
|
273
|
+
|
|
274
|
+
glfwGetMonitorPos(monId, xPtr, yPtr) {
|
|
275
|
+
const m = this._monitors.get(monId)
|
|
276
|
+
if (!m) return
|
|
277
|
+
this._wi32(xPtr, m.x)
|
|
278
|
+
this._wi32(yPtr, m.y)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
glfwGetMonitorWorkarea(monId, xPtr, yPtr, wPtr, hPtr) {
|
|
282
|
+
const m = this._monitors.get(monId)
|
|
283
|
+
if (!m) return
|
|
284
|
+
this._wi32(xPtr, m.workX); this._wi32(yPtr, m.workY)
|
|
285
|
+
this._wi32(wPtr, m.workWidth); this._wi32(hPtr, m.workHeight)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
glfwGetMonitorPhysicalSize(monId, wmmPtr, hmmPtr) {
|
|
289
|
+
const m = this._monitors.get(monId)
|
|
290
|
+
if (!m) return
|
|
291
|
+
this._wi32(wmmPtr, m.widthMM)
|
|
292
|
+
this._wi32(hmmPtr, m.heightMM)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
glfwGetMonitorContentScale(monId, xsPtr, ysPtr) {
|
|
296
|
+
const dpr = window.devicePixelRatio || 1
|
|
297
|
+
this._wf32(xsPtr, dpr)
|
|
298
|
+
this._wf32(ysPtr, dpr)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
glfwGetMonitorName(monId) {
|
|
302
|
+
return this._allocStr(this._monitors.get(monId)?.name ?? '')
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
glfwSetMonitorUserPointer(monId, ptr) {
|
|
306
|
+
const m = this._monitors.get(monId)
|
|
307
|
+
if (m) m.userPointer = ptr
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
glfwGetMonitorUserPointer(monId) {
|
|
311
|
+
return this._monitors.get(monId)?.userPointer ?? 0
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
glfwSetMonitorCallback(cb) {
|
|
315
|
+
const prev = this._monitorCb
|
|
316
|
+
this._monitorCb = cb
|
|
317
|
+
return prev
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
glfwGetVideoModes(monId, countPtr) {
|
|
321
|
+
this._wi32(countPtr, 1)
|
|
322
|
+
return this.glfwGetVideoMode(monId)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
glfwGetVideoMode(monId) {
|
|
326
|
+
// GLFWvidmode: int width, height, redBits, greenBits, blueBits, refreshRate (24 bytes)
|
|
327
|
+
if (!this._vidmodePtr) {
|
|
328
|
+
this._vidmodePtr = this._malloc(24)
|
|
329
|
+
if (this._vidmodePtr) {
|
|
330
|
+
const v = this._view
|
|
331
|
+
v.setInt32(this._vidmodePtr + 0, screen.width, true)
|
|
332
|
+
v.setInt32(this._vidmodePtr + 4, screen.height, true)
|
|
333
|
+
v.setInt32(this._vidmodePtr + 8, 8, true)
|
|
334
|
+
v.setInt32(this._vidmodePtr + 12, 8, true)
|
|
335
|
+
v.setInt32(this._vidmodePtr + 16, 8, true)
|
|
336
|
+
v.setInt32(this._vidmodePtr + 20, 60, true)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return this._vidmodePtr
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
glfwSetGamma() {}
|
|
343
|
+
glfwGetGammaRamp() { return 0 }
|
|
344
|
+
glfwSetGammaRamp() {}
|
|
345
|
+
|
|
346
|
+
// ── Window hints ─────────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
glfwDefaultWindowHints() { this._hints = {} }
|
|
349
|
+
glfwWindowHint(hint, value) { this._hints[hint] = value }
|
|
350
|
+
glfwWindowHintString(hint, valuePtr) { this._hints[hint] = this._readStr(valuePtr) }
|
|
351
|
+
|
|
352
|
+
// ── Windows ──────────────────────────────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
glfwCreateWindow(width, height, titlePtr, monitorPtr, sharePtr) {
|
|
355
|
+
const title = this._readStr(titlePtr)
|
|
356
|
+
|
|
357
|
+
let canvas = this._canvas
|
|
358
|
+
if (!canvas) {
|
|
359
|
+
canvas = document.createElement('canvas')
|
|
360
|
+
canvas.style.display = 'block'
|
|
361
|
+
document.body.appendChild(canvas)
|
|
362
|
+
}
|
|
363
|
+
canvas.width = width
|
|
364
|
+
canvas.height = height
|
|
365
|
+
canvas.title = title
|
|
366
|
+
|
|
367
|
+
const noApi = this._hints[0x00022001] === 0 // GLFW_CLIENT_API = GLFW_NO_API
|
|
368
|
+
const gl = noApi ? null : canvas.getContext('webgl2', {
|
|
369
|
+
antialias: (this._hints[0x0002100D] || 0) > 0, // GLFW_SAMPLES
|
|
370
|
+
depth: (this._hints[0x00021005] ?? 24) > 0, // GLFW_DEPTH_BITS
|
|
371
|
+
stencil: (this._hints[0x00021006] || 0) > 0, // GLFW_STENCIL_BITS
|
|
372
|
+
alpha: true,
|
|
373
|
+
premultipliedAlpha: false,
|
|
374
|
+
powerPreference: 'high-performance',
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
if (gl === null && !noApi) {
|
|
378
|
+
console.error('[glfw] WebGL2 context creation failed')
|
|
379
|
+
return 0
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const id = this._nextHandle++
|
|
383
|
+
const win = {
|
|
384
|
+
id, canvas, gl, width, height, x: 0, y: 0, title,
|
|
385
|
+
shouldClose: false, userPointer: 0,
|
|
386
|
+
visible: this._hints[0x00020004] !== 0, // GLFW_VISIBLE
|
|
387
|
+
focused: true, iconified: false, maximized: false,
|
|
388
|
+
cb: {},
|
|
389
|
+
keys: new Map(), // GLFW key → 0/1/2 (release/press/repeat)
|
|
390
|
+
buttons: new Map(), // GLFW button → 0/1
|
|
391
|
+
cx: 0, cy: 0,
|
|
392
|
+
inputMode: {
|
|
393
|
+
cursor: 0x00034001, // GLFW_CURSOR_NORMAL
|
|
394
|
+
stickyKeys: 0, stickyMouseButtons: 0,
|
|
395
|
+
lockKeyMods: 0, rawMouseMotion: 0,
|
|
396
|
+
},
|
|
397
|
+
cleanups: [],
|
|
398
|
+
}
|
|
399
|
+
this._windows.set(id, win)
|
|
400
|
+
this._attachEvents(win)
|
|
401
|
+
return id
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
glfwDestroyWindow(winId) {
|
|
405
|
+
const win = this._windows.get(winId)
|
|
406
|
+
if (!win) return
|
|
407
|
+
for (const fn of win.cleanups) fn()
|
|
408
|
+
this._windows.delete(winId)
|
|
409
|
+
if (this._currentContext === winId) this._currentContext = 0
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
glfwWindowShouldClose(winId) {
|
|
413
|
+
return this._windows.get(winId)?.shouldClose ? 1 : 0
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
glfwSetWindowShouldClose(winId, value) {
|
|
417
|
+
const win = this._windows.get(winId)
|
|
418
|
+
if (win) win.shouldClose = value !== 0
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
glfwGetWindowTitle(winId) {
|
|
422
|
+
return this._allocStr(this._windows.get(winId)?.title ?? '')
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
glfwSetWindowTitle(winId, titlePtr) {
|
|
426
|
+
const win = this._windows.get(winId)
|
|
427
|
+
if (!win) return
|
|
428
|
+
win.title = this._readStr(titlePtr)
|
|
429
|
+
win.canvas.title = win.title
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
glfwSetWindowIcon() {}
|
|
433
|
+
|
|
434
|
+
glfwGetWindowPos(winId, xPtr, yPtr) {
|
|
435
|
+
const win = this._windows.get(winId)
|
|
436
|
+
if (!win) return
|
|
437
|
+
const r = win.canvas.getBoundingClientRect()
|
|
438
|
+
this._wi32(xPtr, Math.round(r.left))
|
|
439
|
+
this._wi32(yPtr, Math.round(r.top))
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
glfwSetWindowPos(winId, x, y) {
|
|
443
|
+
const win = this._windows.get(winId)
|
|
444
|
+
if (!win) return
|
|
445
|
+
win.canvas.style.position = 'absolute'
|
|
446
|
+
win.canvas.style.left = x + 'px'
|
|
447
|
+
win.canvas.style.top = y + 'px'
|
|
448
|
+
win.x = x; win.y = y
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
glfwGetWindowSize(winId, wPtr, hPtr) {
|
|
452
|
+
const win = this._windows.get(winId)
|
|
453
|
+
if (!win) return
|
|
454
|
+
this._wi32(wPtr, win.width)
|
|
455
|
+
this._wi32(hPtr, win.height)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
glfwSetWindowSizeLimits() {}
|
|
459
|
+
glfwSetWindowAspectRatio() {}
|
|
460
|
+
|
|
461
|
+
glfwSetWindowSize(winId, w, h) {
|
|
462
|
+
const win = this._windows.get(winId)
|
|
463
|
+
if (!win) return
|
|
464
|
+
win.canvas.width = w; win.canvas.height = h
|
|
465
|
+
win.width = w; win.height = h
|
|
466
|
+
this._callFn(win.cb.windowSize, winId, w, h)
|
|
467
|
+
const dpr = window.devicePixelRatio || 1
|
|
468
|
+
this._callFn(win.cb.framebufferSize, winId,
|
|
469
|
+
Math.round(w * dpr), Math.round(h * dpr))
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
glfwGetFramebufferSize(winId, wPtr, hPtr) {
|
|
473
|
+
const win = this._windows.get(winId)
|
|
474
|
+
if (!win) return
|
|
475
|
+
const dpr = window.devicePixelRatio || 1
|
|
476
|
+
this._wi32(wPtr, Math.round(win.width * dpr))
|
|
477
|
+
this._wi32(hPtr, Math.round(win.height * dpr))
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
glfwGetWindowFrameSize(winId, lPtr, tPtr, rPtr, bPtr) {
|
|
481
|
+
this._wi32(lPtr, 0); this._wi32(tPtr, 0)
|
|
482
|
+
this._wi32(rPtr, 0); this._wi32(bPtr, 0)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
glfwGetWindowContentScale(winId, xsPtr, ysPtr) {
|
|
486
|
+
const dpr = window.devicePixelRatio || 1
|
|
487
|
+
this._wf32(xsPtr, dpr)
|
|
488
|
+
this._wf32(ysPtr, dpr)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
glfwGetWindowOpacity() { return 1.0 }
|
|
492
|
+
|
|
493
|
+
glfwSetWindowOpacity(winId, opacity) {
|
|
494
|
+
const win = this._windows.get(winId)
|
|
495
|
+
if (win) win.canvas.style.opacity = opacity
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
glfwIconifyWindow() {}
|
|
499
|
+
glfwRestoreWindow() {}
|
|
500
|
+
glfwMaximizeWindow() {}
|
|
501
|
+
|
|
502
|
+
glfwShowWindow(winId) {
|
|
503
|
+
const win = this._windows.get(winId)
|
|
504
|
+
if (win) { win.canvas.style.display = 'block'; win.visible = true }
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
glfwHideWindow(winId) {
|
|
508
|
+
const win = this._windows.get(winId)
|
|
509
|
+
if (win) { win.canvas.style.display = 'none'; win.visible = false }
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
glfwFocusWindow(winId) {
|
|
513
|
+
this._windows.get(winId)?.canvas.focus()
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
glfwRequestWindowAttention() {}
|
|
517
|
+
|
|
518
|
+
glfwGetWindowMonitor() { return 0 }
|
|
519
|
+
|
|
520
|
+
glfwSetWindowMonitor(winId, monId, x, y, w, h, refreshRate) {
|
|
521
|
+
const win = this._windows.get(winId)
|
|
522
|
+
if (!win) return
|
|
523
|
+
if (monId) {
|
|
524
|
+
win.canvas.style.width = '100vw'
|
|
525
|
+
win.canvas.style.height = '100vh'
|
|
526
|
+
win.canvas.style.position = 'fixed'
|
|
527
|
+
win.canvas.style.left = '0'
|
|
528
|
+
win.canvas.style.top = '0'
|
|
529
|
+
} else {
|
|
530
|
+
win.canvas.style.width = ''
|
|
531
|
+
win.canvas.style.height = ''
|
|
532
|
+
win.canvas.style.position = ''
|
|
533
|
+
this.glfwSetWindowPos(winId, x, y)
|
|
534
|
+
this.glfwSetWindowSize(winId, w, h)
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
glfwGetWindowAttrib(winId, attrib) {
|
|
539
|
+
const win = this._windows.get(winId)
|
|
540
|
+
if (!win) return 0
|
|
541
|
+
switch (attrib) {
|
|
542
|
+
case 0x00020001: return win.focused ? 1 : 0
|
|
543
|
+
case 0x00020002: return win.iconified ? 1 : 0
|
|
544
|
+
case 0x00020003: return 1 // resizable
|
|
545
|
+
case 0x00020004: return win.visible ? 1 : 0
|
|
546
|
+
case 0x00020005: return 1 // decorated
|
|
547
|
+
case 0x00020008: return win.maximized ? 1 : 0
|
|
548
|
+
case 0x0002000B: return win.focused ? 1 : 0 // hovered
|
|
549
|
+
case 0x00022001: return win.gl ? 0x00030001 : 0 // CLIENT_API
|
|
550
|
+
case 0x00022002: return 4 // CONTEXT_VERSION_MAJOR (WebGL2 ≈ GL 4.x)
|
|
551
|
+
case 0x00022003: return 6 // CONTEXT_VERSION_MINOR
|
|
552
|
+
default: return 0
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
glfwSetWindowAttrib() {}
|
|
557
|
+
|
|
558
|
+
glfwSetWindowUserPointer(winId, ptr) {
|
|
559
|
+
const win = this._windows.get(winId)
|
|
560
|
+
if (win) win.userPointer = ptr
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
glfwGetWindowUserPointer(winId) {
|
|
564
|
+
return this._windows.get(winId)?.userPointer ?? 0
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Window callbacks
|
|
568
|
+
glfwSetWindowPosCallback(w, cb) { return this._setCb(w, 'windowPos', cb) }
|
|
569
|
+
glfwSetWindowSizeCallback(w, cb) { return this._setCb(w, 'windowSize', cb) }
|
|
570
|
+
glfwSetWindowCloseCallback(w, cb) { return this._setCb(w, 'windowClose', cb) }
|
|
571
|
+
glfwSetWindowRefreshCallback(w, cb) { return this._setCb(w, 'windowRefresh', cb) }
|
|
572
|
+
glfwSetWindowFocusCallback(w, cb) { return this._setCb(w, 'windowFocus', cb) }
|
|
573
|
+
glfwSetWindowIconifyCallback(w, cb) { return this._setCb(w, 'windowIconify', cb) }
|
|
574
|
+
glfwSetWindowMaximizeCallback(w, cb) { return this._setCb(w, 'windowMaximize', cb) }
|
|
575
|
+
glfwSetFramebufferSizeCallback(w, cb) { return this._setCb(w, 'framebufferSize', cb) }
|
|
576
|
+
glfwSetWindowContentScaleCallback(w, cb) { return this._setCb(w, 'contentScale', cb) }
|
|
577
|
+
|
|
578
|
+
_setCb(winId, name, cb) {
|
|
579
|
+
const win = this._windows.get(winId)
|
|
580
|
+
if (!win) return 0
|
|
581
|
+
const prev = win.cb[name] ?? 0
|
|
582
|
+
win.cb[name] = cb
|
|
583
|
+
return prev
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ── Events ───────────────────────────────────────────────────────────────────
|
|
587
|
+
|
|
588
|
+
glfwPollEvents() {
|
|
589
|
+
// DOM events arrive asynchronously via listeners; key/button state is kept
|
|
590
|
+
// current. In an asyncify/JSPI setup, yielding here lets the event loop
|
|
591
|
+
// flush. In a synchronous loop this is effectively a no-op.
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
glfwWaitEvents() {}
|
|
595
|
+
glfwWaitEventsTimeout() {}
|
|
596
|
+
glfwPostEmptyEvent() {}
|
|
597
|
+
|
|
598
|
+
// ── Input ────────────────────────────────────────────────────────────────────
|
|
599
|
+
|
|
600
|
+
glfwGetInputMode(winId, mode) {
|
|
601
|
+
const win = this._windows.get(winId)
|
|
602
|
+
if (!win) return 0
|
|
603
|
+
switch (mode) {
|
|
604
|
+
case 0x00033001: return win.inputMode.cursor
|
|
605
|
+
case 0x00033002: return win.inputMode.stickyKeys
|
|
606
|
+
case 0x00033003: return win.inputMode.stickyMouseButtons
|
|
607
|
+
case 0x00033004: return win.inputMode.lockKeyMods
|
|
608
|
+
case 0x00033005: return win.inputMode.rawMouseMotion
|
|
609
|
+
default: return 0
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
glfwSetInputMode(winId, mode, value) {
|
|
614
|
+
const win = this._windows.get(winId)
|
|
615
|
+
if (!win) return
|
|
616
|
+
switch (mode) {
|
|
617
|
+
case 0x00033001:
|
|
618
|
+
win.inputMode.cursor = value
|
|
619
|
+
if (value === 0x00034002) { // HIDDEN
|
|
620
|
+
win.canvas.style.cursor = 'none'
|
|
621
|
+
} else if (value === 0x00034003) { // DISABLED
|
|
622
|
+
win.canvas.requestPointerLock?.()
|
|
623
|
+
win.canvas.style.cursor = 'none'
|
|
624
|
+
} else {
|
|
625
|
+
win.canvas.style.cursor = ''
|
|
626
|
+
if (document.pointerLockElement === win.canvas) document.exitPointerLock?.()
|
|
627
|
+
}
|
|
628
|
+
break
|
|
629
|
+
case 0x00033002: win.inputMode.stickyKeys = value; break
|
|
630
|
+
case 0x00033003: win.inputMode.stickyMouseButtons = value; break
|
|
631
|
+
case 0x00033004: win.inputMode.lockKeyMods = value; break
|
|
632
|
+
case 0x00033005: win.inputMode.rawMouseMotion = value; break
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
glfwRawMouseMotionSupported() { return 0 }
|
|
637
|
+
|
|
638
|
+
glfwGetKeyName(key, scancode) { return this._allocStr(KEY_NAMES[key] ?? '') }
|
|
639
|
+
glfwGetKeyScancode(key) { return key }
|
|
640
|
+
|
|
641
|
+
glfwGetKey(winId, key) {
|
|
642
|
+
const win = this._windows.get(winId)
|
|
643
|
+
if (!win) return 0
|
|
644
|
+
const state = win.keys.get(key) ?? 0
|
|
645
|
+
if (win.inputMode.stickyKeys && state) { win.keys.set(key, 0); return 1 }
|
|
646
|
+
return state
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
glfwGetMouseButton(winId, button) {
|
|
650
|
+
const win = this._windows.get(winId)
|
|
651
|
+
if (!win) return 0
|
|
652
|
+
const state = win.buttons.get(button) ?? 0
|
|
653
|
+
if (win.inputMode.stickyMouseButtons && state) { win.buttons.set(button, 0); return 1 }
|
|
654
|
+
return state
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
glfwGetCursorPos(winId, xPtr, yPtr) {
|
|
658
|
+
const win = this._windows.get(winId)
|
|
659
|
+
if (!win) return
|
|
660
|
+
this._wf64(xPtr, win.cx)
|
|
661
|
+
this._wf64(yPtr, win.cy)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
glfwSetCursorPos(winId, x, y) {
|
|
665
|
+
const win = this._windows.get(winId)
|
|
666
|
+
if (win) { win.cx = x; win.cy = y }
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// ── Cursors ──────────────────────────────────────────────────────────────────
|
|
670
|
+
|
|
671
|
+
glfwCreateCursor(imagePtr, xhot, yhot) {
|
|
672
|
+
const id = this._nextHandle++
|
|
673
|
+
this._cursors.set(id, { css: 'default' })
|
|
674
|
+
return id
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
glfwCreateStandardCursor(shape) {
|
|
678
|
+
const id = this._nextHandle++
|
|
679
|
+
this._cursors.set(id, { css: CURSOR_CSS[shape] ?? 'default' })
|
|
680
|
+
return id
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
glfwDestroyCursor(curId) { this._cursors.delete(curId) }
|
|
684
|
+
|
|
685
|
+
glfwSetCursor(winId, curId) {
|
|
686
|
+
const win = this._windows.get(winId)
|
|
687
|
+
if (!win) return
|
|
688
|
+
win.canvas.style.cursor = this._cursors.get(curId)?.css ?? ''
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Input callbacks
|
|
692
|
+
glfwSetKeyCallback(w, cb) { return this._setCb(w, 'key', cb) }
|
|
693
|
+
glfwSetCharCallback(w, cb) { return this._setCb(w, 'char', cb) }
|
|
694
|
+
glfwSetCharModsCallback(w, cb) { return this._setCb(w, 'charMods', cb) }
|
|
695
|
+
glfwSetMouseButtonCallback(w, cb) { return this._setCb(w, 'mouseButton', cb) }
|
|
696
|
+
glfwSetCursorPosCallback(w, cb) { return this._setCb(w, 'cursorPos', cb) }
|
|
697
|
+
glfwSetCursorEnterCallback(w, cb) { return this._setCb(w, 'cursorEnter', cb) }
|
|
698
|
+
glfwSetScrollCallback(w, cb) { return this._setCb(w, 'scroll', cb) }
|
|
699
|
+
glfwSetDropCallback(w, cb) { return this._setCb(w, 'drop', cb) }
|
|
700
|
+
|
|
701
|
+
// ── Joystick / Gamepad ───────────────────────────────────────────────────────
|
|
702
|
+
|
|
703
|
+
glfwJoystickPresent(jid) {
|
|
704
|
+
return (navigator.getGamepads?.()[jid]) ? 1 : 0
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
glfwGetJoystickAxes(jid, countPtr) {
|
|
708
|
+
const pad = navigator.getGamepads?.()[jid]
|
|
709
|
+
this._wi32(countPtr, pad ? pad.axes.length : 0)
|
|
710
|
+
if (!pad || !pad.axes.length) return 0
|
|
711
|
+
const ptr = this._malloc(pad.axes.length * 4)
|
|
712
|
+
if (!ptr) return 0
|
|
713
|
+
for (let i = 0; i < pad.axes.length; i++) {
|
|
714
|
+
this._view.setFloat32(ptr + i * 4, pad.axes[i], true)
|
|
715
|
+
}
|
|
716
|
+
return ptr
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
glfwGetJoystickButtons(jid, countPtr) {
|
|
720
|
+
const pad = navigator.getGamepads?.()[jid]
|
|
721
|
+
this._wi32(countPtr, pad ? pad.buttons.length : 0)
|
|
722
|
+
if (!pad || !pad.buttons.length) return 0
|
|
723
|
+
const ptr = this._malloc(pad.buttons.length)
|
|
724
|
+
if (!ptr) return 0
|
|
725
|
+
const buf = new Uint8Array(this._memory.buffer, ptr, pad.buttons.length)
|
|
726
|
+
for (let i = 0; i < pad.buttons.length; i++) buf[i] = pad.buttons[i].pressed ? 1 : 0
|
|
727
|
+
return ptr
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
glfwGetJoystickHats(jid, countPtr) { this._wi32(countPtr, 0); return 0 }
|
|
731
|
+
|
|
732
|
+
glfwGetJoystickName(jid) {
|
|
733
|
+
return this._allocStr(navigator.getGamepads?.()[jid]?.id ?? '')
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
glfwGetJoystickGUID() { return 0 }
|
|
737
|
+
glfwSetJoystickUserPointer() {}
|
|
738
|
+
glfwGetJoystickUserPointer() { return 0 }
|
|
739
|
+
glfwJoystickIsGamepad(jid) { return this.glfwJoystickPresent(jid) }
|
|
740
|
+
|
|
741
|
+
glfwSetJoystickCallback(cb) {
|
|
742
|
+
const prev = this._joystickCb
|
|
743
|
+
this._joystickCb = cb
|
|
744
|
+
return prev
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
glfwUpdateGamepadMappings() { return 1 }
|
|
748
|
+
|
|
749
|
+
glfwGetGamepadName(jid) { return this.glfwGetJoystickName(jid) }
|
|
750
|
+
|
|
751
|
+
glfwGetGamepadState(jid, statePtr) {
|
|
752
|
+
// GLFWgamepadstate: unsigned char buttons[15] + pad + float axes[6] = 40 bytes
|
|
753
|
+
const pad = navigator.getGamepads?.()[jid]
|
|
754
|
+
if (!pad || !statePtr) return 0
|
|
755
|
+
const mem = new Uint8Array(this._memory.buffer)
|
|
756
|
+
for (let i = 0; i < 15; i++) {
|
|
757
|
+
mem[statePtr + i] = pad.buttons[i]?.pressed ? 1 : 0
|
|
758
|
+
}
|
|
759
|
+
for (let i = 0; i < 6; i++) {
|
|
760
|
+
this._view.setFloat32(statePtr + 16 + i * 4, pad.axes[i] ?? 0, true)
|
|
761
|
+
}
|
|
762
|
+
return 1
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ── Clipboard ────────────────────────────────────────────────────────────────
|
|
766
|
+
|
|
767
|
+
glfwSetClipboardString(winId, strPtr) {
|
|
768
|
+
navigator.clipboard?.writeText(this._readStr(strPtr))
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
glfwGetClipboardString() {
|
|
772
|
+
// Clipboard API is async; synchronous access not possible in web.
|
|
773
|
+
return 0
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// ── Time ─────────────────────────────────────────────────────────────────────
|
|
777
|
+
|
|
778
|
+
glfwGetTime() {
|
|
779
|
+
return this._timeOffset + (performance.now() / 1000 - this._timeBase)
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
glfwSetTime(t) {
|
|
783
|
+
this._timeOffset = t
|
|
784
|
+
this._timeBase = performance.now() / 1000
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Returns i64 (BigInt in JS ↔ wasm i64 boundary)
|
|
788
|
+
glfwGetTimerValue() {
|
|
789
|
+
return BigInt(Math.floor(performance.now() * 1000))
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
glfwGetTimerFrequency() {
|
|
793
|
+
return BigInt(1_000_000)
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// ── Context ──────────────────────────────────────────────────────────────────
|
|
797
|
+
|
|
798
|
+
glfwMakeContextCurrent(winId) { this._currentContext = winId }
|
|
799
|
+
glfwGetCurrentContext() { return this._currentContext }
|
|
800
|
+
|
|
801
|
+
// WebGL auto-presents; swap and interval are no-ops
|
|
802
|
+
glfwSwapBuffers() {}
|
|
803
|
+
glfwSwapInterval() {}
|
|
804
|
+
|
|
805
|
+
glfwExtensionSupported() { return 0 }
|
|
806
|
+
|
|
807
|
+
// WebGL functions cannot be returned as raw function pointers to wasm.
|
|
808
|
+
// Use a dedicated WebGL import namespace (e.g. @easywasm/gl) for GL calls.
|
|
809
|
+
glfwGetProcAddress() { return 0 }
|
|
810
|
+
|
|
811
|
+
// ── Vulkan (not supported in web) ────────────────────────────────────────────
|
|
812
|
+
|
|
813
|
+
glfwVulkanSupported() { return 0 }
|
|
814
|
+
glfwGetRequiredInstanceExtensions(countPtr) { this._wi32(countPtr, 0); return 0 }
|
|
815
|
+
glfwGetInstanceProcAddress() { return 0 }
|
|
816
|
+
glfwGetPhysicalDevicePresentationSupport() { return 0 }
|
|
817
|
+
glfwCreateWindowSurface() { return -1 } // VK_ERROR_EXTENSION_NOT_PRESENT
|
|
818
|
+
|
|
819
|
+
// ── Private: DOM event wiring ────────────────────────────────────────────────
|
|
820
|
+
|
|
821
|
+
_on(win, target, type, fn, opts) {
|
|
822
|
+
target.addEventListener(type, fn, opts)
|
|
823
|
+
win.cleanups.push(() => target.removeEventListener(type, fn))
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
_attachEvents(win) {
|
|
827
|
+
const { canvas } = win
|
|
828
|
+
canvas.setAttribute('tabindex', '0')
|
|
829
|
+
|
|
830
|
+
this._on(win, canvas, 'keydown', e => {
|
|
831
|
+
const key = KEY_MAP[e.code] ?? -1
|
|
832
|
+
const mods = getMods(e)
|
|
833
|
+
const action = e.repeat ? 2 : 1 // REPEAT : PRESS
|
|
834
|
+
win.keys.set(key, action)
|
|
835
|
+
this._callFn(win.cb.key, win.id, key, key, action, mods)
|
|
836
|
+
if (e.key.length === 1) {
|
|
837
|
+
const cp = e.key.codePointAt(0)
|
|
838
|
+
this._callFn(win.cb.char, win.id, cp)
|
|
839
|
+
this._callFn(win.cb.charMods, win.id, cp, mods)
|
|
840
|
+
}
|
|
841
|
+
// Only suppress default for keys the app likely handles
|
|
842
|
+
if (!['F5', 'F12', 'Tab'].includes(e.key)) e.preventDefault()
|
|
843
|
+
})
|
|
844
|
+
|
|
845
|
+
this._on(win, canvas, 'keyup', e => {
|
|
846
|
+
const key = KEY_MAP[e.code] ?? -1
|
|
847
|
+
win.keys.set(key, 0)
|
|
848
|
+
this._callFn(win.cb.key, win.id, key, key, 0, getMods(e))
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
this._on(win, canvas, 'mousedown', e => {
|
|
852
|
+
canvas.focus()
|
|
853
|
+
const btn = BTN_MAP[e.button] ?? e.button
|
|
854
|
+
win.buttons.set(btn, 1)
|
|
855
|
+
this._callFn(win.cb.mouseButton, win.id, btn, 1, getMods(e))
|
|
856
|
+
e.preventDefault()
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
this._on(win, canvas, 'mouseup', e => {
|
|
860
|
+
const btn = BTN_MAP[e.button] ?? e.button
|
|
861
|
+
win.buttons.set(btn, 0)
|
|
862
|
+
this._callFn(win.cb.mouseButton, win.id, btn, 0, getMods(e))
|
|
863
|
+
})
|
|
864
|
+
|
|
865
|
+
this._on(win, canvas, 'mousemove', e => {
|
|
866
|
+
const r = canvas.getBoundingClientRect()
|
|
867
|
+
win.cx = e.clientX - r.left
|
|
868
|
+
win.cy = e.clientY - r.top
|
|
869
|
+
this._callFn(win.cb.cursorPos, win.id, win.cx, win.cy)
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
this._on(win, canvas, 'mouseenter', () => {
|
|
873
|
+
this._callFn(win.cb.cursorEnter, win.id, 1)
|
|
874
|
+
})
|
|
875
|
+
|
|
876
|
+
this._on(win, canvas, 'mouseleave', () => {
|
|
877
|
+
this._callFn(win.cb.cursorEnter, win.id, 0)
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
this._on(win, canvas, 'wheel', e => {
|
|
881
|
+
// Normalize to "lines" — deltaMode 0=pixel, 1=line, 2=page
|
|
882
|
+
const scale = e.deltaMode === 0 ? 1 / 100 : e.deltaMode === 2 ? 10 : 1
|
|
883
|
+
this._callFn(win.cb.scroll, win.id, -e.deltaX * scale, -e.deltaY * scale)
|
|
884
|
+
e.preventDefault()
|
|
885
|
+
}, { passive: false })
|
|
886
|
+
|
|
887
|
+
this._on(win, canvas, 'contextmenu', e => e.preventDefault())
|
|
888
|
+
|
|
889
|
+
this._on(win, canvas, 'dragover', e => e.preventDefault())
|
|
890
|
+
|
|
891
|
+
this._on(win, canvas, 'drop', e => {
|
|
892
|
+
e.preventDefault()
|
|
893
|
+
// TODO: allocate path strings and call drop callback
|
|
894
|
+
})
|
|
895
|
+
|
|
896
|
+
this._on(win, canvas, 'focus', () => {
|
|
897
|
+
win.focused = true
|
|
898
|
+
this._callFn(win.cb.windowFocus, win.id, 1)
|
|
899
|
+
})
|
|
900
|
+
|
|
901
|
+
this._on(win, canvas, 'blur', () => {
|
|
902
|
+
win.focused = false
|
|
903
|
+
// Clear all held keys on focus loss to avoid stuck-key issues
|
|
904
|
+
win.keys.clear()
|
|
905
|
+
win.buttons.clear()
|
|
906
|
+
this._callFn(win.cb.windowFocus, win.id, 0)
|
|
907
|
+
})
|
|
908
|
+
|
|
909
|
+
const ro = new ResizeObserver(entries => {
|
|
910
|
+
for (const entry of entries) {
|
|
911
|
+
const w = Math.round(entry.contentRect.width)
|
|
912
|
+
const h = Math.round(entry.contentRect.height)
|
|
913
|
+
if (w === win.width && h === win.height) continue
|
|
914
|
+
win.width = w; win.height = h
|
|
915
|
+
win.canvas.width = w; win.canvas.height = h
|
|
916
|
+
this._callFn(win.cb.windowSize, win.id, w, h)
|
|
917
|
+
const dpr = window.devicePixelRatio || 1
|
|
918
|
+
this._callFn(win.cb.framebufferSize, win.id, Math.round(w * dpr), Math.round(h * dpr))
|
|
919
|
+
}
|
|
920
|
+
})
|
|
921
|
+
ro.observe(canvas)
|
|
922
|
+
win.cleanups.push(() => ro.disconnect())
|
|
923
|
+
|
|
924
|
+
const onUnload = () => {
|
|
925
|
+
win.shouldClose = true
|
|
926
|
+
this._callFn(win.cb.windowClose, win.id)
|
|
927
|
+
}
|
|
928
|
+
window.addEventListener('beforeunload', onUnload)
|
|
929
|
+
win.cleanups.push(() => window.removeEventListener('beforeunload', onUnload))
|
|
930
|
+
}
|
|
931
|
+
}
|