// SPDX-License-Identifier: Unlicense OR MIT // Gio WASM window implementation. // Callback model: no goroutines. rAF drives the paint loop. // Events coalesce into the window state; user callbacks are fired during paint. package gio import ( "image" "image/color" "jsbridge/app" ) // Window is the application window. Only one window is supported per WASM module. type Window struct { glCtx GLContext g GPU // nil until GPU init completes scale float32 size image.Point // physical pixels animating bool rafPending bool focused bool ctxLost bool // WebGL context currently lost onFrame func(FrameEvent) onDestroy func() ptrEvents []AppPointerEvent keyEvents []AppKeyEvent textEvents []AppTextEvent pasteRunes []int32 // pending clipboard paste codepoints } // FrameEvent is delivered to the OnFrame callback. type FrameEvent struct { Size image.Point Metric Metric w *Window } // Frame submits the op list for rendering. Call exactly once per FrameEvent. func (e FrameEvent) Frame(ops *OpList) { e.w.renderFrame(ops) } // DestroyEvent signals window closure. type DestroyEvent struct{} // AppTextEvent holds a single Unicode codepoint from keyboard or IME input. type AppTextEvent struct { Codepoint int32 } // AppPasteEvent holds a single Unicode codepoint from a clipboard paste operation. type AppPasteEvent struct { Codepoint int32 } // AppFocusEvent signals window focus gain or loss. type AppFocusEvent struct { Focused bool } // AppPointerEvent holds a coalesced pointer/mouse/touch input event. type AppPointerEvent struct { Kind int32 // 0=Move 1=Press 2=Release 3=Scroll 4=Cancel X, Y float32 Dx, Dy float32 Buttons int32 Mods int32 } // AppKeyEvent holds a coalesced keyboard input event. type AppKeyEvent struct { Code int32 // Unicode codepoint or special code >= 256 Mods int32 Press bool } // windowState holds the active window singleton (one per WASM module). // Single package-level struct instance - the only mutable global. type windowState struct { w *Window } // wst is the window singleton. Accessed by wasmexport callbacks. var wst windowState // NewWindow creates and shows the application window. func NewWindow() *Window { app.CreateFullscreenCanvas() glCtxHandle := app.CreateWebGLContext() if glCtxHandle == 0 { panic("gio: failed to create WebGL2 context") } glCtx := GLContext(glCtxHandle) w := &Window{glCtx: glCtx} wst.w = w w.doResize() d, err := newOpenGLDevice(OpenGL{Context: glCtx}) if err != nil { panic("gio: OpenGL device init failed") } g, err := NewGPUWithDevice(d) if err != nil { panic("gio: GPU init failed") } w.g = g app.RegisterPointerEvents() app.RegisterKeyEvents() app.RegisterResizeEvents() app.RegisterTextEvents() app.RegisterContextLossEvents() w.scheduleRAF() return w } // FocusTextArea requests keyboard focus so text input is received. func (w *Window) FocusTextArea() { app.FocusTextArea() } // BlurTextArea releases keyboard focus. func (w *Window) BlurTextArea() { app.BlurTextArea() } // Focused reports whether the window textarea currently has focus. func (w *Window) Focused() bool { return w.focused } // WriteClipboard writes text to the system clipboard. func (w *Window) WriteClipboard(text string) { app.WriteClipboard(text) } // ReadClipboard requests an async clipboard read. // Paste codepoints will arrive via PasteRunes() on the next frame. func (w *Window) ReadClipboard() { app.ReadClipboard() } // OnFrame sets the frame callback. Called once per rAF with a FrameEvent. func (w *Window) OnFrame(fn func(FrameEvent)) { w.onFrame = fn } // OnDestroy sets the destroy callback. func (w *Window) OnDestroy(fn func()) { w.onDestroy = fn } // Invalidate requests a frame on the next animation frame. func (w *Window) Invalidate() { if !w.rafPending { w.scheduleRAF() } } // SetAnimating enables continuous animation at display refresh rate. func (w *Window) SetAnimating(anim bool) { w.animating = anim if anim && !w.rafPending { w.scheduleRAF() } } // SetTitle sets the browser tab title. func (w *Window) SetTitle(title string) { app.SetTitle(title) } // SetCursor sets the CSS cursor style. func (w *Window) SetCursor(name string) { app.SetCursor(name) } // SetFullscreen enters or exits fullscreen mode. func (w *Window) SetFullscreen(full bool) { app.SetFullscreen(full) } func (w *Window) scheduleRAF() { w.rafPending = true app.RequestRAF() } func (w *Window) doResize() { w.scale = app.GetDevicePixelRatio() var cssW, cssH int32 app.GetCanvasCSSSize(&cssW, &cssH) physW := int32(float32(cssW) * w.scale) physH := int32(float32(cssH) * w.scale) if physW < 1 { physW = 1 } if physH < 1 { physH = 1 } app.SetCanvasBacking(physW, physH) w.size = image.Point{X: int(physW), Y: int(physH)} } func (w *Window) paint() { w.rafPending = false if w.size.X <= 0 || w.size.Y <= 0 || w.g == nil { return } if w.onFrame != nil { w.onFrame(FrameEvent{ Size: w.size, Metric: Metric{PxPerDp: w.scale, PxPerSp: w.scale}, w: w, }) } if w.animating { w.scheduleRAF() } } func (w *Window) reinitGPU() { d, err := newOpenGLDevice(OpenGL{Context: w.glCtx}) if err != nil { return } g, err := NewGPUWithDevice(d) if err != nil { return } w.g = g w.ctxLost = false } func (w *Window) renderFrame(ops *OpList) { if w.g == nil { return } w.g.Clear(color.NRGBA{A: 0}) var nilTarget RenderTarget // nil = render to default canvas FBO if err := w.g.Frame(ops, nilTarget, w.size); err != nil { return } } // PointerEvents returns and clears pending pointer events. func (w *Window) PointerEvents() []AppPointerEvent { evts := w.ptrEvents w.ptrEvents = w.ptrEvents[:0] return evts } // KeyEvents returns and clears pending key events. func (w *Window) KeyEvents() []AppKeyEvent { evts := w.keyEvents w.keyEvents = w.keyEvents[:0] return evts } // TextEvents returns and clears pending text input events (keyboard + IME). func (w *Window) TextEvents() []AppTextEvent { evts := w.textEvents w.textEvents = w.textEvents[:0] return evts } // PasteRunes returns and clears pending clipboard paste codepoints. func (w *Window) PasteRunes() []int32 { r := w.pasteRunes w.pasteRunes = w.pasteRunes[:0] return r } // Window pointer event kind constants (raw int32 values from JS). // Distinct from event.mx's PointerKind bitmask type. const ( WinPointerMove int32 = 0 WinPointerPress int32 = 1 WinPointerRelease int32 = 2 WinPointerScroll int32 = 3 WinPointerCancel int32 = 4 ) // Key code constants (must match app.mjs). const ( KeyArrowUp int32 = 256 KeyArrowDown int32 = 257 KeyArrowLeft int32 = 258 KeyArrowRight int32 = 259 KeyEscape int32 = 260 KeyEnter int32 = 261 KeyBackspace int32 = 262 KeyDelete int32 = 263 KeyHome int32 = 264 KeyEnd int32 = 265 KeyPageUp int32 = 266 KeyPageDown int32 = 267 KeyTab int32 = 268 KeySpace int32 = 269 KeyF1 int32 = 270 KeyF12 int32 = 281 KeyCtrl int32 = 282 KeyShift int32 = 283 KeyAlt int32 = 284 KeySuper int32 = 285 KeyCommand int32 = 286 ) // Modifier bit constants. const ( ModAltBit int32 = 1 ModCtrlBit int32 = 2 ModShiftBit int32 = 4 ModMetaBit int32 = 8 ) // CSS cursor strings (pass directly to SetCursor). // These are kept as package-level constants for convenience; the actual // cursor type enum lives in event.mx as PointerCursor. const ( CSSCursorDefault = "default" CSSCursorPointer = "pointer" CSSCursorText = "text" CSSCursorCrosshair = "crosshair" CSSCursorGrab = "grab" CSSCursorGrabbing = "grabbing" CSSCursorNone = "none" CSSCursorWait = "wait" CSSCursorNSResize = "ns-resize" CSSCursorEWResize = "ew-resize" ) // --- JS export hooks --- //go:wasmexport __gio_raf func gioRAF() { if wst.w != nil { wst.w.paint() } } //go:wasmexport __gio_resize func gioResize() { if wst.w != nil { wst.w.doResize() wst.w.Invalidate() } } //go:wasmexport __gio_pointer func gioPointer(kind int32, x, y, dx, dy float32, buttons, mods int32) { if wst.w == nil { return } wst.w.ptrEvents = append(wst.w.ptrEvents, AppPointerEvent{ Kind: kind, X: x, Y: y, Dx: dx, Dy: dy, Buttons: buttons, Mods: mods, }) wst.w.Invalidate() } //go:wasmexport __gio_key func gioKey(code, mods, press int32) { if wst.w == nil { return } wst.w.keyEvents = append(wst.w.keyEvents, AppKeyEvent{ Code: code, Mods: mods, Press: press != 0, }) wst.w.Invalidate() } //go:wasmexport __gio_text func gioText(codepoint int32) { if wst.w == nil { return } wst.w.textEvents = append(wst.w.textEvents, AppTextEvent{Codepoint: codepoint}) wst.w.Invalidate() } //go:wasmexport __gio_paste func gioPaste(codepoint int32) { if wst.w == nil { return } wst.w.pasteRunes = append(wst.w.pasteRunes, codepoint) wst.w.Invalidate() } //go:wasmexport __gio_focus func gioFocus(focused int32) { if wst.w == nil { return } wst.w.focused = focused != 0 wst.w.Invalidate() } //go:wasmexport __gio_context_loss func gioContextLoss(lost int32) { if wst.w == nil { return } if lost != 0 { wst.w.g = nil wst.w.ctxLost = true } else { wst.w.reinitGPU() wst.w.Invalidate() } }