Files

408 lines
8.9 KiB
Go

//go:build js
package web
import (
"bytes"
"math"
"strconv"
"strings"
"syscall/js"
"aego/core/errs"
"aego/core/mathx"
"aego/platform"
"aego/platform/input"
)
const (
MimeText = "text/plain"
CanvasAttr = "data-aego"
contextName = "webgl2"
)
type Options struct {
Canvas js.Value
Selector string
}
type listener struct {
target js.Value
name string
fn js.Func
}
type Platform struct {
opt Options
global js.Value
doc js.Value
nav js.Value
raf js.Value
now js.Value
gamepads js.Value
info platform.Info
win *Window
queue []platform.Event
spare []platform.Event
listeners []listener
noop js.Func
start float64
hiddenAt float64
hiddenFor float64
hidden bool
padLive int
pads [input.MaxPads]padSlot
pad padFrame
}
func try(v js.Value, name string, args ...any) js.Value {
if !v.Truthy() || v.Get(name).Type() != js.TypeFunction {
return js.Undefined()
}
return v.Call(name, args...)
}
func New(opt Options) (*Platform, error) {
g := js.Global()
doc := g.Get("document")
if !doc.Truthy() {
return nil, errs.New(errs.PlatformInit, "the web platform needs a browser document")
}
perf := g.Get("performance")
nav := g.Get("navigator")
p := &Platform{
opt: opt,
global: g,
doc: doc,
nav: nav,
raf: g.Get("requestAnimationFrame").Call("bind", g),
now: perf.Get("now").Call("bind", perf),
queue: make([]platform.Event, 0, 64),
spare: make([]platform.Event, 0, 64),
}
if f := nav.Get("getGamepads"); f.Type() == js.TypeFunction {
p.gamepads = f.Call("bind", nav)
}
p.info = platform.Info{OS: "js", Arch: "wasm", Backend: "web", Version: nav.Get("userAgent").String(), VideoDriver: contextName}
p.noop = js.FuncOf(func(js.Value, []js.Value) any { return nil })
p.start = p.ms()
p.hidden = doc.Get("hidden").Truthy()
p.hiddenAt = p.start
p.listen(doc, "visibilitychange", true, p.visibility)
p.listen(g, "gamepadconnected", true, func(js.Value) { p.padLive++ })
p.listen(g, "gamepaddisconnected", true, func(js.Value) { p.padLive = max(0, p.padLive-1) })
p.listen(g, "blur", true, func(js.Value) { p.push(platform.Event{Kind: platform.EvWindowFocusLost}) })
return p, nil
}
func (p *Platform) ms() float64 {
return p.now.Invoke().Float()
}
func (p *Platform) Now() float64 {
t := p.ms()
if p.hidden {
t = p.hiddenAt
}
return (t - p.start - p.hiddenFor) / 1000
}
func (p *Platform) visibility(js.Value) {
hidden := p.doc.Get("hidden").Truthy()
if hidden == p.hidden {
return
}
t := p.ms()
if hidden {
p.hiddenAt = t
p.push(platform.Event{Kind: platform.EvWindowFocusLost})
} else {
p.hiddenFor += t - p.hiddenAt
}
p.hidden = hidden
}
func (p *Platform) quiet(v js.Value) {
if v.Truthy() && v.Get("catch").Type() == js.TypeFunction {
v.Call("catch", p.noop)
}
}
func (p *Platform) writeText(s string) {
p.quiet(try(p.nav.Get("clipboard"), "writeText", s))
}
func (p *Platform) canvas() js.Value {
c := p.opt.Canvas
if !c.Truthy() && p.opt.Selector != "" {
c = p.doc.Call("querySelector", p.opt.Selector)
}
if !c.Truthy() {
c = p.doc.Call("querySelector", "canvas["+CanvasAttr+"]")
}
if !c.Truthy() {
c = p.doc.Call("createElement", "canvas")
c.Call("setAttribute", CanvasAttr, "")
p.doc.Get("body").Call("appendChild", c)
}
return c
}
func (p *Platform) CreateWindow(desc platform.WindowDesc) (platform.Window, error) {
if p.win != nil {
return nil, errs.New(errs.PlatformWindow, "only one window is supported")
}
c := p.canvas()
ctx := c.Call("getContext", contextName, map[string]any{
"alpha": false, "antialias": false, "depth": false, "stencil": false,
"premultipliedAlpha": false, "preserveDrawingBuffer": false, "powerPreference": "high-performance",
})
if !ctx.Truthy() {
return nil, errs.New(errs.PlatformContext, "this browser has no WebGL2")
}
w := &Window{p: p, canvas: c, style: c.Get("style"), ctx: ctx, cursor: 255}
c.Set("tabIndex", 0)
w.style.Set("touchAction", "none")
w.style.Set("outline", "none")
if r := c.Call("getBoundingClientRect"); r.Get("width").Float() == 0 || r.Get("height").Float() == 0 {
w.style.Set("width", strconv.Itoa(max(desc.Width, 1))+"px")
w.style.Set("height", strconv.Itoa(max(desc.Height, 1))+"px")
}
p.win = w
w.refresh(false)
p.listenWindow(w)
w.wantFull = desc.Fullscreen
return w, nil
}
func (p *Platform) PollEvents(fn func(platform.Event)) {
if w := p.win; w != nil && p.global.Get("devicePixelRatio").Float() != w.dpr {
w.refresh(true)
}
p.pollPads()
if len(p.queue) == 0 {
return
}
p.spare, p.queue = p.queue, p.spare[:0]
for i := range p.spare {
fn(p.spare[i])
}
p.spare = p.spare[:0]
}
func (p *Platform) Drive(step func() bool) {
done := make(chan struct{})
var frame js.Func
frame = js.FuncOf(func(js.Value, []js.Value) any {
if !step() {
frame.Release()
close(done)
return nil
}
p.raf.Invoke(frame)
return nil
})
p.raf.Invoke(frame)
<-done
}
func (p *Platform) Info() platform.Info {
return p.info
}
func (p *Platform) Shutdown() {
if p.win != nil {
p.win.Close()
}
for _, l := range p.listeners {
l.target.Call("removeEventListener", l.name, l.fn)
l.fn.Release()
}
p.listeners = nil
}
type memory struct {
mime string
data []byte
text string
}
type Window struct {
p *Platform
canvas js.Value
style js.Value
ctx js.Value
observer js.Value
resize js.Func
observing bool
metrics platform.WindowMetrics
dpr float64
last mathx.Vec2
textInput bool
cursor platform.CursorShape
wantLock bool
locked bool
wantFull bool
clip memory
closed bool
}
func (w *Window) WebGL2() js.Value {
return w.ctx
}
func (w *Window) Canvas() js.Value {
return w.canvas
}
func (w *Window) refresh(push bool) {
r := w.canvas.Call("getBoundingClientRect")
cw, ch := r.Get("width").Float(), r.Get("height").Float()
dpr := w.p.global.Get("devicePixelRatio").Float()
if dpr <= 0 {
dpr = 1
}
w.dpr = dpr
m := platform.WindowMetrics{
LogicalW: int(math.Round(cw)),
LogicalH: int(math.Round(ch)),
PixelW: max(1, int(math.Round(cw*dpr))),
PixelH: max(1, int(math.Round(ch*dpr))),
PixelDensity: float32(dpr),
DisplayScale: float32(dpr),
}
if m == w.metrics {
return
}
if w.canvas.Get("width").Int() != m.PixelW {
w.canvas.Set("width", m.PixelW)
}
if w.canvas.Get("height").Int() != m.PixelH {
w.canvas.Set("height", m.PixelH)
}
w.metrics = m
if push {
w.p.push(platform.Event{Kind: platform.EvWindowResized})
}
}
func (w *Window) pos(e js.Value) mathx.Vec2 {
return mathx.Vec2{X: float32(e.Get("offsetX").Float()), Y: float32(e.Get("offsetY").Float())}
}
func (w *Window) gesture() {
if w.wantLock && !w.locked {
w.p.quiet(try(w.canvas, "requestPointerLock"))
}
if w.wantFull {
w.wantFull = false
if !w.p.doc.Get("fullscreenElement").Truthy() {
w.p.quiet(try(w.canvas, "requestFullscreen"))
}
}
}
func (w *Window) Metrics() platform.WindowMetrics {
return w.metrics
}
func (w *Window) SetTitle(s string) {
w.p.doc.Set("title", s)
}
func (w *Window) SetVSync(platform.VSyncMode) platform.VSyncMode {
return platform.VSyncOn
}
func (w *Window) SetFullscreen(on bool) {
w.wantFull = on
if !on && w.p.doc.Get("fullscreenElement").Truthy() {
w.p.quiet(try(w.p.doc, "exitFullscreen"))
}
}
func (w *Window) SetPointerLock(on bool) {
w.wantLock = on
if !on && w.locked {
try(w.p.doc, "exitPointerLock")
}
}
func (w *Window) PointerLocked() bool {
return w.locked
}
func (w *Window) SwapBuffers() {}
func (w *Window) StartTextInput() {
w.textInput = true
}
func (w *Window) StopTextInput() {
w.textInput = false
}
func (w *Window) SetTextInputArea(mathx.Rect, int) {}
func textMime(m string) bool {
return m == MimeText || strings.HasPrefix(m, MimeText+";")
}
func (w *Window) ClipboardText() string {
return w.clip.text
}
func (w *Window) SetClipboardText(s string) {
w.clip = memory{text: s}
w.p.writeText(s)
}
func (w *Window) SetClipboardData(mime string, data []byte, text string) bool {
w.clip = memory{mime: mime, data: bytes.Clone(data), text: text}
w.p.writeText(text)
return true
}
func (w *Window) ClipboardData(mime string) ([]byte, bool) {
switch {
case mime != "" && mime == w.clip.mime:
return bytes.Clone(w.clip.data), true
case textMime(mime) && w.clip.text != "":
return []byte(w.clip.text), true
}
return nil, false
}
func (w *Window) SetCursor(s platform.CursorShape) {
if s == w.cursor {
return
}
w.cursor = s
w.style.Set("cursor", cursorName(s))
}
func (w *Window) CaptureMouse(bool) {}
func (w *Window) Close() {
if w.closed {
return
}
w.closed = true
if w.observing {
w.observer.Call("disconnect")
w.resize.Release()
w.observing = false
}
if w.p.win == w {
w.p.win = nil
}
}
var (
_ platform.Driver = (*Platform)(nil)
_ platform.TextInputer = (*Window)(nil)
_ platform.Clipboard = (*Window)(nil)
_ platform.Cursorer = (*Window)(nil)
_ platform.MouseCapturer = (*Window)(nil)
)