@easywasm/gl 0.0.2 → 0.0.4

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/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
+ }