app.mx raw
1 // SPDX-License-Identifier: Unlicense OR MIT
2
3 // Gio WASM window implementation.
4 // Callback model: no goroutines. rAF drives the paint loop.
5 // Events coalesce into the window state; user callbacks are fired during paint.
6
7 package gio
8
9 import (
10 "image"
11 "image/color"
12
13 "jsbridge/app"
14 )
15
16 // Window is the application window. Only one window is supported per WASM module.
17 type Window struct {
18 glCtx GLContext
19 g GPU // nil until GPU init completes
20
21 scale float32
22 size image.Point // physical pixels
23
24 animating bool
25 rafPending bool
26 focused bool
27 ctxLost bool // WebGL context currently lost
28
29 onFrame func(FrameEvent)
30 onDestroy func()
31
32 ptrEvents []AppPointerEvent
33 keyEvents []AppKeyEvent
34 textEvents []AppTextEvent
35 pasteRunes []int32 // pending clipboard paste codepoints
36 }
37
38 // FrameEvent is delivered to the OnFrame callback.
39 type FrameEvent struct {
40 Size image.Point
41 Metric Metric
42 w *Window
43 }
44
45 // Frame submits the op list for rendering. Call exactly once per FrameEvent.
46 func (e FrameEvent) Frame(ops *OpList) {
47 e.w.renderFrame(ops)
48 }
49
50 // DestroyEvent signals window closure.
51 type DestroyEvent struct{}
52
53 // AppTextEvent holds a single Unicode codepoint from keyboard or IME input.
54 type AppTextEvent struct {
55 Codepoint int32
56 }
57
58 // AppPasteEvent holds a single Unicode codepoint from a clipboard paste operation.
59 type AppPasteEvent struct {
60 Codepoint int32
61 }
62
63 // AppFocusEvent signals window focus gain or loss.
64 type AppFocusEvent struct {
65 Focused bool
66 }
67
68 // AppPointerEvent holds a coalesced pointer/mouse/touch input event.
69 type AppPointerEvent struct {
70 Kind int32 // 0=Move 1=Press 2=Release 3=Scroll 4=Cancel
71 X, Y float32
72 Dx, Dy float32
73 Buttons int32
74 Mods int32
75 }
76
77 // AppKeyEvent holds a coalesced keyboard input event.
78 type AppKeyEvent struct {
79 Code int32 // Unicode codepoint or special code >= 256
80 Mods int32
81 Press bool
82 }
83
84 // windowState holds the active window singleton (one per WASM module).
85 // Single package-level struct instance - the only mutable global.
86 type windowState struct {
87 w *Window
88 }
89
90 // wst is the window singleton. Accessed by wasmexport callbacks.
91 var wst windowState
92
93 // NewWindow creates and shows the application window.
94 func NewWindow() *Window {
95 app.CreateFullscreenCanvas()
96
97 glCtxHandle := app.CreateWebGLContext()
98 if glCtxHandle == 0 {
99 panic("gio: failed to create WebGL2 context")
100 }
101 glCtx := GLContext(glCtxHandle)
102
103 w := &Window{glCtx: glCtx}
104 wst.w = w
105
106 w.doResize()
107
108 d, err := newOpenGLDevice(OpenGL{Context: glCtx})
109 if err != nil {
110 panic("gio: OpenGL device init failed")
111 }
112 g, err := NewGPUWithDevice(d)
113 if err != nil {
114 panic("gio: GPU init failed")
115 }
116 w.g = g
117
118 app.RegisterPointerEvents()
119 app.RegisterKeyEvents()
120 app.RegisterResizeEvents()
121 app.RegisterTextEvents()
122 app.RegisterContextLossEvents()
123
124 w.scheduleRAF()
125 return w
126 }
127
128 // FocusTextArea requests keyboard focus so text input is received.
129 func (w *Window) FocusTextArea() { app.FocusTextArea() }
130
131 // BlurTextArea releases keyboard focus.
132 func (w *Window) BlurTextArea() { app.BlurTextArea() }
133
134 // Focused reports whether the window textarea currently has focus.
135 func (w *Window) Focused() bool { return w.focused }
136
137 // WriteClipboard writes text to the system clipboard.
138 func (w *Window) WriteClipboard(text string) { app.WriteClipboard(text) }
139
140 // ReadClipboard requests an async clipboard read.
141 // Paste codepoints will arrive via PasteRunes() on the next frame.
142 func (w *Window) ReadClipboard() { app.ReadClipboard() }
143
144 // OnFrame sets the frame callback. Called once per rAF with a FrameEvent.
145 func (w *Window) OnFrame(fn func(FrameEvent)) { w.onFrame = fn }
146
147 // OnDestroy sets the destroy callback.
148 func (w *Window) OnDestroy(fn func()) { w.onDestroy = fn }
149
150 // Invalidate requests a frame on the next animation frame.
151 func (w *Window) Invalidate() {
152 if !w.rafPending {
153 w.scheduleRAF()
154 }
155 }
156
157 // SetAnimating enables continuous animation at display refresh rate.
158 func (w *Window) SetAnimating(anim bool) {
159 w.animating = anim
160 if anim && !w.rafPending {
161 w.scheduleRAF()
162 }
163 }
164
165 // SetTitle sets the browser tab title.
166 func (w *Window) SetTitle(title string) { app.SetTitle(title) }
167
168 // SetCursor sets the CSS cursor style.
169 func (w *Window) SetCursor(name string) { app.SetCursor(name) }
170
171 // SetFullscreen enters or exits fullscreen mode.
172 func (w *Window) SetFullscreen(full bool) { app.SetFullscreen(full) }
173
174 func (w *Window) scheduleRAF() {
175 w.rafPending = true
176 app.RequestRAF()
177 }
178
179 func (w *Window) doResize() {
180 w.scale = app.GetDevicePixelRatio()
181 var cssW, cssH int32
182 app.GetCanvasCSSSize(&cssW, &cssH)
183 physW := int32(float32(cssW) * w.scale)
184 physH := int32(float32(cssH) * w.scale)
185 if physW < 1 {
186 physW = 1
187 }
188 if physH < 1 {
189 physH = 1
190 }
191 app.SetCanvasBacking(physW, physH)
192 w.size = image.Point{X: int(physW), Y: int(physH)}
193 }
194
195 func (w *Window) paint() {
196 w.rafPending = false
197 if w.size.X <= 0 || w.size.Y <= 0 || w.g == nil {
198 return
199 }
200 if w.onFrame != nil {
201 w.onFrame(FrameEvent{
202 Size: w.size,
203 Metric: Metric{PxPerDp: w.scale, PxPerSp: w.scale},
204 w: w,
205 })
206 }
207 if w.animating {
208 w.scheduleRAF()
209 }
210 }
211
212 func (w *Window) reinitGPU() {
213 d, err := newOpenGLDevice(OpenGL{Context: w.glCtx})
214 if err != nil {
215 return
216 }
217 g, err := NewGPUWithDevice(d)
218 if err != nil {
219 return
220 }
221 w.g = g
222 w.ctxLost = false
223 }
224
225 func (w *Window) renderFrame(ops *OpList) {
226 if w.g == nil {
227 return
228 }
229 w.g.Clear(color.NRGBA{A: 0})
230 var nilTarget RenderTarget // nil = render to default canvas FBO
231 if err := w.g.Frame(ops, nilTarget, w.size); err != nil {
232 return
233 }
234 }
235
236 // PointerEvents returns and clears pending pointer events.
237 func (w *Window) PointerEvents() []AppPointerEvent {
238 evts := w.ptrEvents
239 w.ptrEvents = w.ptrEvents[:0]
240 return evts
241 }
242
243 // KeyEvents returns and clears pending key events.
244 func (w *Window) KeyEvents() []AppKeyEvent {
245 evts := w.keyEvents
246 w.keyEvents = w.keyEvents[:0]
247 return evts
248 }
249
250 // TextEvents returns and clears pending text input events (keyboard + IME).
251 func (w *Window) TextEvents() []AppTextEvent {
252 evts := w.textEvents
253 w.textEvents = w.textEvents[:0]
254 return evts
255 }
256
257 // PasteRunes returns and clears pending clipboard paste codepoints.
258 func (w *Window) PasteRunes() []int32 {
259 r := w.pasteRunes
260 w.pasteRunes = w.pasteRunes[:0]
261 return r
262 }
263
264 // Window pointer event kind constants (raw int32 values from JS).
265 // Distinct from event.mx's PointerKind bitmask type.
266 const (
267 WinPointerMove int32 = 0
268 WinPointerPress int32 = 1
269 WinPointerRelease int32 = 2
270 WinPointerScroll int32 = 3
271 WinPointerCancel int32 = 4
272 )
273
274 // Key code constants (must match app.mjs).
275 const (
276 KeyArrowUp int32 = 256
277 KeyArrowDown int32 = 257
278 KeyArrowLeft int32 = 258
279 KeyArrowRight int32 = 259
280 KeyEscape int32 = 260
281 KeyEnter int32 = 261
282 KeyBackspace int32 = 262
283 KeyDelete int32 = 263
284 KeyHome int32 = 264
285 KeyEnd int32 = 265
286 KeyPageUp int32 = 266
287 KeyPageDown int32 = 267
288 KeyTab int32 = 268
289 KeySpace int32 = 269
290 KeyF1 int32 = 270
291 KeyF12 int32 = 281
292 KeyCtrl int32 = 282
293 KeyShift int32 = 283
294 KeyAlt int32 = 284
295 KeySuper int32 = 285
296 KeyCommand int32 = 286
297 )
298
299 // Modifier bit constants.
300 const (
301 ModAltBit int32 = 1
302 ModCtrlBit int32 = 2
303 ModShiftBit int32 = 4
304 ModMetaBit int32 = 8
305 )
306
307 // CSS cursor strings (pass directly to SetCursor).
308 // These are kept as package-level constants for convenience; the actual
309 // cursor type enum lives in event.mx as PointerCursor.
310 const (
311 CSSCursorDefault = "default"
312 CSSCursorPointer = "pointer"
313 CSSCursorText = "text"
314 CSSCursorCrosshair = "crosshair"
315 CSSCursorGrab = "grab"
316 CSSCursorGrabbing = "grabbing"
317 CSSCursorNone = "none"
318 CSSCursorWait = "wait"
319 CSSCursorNSResize = "ns-resize"
320 CSSCursorEWResize = "ew-resize"
321 )
322
323 // --- JS export hooks ---
324
325 //go:wasmexport __gio_raf
326 func gioRAF() {
327 if wst.w != nil {
328 wst.w.paint()
329 }
330 }
331
332 //go:wasmexport __gio_resize
333 func gioResize() {
334 if wst.w != nil {
335 wst.w.doResize()
336 wst.w.Invalidate()
337 }
338 }
339
340 //go:wasmexport __gio_pointer
341 func gioPointer(kind int32, x, y, dx, dy float32, buttons, mods int32) {
342 if wst.w == nil {
343 return
344 }
345 wst.w.ptrEvents = append(wst.w.ptrEvents, AppPointerEvent{
346 Kind: kind, X: x, Y: y, Dx: dx, Dy: dy,
347 Buttons: buttons, Mods: mods,
348 })
349 wst.w.Invalidate()
350 }
351
352 //go:wasmexport __gio_key
353 func gioKey(code, mods, press int32) {
354 if wst.w == nil {
355 return
356 }
357 wst.w.keyEvents = append(wst.w.keyEvents, AppKeyEvent{
358 Code: code, Mods: mods, Press: press != 0,
359 })
360 wst.w.Invalidate()
361 }
362
363 //go:wasmexport __gio_text
364 func gioText(codepoint int32) {
365 if wst.w == nil {
366 return
367 }
368 wst.w.textEvents = append(wst.w.textEvents, AppTextEvent{Codepoint: codepoint})
369 wst.w.Invalidate()
370 }
371
372 //go:wasmexport __gio_paste
373 func gioPaste(codepoint int32) {
374 if wst.w == nil {
375 return
376 }
377 wst.w.pasteRunes = append(wst.w.pasteRunes, codepoint)
378 wst.w.Invalidate()
379 }
380
381 //go:wasmexport __gio_focus
382 func gioFocus(focused int32) {
383 if wst.w == nil {
384 return
385 }
386 wst.w.focused = focused != 0
387 wst.w.Invalidate()
388 }
389
390 //go:wasmexport __gio_context_loss
391 func gioContextLoss(lost int32) {
392 if wst.w == nil {
393 return
394 }
395 if lost != 0 {
396 wst.w.g = nil
397 wst.w.ctxLost = true
398 } else {
399 wst.w.reinitGPU()
400 wst.w.Invalidate()
401 }
402 }
403