From 0397cbb599a37bef5a16ceb6431af127201f34ea Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Sat, 11 May 2019 13:15:48 +0200 Subject: [PATCH] ui/app: add basic webassembly/webgl support It is still slow. And crashy: https://github.com/golang/go/issues/31980. Signed-off-by: Elias Naur --- ui/app/gl_js.go | 81 +++++++ ui/app/internal/gl/gl_js.go | 329 +++++++++++++++++++++++++++ ui/app/internal/gl/types_js.go | 19 ++ ui/app/os_js.go | 402 +++++++++++++++++++++++++++++++++ 4 files changed, 831 insertions(+) create mode 100644 ui/app/gl_js.go create mode 100644 ui/app/internal/gl/gl_js.go create mode 100644 ui/app/internal/gl/types_js.go create mode 100644 ui/app/os_js.go diff --git a/ui/app/gl_js.go b/ui/app/gl_js.go new file mode 100644 index 00000000..a2ffa11d --- /dev/null +++ b/ui/app/gl_js.go @@ -0,0 +1,81 @@ +package app + +import ( + "errors" + "syscall/js" + + "gioui.org/ui/app/internal/gl" +) + +type context struct { + ctx js.Value + cnv js.Value + f *gl.Functions + srgbFBO *gl.SRGBFBO +} + +func newContext(w *window) (*context, error) { + args := map[string]interface{}{ + // Enable low latency rendering. + // See https://developers.google.com/web/updates/2019/05/desynchronized. + "desynchronized": true, + "preserveDrawingBuffer": true, + } + ctx := w.cnv.Call("getContext", "webgl2", args) + if ctx == js.Null() { + ctx = w.cnv.Call("getContext", "webgl", args) + } + if ctx == js.Null() { + return nil, errors.New("app: webgl is not supported") + } + f := &gl.Functions{Ctx: ctx} + f.Init() + c := &context{ + ctx: ctx, + cnv: w.cnv, + f: f, + } + return c, nil +} + +func (c *context) Functions() *gl.Functions { + return c.f +} + +func (c *context) Release() { + if c.srgbFBO != nil { + c.srgbFBO.Release() + c.srgbFBO = nil + } +} + +func (c *context) Present() error { + if c.srgbFBO != nil { + c.srgbFBO.Blit() + } + if c.srgbFBO != nil { + c.srgbFBO.AfterPresent() + } + if c.ctx.Call("isContextLost").Bool() { + return errors.New("context lost") + } + return nil +} + +func (c *context) MakeCurrent() error { + if c.srgbFBO == nil { + var err error + c.srgbFBO, err = gl.NewSRGBFBO(c.f) + if err != nil { + c.Release() + c.srgbFBO = nil + return err + } + } + w, h := c.cnv.Get("width").Int(), c.cnv.Get("height").Int() + if err := c.srgbFBO.Refresh(w, h); err != nil { + c.Release() + return err + } + return nil +} diff --git a/ui/app/internal/gl/gl_js.go b/ui/app/internal/gl/gl_js.go new file mode 100644 index 00000000..3d8282e1 --- /dev/null +++ b/ui/app/internal/gl/gl_js.go @@ -0,0 +1,329 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gl + +import ( + "strings" + "syscall/js" +) + +type Functions struct { + Ctx js.Value + EXT_disjoint_timer_query js.Value + EXT_disjoint_timer_query_webgl2 js.Value +} + +var ( + uint8Array = js.Global().Get("Uint8Array") + int32Array = js.Global().Get("Int32Array") +) + +func (f *Functions) Init() { + f.EXT_disjoint_timer_query_webgl2 = f.getExtension("EXT_disjoint_timer_query_webgl2") + if f.EXT_disjoint_timer_query_webgl2 == js.Null() { + f.EXT_disjoint_timer_query = f.getExtension("EXT_disjoint_timer_query") + } + // Enable extensions. + f.getExtension("OES_texture_half_float") + f.getExtension("OES_texture_float") + f.getExtension("EXT_sRGB") + // WebGL2 extensions + f.getExtension("EXT_color_buffer_half_float") + f.getExtension("EXT_color_buffer_float") +} + +func (f *Functions) getExtension(name string) js.Value { + return f.Ctx.Call("getExtension", name) +} + +func (f *Functions) ActiveTexture(t Enum) { + f.Ctx.Call("activeTexture", int(t)) +} +func (f *Functions) AttachShader(p Program, s Shader) { + f.Ctx.Call("attachShader", js.Value(p), js.Value(s)) +} +func (f *Functions) BeginQuery(target Enum, query Query) { + if f.EXT_disjoint_timer_query_webgl2 != js.Null() { + f.Ctx.Call("beginQuery", int(target), js.Value(query)) + } else { + f.EXT_disjoint_timer_query.Call("beginQueryEXT", int(target), js.Value(query)) + } +} +func (f *Functions) BindAttribLocation(p Program, a Attrib, name string) { + f.Ctx.Call("bindAttribLocation", js.Value(p), int(a), name) +} +func (f *Functions) BindBuffer(target Enum, b Buffer) { + f.Ctx.Call("bindBuffer", int(target), js.Value(b)) +} +func (f *Functions) BindFramebuffer(target Enum, fb Framebuffer) { + f.Ctx.Call("bindFramebuffer", int(target), js.Value(fb)) +} +func (f *Functions) BindRenderbuffer(target Enum, rb Renderbuffer) { + f.Ctx.Call("bindRenderbuffer", int(target), js.Value(rb)) +} +func (f *Functions) BindTexture(target Enum, t Texture) { + f.Ctx.Call("bindTexture", int(target), js.Value(t)) +} +func (f *Functions) BlendEquation(mode Enum) { + f.Ctx.Call("blendEquation", int(mode)) +} +func (f *Functions) BlendFunc(sfactor, dfactor Enum) { + f.Ctx.Call("blendFunc", int(sfactor), int(dfactor)) +} +func (f *Functions) BufferData(target Enum, src []byte, usage Enum) { + a := byteArrayOf(src) + f.Ctx.Call("bufferData", int(target), a, int(usage)) +} +func (f *Functions) CheckFramebufferStatus(target Enum) Enum { + return Enum(f.Ctx.Call("checkFramebufferStatus", int(target)).Int()) +} +func (f *Functions) Clear(mask Enum) { + f.Ctx.Call("clear", int(mask)) +} +func (f *Functions) ClearColor(red, green, blue, alpha float32) { + f.Ctx.Call("clearColor", red, green, blue, alpha) +} +func (f *Functions) ClearDepthf(d float32) { + f.Ctx.Call("clearDepth", d) +} +func (f *Functions) CompileShader(s Shader) { + f.Ctx.Call("compileShader", js.Value(s)) +} +func (f *Functions) CreateBuffer() Buffer { + return Buffer(f.Ctx.Call("createBuffer")) +} +func (f *Functions) CreateFramebuffer() Framebuffer { + return Framebuffer(f.Ctx.Call("createFramebuffer")) +} +func (f *Functions) CreateProgram() Program { + return Program(f.Ctx.Call("createProgram")) +} +func (f *Functions) CreateQuery() Query { + return Query(f.Ctx.Call("createQuery")) +} +func (f *Functions) CreateRenderbuffer() Renderbuffer { + return Renderbuffer(f.Ctx.Call("createRenderbuffer")) +} +func (f *Functions) CreateShader(ty Enum) Shader { + return Shader(f.Ctx.Call("createShader", int(ty))) +} +func (f *Functions) CreateTexture() Texture { + return Texture(f.Ctx.Call("createTexture")) +} +func (f *Functions) DeleteBuffer(v Buffer) { + f.Ctx.Call("deleteBuffer", js.Value(v)) +} +func (f *Functions) DeleteFramebuffer(v Framebuffer) { + f.Ctx.Call("deleteFramebuffer", js.Value(v)) +} +func (f *Functions) DeleteProgram(p Program) { + f.Ctx.Call("deleteProgram", js.Value(p)) +} +func (f *Functions) DeleteQuery(query Query) { + if f.EXT_disjoint_timer_query_webgl2 != js.Null() { + f.Ctx.Call("deleteQuery", js.Value(query)) + } else { + f.EXT_disjoint_timer_query.Call("deleteQueryEXT", js.Value(query)) + } +} +func (f *Functions) DeleteShader(s Shader) { + f.Ctx.Call("deleteShader", js.Value(s)) +} +func (f *Functions) DeleteRenderbuffer(v Renderbuffer) { + f.Ctx.Call("deleteRenderbuffer", js.Value(v)) +} +func (f *Functions) DeleteTexture(v Texture) { + f.Ctx.Call("deleteTexture", js.Value(v)) +} +func (f *Functions) DepthFunc(fn Enum) { + f.Ctx.Call("depthFunc", int(fn)) +} +func (f *Functions) DepthMask(mask bool) { + f.Ctx.Call("depthMask", mask) +} +func (f *Functions) DisableVertexAttribArray(a Attrib) { + f.Ctx.Call("disableVertexAttribArray", int(a)) +} +func (f *Functions) Disable(cap Enum) { + f.Ctx.Call("disable", int(cap)) +} +func (f *Functions) DrawArrays(mode Enum, first, count int) { + f.Ctx.Call("drawArrays", int(mode), first, count) +} +func (f *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) { + f.Ctx.Call("drawElements", int(mode), count, int(ty), offset) +} +func (f *Functions) Enable(cap Enum) { + f.Ctx.Call("enable", int(cap)) +} +func (f *Functions) EnableVertexAttribArray(a Attrib) { + f.Ctx.Call("enableVertexAttribArray", int(a)) +} +func (f *Functions) EndQuery(target Enum) { + if f.EXT_disjoint_timer_query_webgl2 != js.Null() { + f.Ctx.Call("endQuery", int(target)) + } else { + f.EXT_disjoint_timer_query.Call("endQueryEXT", int(target)) + } +} +func (f *Functions) Finish() { + f.Ctx.Call("finish") +} +func (f *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) { + f.Ctx.Call("framebufferRenderbuffer", int(target), int(attachment), int(renderbuffertarget), js.Value(renderbuffer)) +} +func (f *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) { + f.Ctx.Call("framebufferTexture2D", int(target), int(attachment), int(texTarget), js.Value(t), level) +} +func (f *Functions) GetError() Enum { + return Enum(f.Ctx.Call("getError").Int()) +} +func (f *Functions) GetRenderbufferParameteri(target, pname Enum) int { + return paramVal(f.Ctx.Call("getRenderbufferParameteri", int(pname))) +} +func (f *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int { + return paramVal(f.Ctx.Call("getFramebufferAttachmentParameter", int(target), int(attachment), int(pname))) +} +func (f *Functions) GetBinding(pname Enum) Object { + return Object(f.Ctx.Call("getParameter", int(pname))) +} +func (f *Functions) GetInteger(pname Enum) int { + return paramVal(f.Ctx.Call("getParameter", int(pname))) +} +func (f *Functions) GetProgrami(p Program, pname Enum) int { + return paramVal(f.Ctx.Call("getProgramParameter", js.Value(p), int(pname))) +} +func (f *Functions) GetProgramInfoLog(p Program) string { + return f.Ctx.Call("getProgramInfoLog", js.Value(p)).String() +} +func (f *Functions) GetQueryObjectuiv(query Query, pname Enum) uint { + if f.EXT_disjoint_timer_query_webgl2 != js.Null() { + return uint(paramVal(f.Ctx.Call("getQueryParameter", js.Value(query), int(pname)))) + } else { + return uint(paramVal(f.EXT_disjoint_timer_query.Call("getQueryObjectEXT", js.Value(query), int(pname)))) + } +} +func (f *Functions) GetShaderi(s Shader, pname Enum) int { + return paramVal(f.Ctx.Call("getShaderParameter", js.Value(s), int(pname))) +} +func (f *Functions) GetShaderInfoLog(s Shader) string { + return f.Ctx.Call("getShaderInfoLog", js.Value(s)).String() +} +func (f *Functions) GetString(pname Enum) string { + switch pname { + case EXTENSIONS: + extsjs := f.Ctx.Call("getSupportedExtensions") + var exts []string + for i := 0; i < extsjs.Length(); i++ { + exts = append(exts, "GL_"+extsjs.Index(i).String()) + } + return strings.Join(exts, " ") + default: + return f.Ctx.Call("getParameter", int(pname)).String() + } +} +func (f *Functions) GetUniformLocation(p Program, name string) Uniform { + return Uniform(f.Ctx.Call("getUniformLocation", js.Value(p), name)) +} +func (f *Functions) InvalidateFramebuffer(target, attachment Enum) { + fn := f.Ctx.Get("invalidateFramebuffer") + if fn != js.Undefined() { + f.Ctx.Call("invalidateFramebuffer", int(target), int32ArrayOf([]int32{int32(attachment)})) + } +} +func (f *Functions) LinkProgram(p Program) { + f.Ctx.Call("linkProgram", js.Value(p)) +} +func (f *Functions) PixelStorei(pname Enum, param int32) { + f.Ctx.Call("pixelStorei", int(pname), param) +} +func (f *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) { + f.Ctx.Call("renderbufferStorage", int(target), int(internalformat), width, height) +} +func (f *Functions) ReadPixels(x, y, width, height int, format, ty Enum, data []byte) { + a := byteArrayOf(data) + f.Ctx.Call("readPixels", x, y, width, height, int(format), int(ty), a) + d := js.TypedArrayOf(data) + d.Call("set", a) +} +func (f *Functions) Scissor(x, y, width, height int32) { + f.Ctx.Call("scissor", x, y, width, height) +} +func (f *Functions) ShaderSource(s Shader, src string) { + f.Ctx.Call("shaderSource", js.Value(s), src) +} +func (f *Functions) TexImage2D(target Enum, level int, internalFormat int, width, height int, format, ty Enum, data []byte) { + f.Ctx.Call("texImage2D", int(target), int(level), int(internalFormat), int(width), int(height), 0, int(format), int(ty), byteArrayOf(data)) +} +func (f *Functions) TexSubImage2D(target Enum, level int, x, y, width, height int, format, ty Enum, data []byte) { + f.Ctx.Call("texSubImage2D", int(target), level, x, y, width, height, int(format), int(ty), byteArrayOf(data)) +} +func (f *Functions) TexParameteri(target, pname Enum, param int) { + f.Ctx.Call("texParameteri", int(target), int(pname), int(param)) +} +func (f *Functions) Uniform1f(dst Uniform, v float32) { + f.Ctx.Call("uniform1f", js.Value(dst), v) +} +func (f *Functions) Uniform1i(dst Uniform, v int) { + f.Ctx.Call("uniform1i", js.Value(dst), v) +} +func (f *Functions) Uniform2f(dst Uniform, v0, v1 float32) { + f.Ctx.Call("uniform2f", js.Value(dst), v0, v1) +} +func (f *Functions) Uniform3f(dst Uniform, v0, v1, v2 float32) { + f.Ctx.Call("uniform3f", js.Value(dst), v0, v1, v2) +} +func (f *Functions) Uniform4f(dst Uniform, v0, v1, v2, v3 float32) { + f.Ctx.Call("uniform4f", js.Value(dst), v0, v1, v2, v3) +} +func (f *Functions) UseProgram(p Program) { + f.Ctx.Call("useProgram", js.Value(p)) +} +func (f *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int) { + f.Ctx.Call("vertexAttribPointer", int(dst), size, int(ty), normalized, stride, offset) +} +func (f *Functions) Viewport(x, y, width, height int) { + f.Ctx.Call("viewport", x, y, width, height) +} +func paramVal(v js.Value) int { + switch v.Type() { + case js.TypeBoolean: + if b := v.Bool(); b { + return 1 + } else { + return 0 + } + case js.TypeNumber: + return v.Int() + default: + panic("unknown parameter type") + } +} + +// *arrayOf copy Go slices into JS backed typed arrays to avoid +// using typed array views of Go memory in case the Go runtime +// decides to grow memory. +// TODO: The workaround is not enough: https://github.com/golang/go/issues/31980. + +func int32ArrayOf(data []int32) js.Value { + if len(data) == 0 { + return js.Null() + } + var a js.Value + a = int32Array.New(len(data)) + s := js.TypedArrayOf(data) + a.Call("set", s) + s.Release() + return a +} +func byteArrayOf(data []byte) js.Value { + if len(data) == 0 { + return js.Null() + } + var a js.Value + a = uint8Array.New(len(data)) + s := js.TypedArrayOf(data) + a.Call("set", s) + s.Release() + return a +} diff --git a/ui/app/internal/gl/types_js.go b/ui/app/internal/gl/types_js.go new file mode 100644 index 00000000..954cf0e5 --- /dev/null +++ b/ui/app/internal/gl/types_js.go @@ -0,0 +1,19 @@ +package gl + +import "syscall/js" + +type ( + Buffer js.Value + Framebuffer js.Value + Program js.Value + Renderbuffer js.Value + Shader js.Value + Texture js.Value + Query js.Value + Uniform js.Value + Object js.Value +) + +func (u Uniform) Valid() bool { + return js.Value(u) != js.Null() +} diff --git a/ui/app/os_js.go b/ui/app/os_js.go new file mode 100644 index 00000000..fc32dfda --- /dev/null +++ b/ui/app/os_js.go @@ -0,0 +1,402 @@ +package app + +import ( + "errors" + "image" + "sync" + "syscall/js" + "time" + + "gioui.org/ui" + "gioui.org/ui/f32" + "gioui.org/ui/key" + "gioui.org/ui/pointer" +) + +type window struct { + window js.Value + parent js.Value + cnv js.Value + tarea js.Value + w *Window + redraw js.Func + requestAnimationFrame js.Value + cleanfuncs []func() + touches []js.Value + composing bool + + mu sync.Mutex + scale float32 + animating bool +} + +var mainDone = make(chan struct{}) + +func createWindow(opts *WindowOptions) error { + doc := js.Global().Get("document") + parent := doc.Call("getElementById", "giowindow") + if parent == js.Null() { + return errors.New("app: #giowindow not found found") + } + cnv := createCanvas(doc) + parent.Call("appendChild", cnv) + tarea := createTextArea(doc) + parent.Call("appendChild", tarea) + w := &window{ + cnv: cnv, + tarea: tarea, + window: js.Global().Get("window"), + } + w.requestAnimationFrame = w.window.Get("requestAnimationFrame") + w.redraw = w.funcOf(func(this js.Value, args []js.Value) interface{} { + w.animCallback() + return nil + }) + w.addEventListeners() + w.w = newWindow(w) + go func() { + windows <- w.w + w.w.event(ChangeStage{StageVisible}) + w.draw(true) + select {} + w.cleanup() + close(mainDone) + }() + return nil +} + +func createTextArea(doc js.Value) js.Value { + tarea := doc.Call("createElement", "textarea") + style := tarea.Get("style") + style.Set("width", "1px") + style.Set("height", "1px") + style.Set("opacity", "0") + style.Set("border", "0") + style.Set("padding", "0") + tarea.Set("autocomplete", "off") + tarea.Set("autocorrect", "off") + tarea.Set("autocapitalize", "off") + tarea.Set("spellcheck", false) + return tarea +} + +func createCanvas(doc js.Value) js.Value { + cnv := doc.Call("createElement", "canvas") + style := cnv.Get("style") + style.Set("position", "fixed") + style.Set("width", "100%") + style.Set("height", "100%") + return cnv +} + +func (w *window) cleanup() { + // Cleanup in the opposite order of + // construction. + for i := len(w.cleanfuncs) - 1; i >= 0; i-- { + w.cleanfuncs[i]() + } + w.cleanfuncs = nil +} + +func (w *window) addEventListeners() { + w.addEventListener(w.window, "resize", func(this js.Value, args []js.Value) interface{} { + w.draw(true) + return nil + }) + w.addEventListener(w.cnv, "mousemove", func(this js.Value, args []js.Value) interface{} { + w.pointerEvent(pointer.Move, 0, 0, args[0]) + return nil + }) + w.addEventListener(w.cnv, "mousedown", func(this js.Value, args []js.Value) interface{} { + w.focus() + w.pointerEvent(pointer.Press, 0, 0, args[0]) + return nil + }) + w.addEventListener(w.cnv, "mouseup", func(this js.Value, args []js.Value) interface{} { + w.pointerEvent(pointer.Release, 0, 0, args[0]) + return nil + }) + w.addEventListener(w.cnv, "wheel", func(this js.Value, args []js.Value) interface{} { + e := args[0] + dx, dy := e.Get("deltaX").Float(), e.Get("deltaY").Float() + mode := e.Get("deltaMode").Int() + switch mode { + case 0x01: // DOM_DELTA_LINE + dx *= 10 + dy *= 10 + case 0x02: // DOM_DELTA_PAGE + dx *= 120 + dy *= 120 + } + w.pointerEvent(pointer.Move, float32(dx), float32(dy), e) + return nil + }) + w.addEventListener(w.cnv, "touchstart", func(this js.Value, args []js.Value) interface{} { + w.focus() + w.touchEvent(pointer.Press, args[0]) + return nil + }) + w.addEventListener(w.cnv, "touchend", func(this js.Value, args []js.Value) interface{} { + w.touchEvent(pointer.Release, args[0]) + return nil + }) + w.addEventListener(w.cnv, "touchmove", func(this js.Value, args []js.Value) interface{} { + w.touchEvent(pointer.Move, args[0]) + return nil + }) + w.addEventListener(w.cnv, "touchcancel", func(this js.Value, args []js.Value) interface{} { + // Cancel all touches even if only one touch was cancelled. + for i := range w.touches { + w.touches[i] = js.Null() + } + w.touches = w.touches[:0] + w.w.event(pointer.Event{ + Type: pointer.Cancel, + Source: pointer.Touch, + }) + return nil + }) + w.addEventListener(w.tarea, "keydown", func(this js.Value, args []js.Value) interface{} { + w.keyEvent(args[0]) + return nil + }) + w.addEventListener(w.tarea, "compositionstart", func(this js.Value, args []js.Value) interface{} { + w.composing = true + return nil + }) + w.addEventListener(w.tarea, "compositionend", func(this js.Value, args []js.Value) interface{} { + w.composing = false + w.flushInput() + return nil + }) + w.addEventListener(w.tarea, "input", func(this js.Value, args []js.Value) interface{} { + if w.composing { + return nil + } + w.flushInput() + return nil + }) +} + +func (w *window) flushInput() { + val := w.tarea.Get("value").String() + w.tarea.Set("value", "") + w.w.event(key.Edit{Text: string(val)}) +} + +func (w *window) blur() { + w.tarea.Call("blur") +} + +func (w *window) focus() { + w.tarea.Call("focus") +} + +func (w *window) keyEvent(e js.Value) { + k := e.Get("key").String() + if n, ok := translateKey(k); ok { + cmd := key.Chord{Name: n} + if e.Call("getModifierState", "Control").Bool() { + cmd.Modifiers |= key.ModCommand + } + w.w.event(cmd) + } +} + +func (w *window) touchEvent(typ pointer.Type, e js.Value) { + e.Call("preventDefault") + t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond + changedTouches := e.Get("changedTouches") + n := changedTouches.Length() + rect := w.cnv.Call("getBoundingClientRect") + w.mu.Lock() + scale := w.scale + w.mu.Unlock() + for i := 0; i < n; i++ { + touch := changedTouches.Index(i) + pid := w.touchIDFor(touch) + x, y := touch.Get("clientX").Float(), touch.Get("clientY").Float() + x -= rect.Get("left").Float() + y -= rect.Get("top").Float() + pos := f32.Point{ + X: float32(x) * scale, + Y: float32(y) * scale, + } + w.w.event(pointer.Event{ + Type: typ, + Source: pointer.Touch, + Position: pos, + PointerID: pid, + Time: t, + }) + } +} + +func (w *window) touchIDFor(touch js.Value) pointer.ID { + id := touch.Get("identifier") + for i, id2 := range w.touches { + if id2 == id { + return pointer.ID(i) + } + } + pid := pointer.ID(len(w.touches)) + w.touches = append(w.touches, id) + return pid +} + +func (w *window) pointerEvent(typ pointer.Type, dx, dy float32, e js.Value) { + e.Call("preventDefault") + x, y := e.Get("clientX").Float(), e.Get("clientY").Float() + rect := w.cnv.Call("getBoundingClientRect") + x -= rect.Get("left").Float() + y -= rect.Get("top").Float() + w.mu.Lock() + scale := w.scale + w.mu.Unlock() + pos := f32.Point{ + X: float32(x) * scale, + Y: float32(y) * scale, + } + scroll := f32.Point{ + X: dx * scale, + Y: dy * scale, + } + t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond + w.w.event(pointer.Event{ + Type: typ, + Source: pointer.Mouse, + Position: pos, + Scroll: scroll, + Time: t, + }) +} + +func (w *window) addEventListener(this js.Value, event string, f func(this js.Value, args []js.Value) interface{}) { + jsf := w.funcOf(f) + this.Call("addEventListener", event, jsf) + w.cleanfuncs = append(w.cleanfuncs, func() { + this.Call("removeEventListener", event, jsf) + }) +} + +// funcOf is like js.FuncOf but adds the js.Func to a list of +// functions to be released up. +func (w *window) funcOf(f func(this js.Value, args []js.Value) interface{}) js.Func { + jsf := js.FuncOf(f) + w.cleanfuncs = append(w.cleanfuncs, jsf.Release) + return jsf +} + +func (w *window) animCallback() { + w.mu.Lock() + anim := w.animating + if anim { + w.requestAnimationFrame.Invoke(w.redraw) + } + w.mu.Unlock() + if anim { + w.draw(false) + } +} + +func (w *window) setAnimating(anim bool) { + w.mu.Lock() + defer w.mu.Unlock() + if anim && !w.animating { + w.requestAnimationFrame.Invoke(w.redraw) + } + w.animating = anim +} + +func (w *window) setTextInput(s key.TextInputState) { + switch s { + case key.TextInputOpen: + w.focus() + case key.TextInputClosed: + w.blur() + } +} + +func (w *window) draw(sync bool) { + width, height, scale, cfg := w.config() + if cfg == (ui.Config{}) { + return + } + w.mu.Lock() + w.scale = float32(scale) + w.mu.Unlock() + cfg.Now = time.Now() + w.w.event(Draw{ + Size: image.Point{ + X: width, + Y: height, + }, + Config: &cfg, + sync: sync, + }) +} + +func (w *window) config() (int, int, float32, ui.Config) { + rect := w.cnv.Call("getBoundingClientRect") + width, height := rect.Get("width").Float(), rect.Get("height").Float() + scale := w.window.Get("devicePixelRatio").Float() + width *= scale + height *= scale + iw, ih := int(width+.5), int(height+.5) + // Adjust internal size of canvas if necessary. + if cw, ch := w.cnv.Get("width").Int(), w.cnv.Get("height").Int(); iw != cw || ih != ch { + w.cnv.Set("width", iw) + w.cnv.Set("height", ih) + } + const ppdp = 96 * inchPrDp + return iw, ih, float32(scale), ui.Config{ + PxPerDp: ppdp * float32(scale), + PxPerSp: ppdp * float32(scale), + } +} + +func Main() { + <-mainDone +} + +func translateKey(k string) (rune, bool) { + if len(k) == 1 { + c := k[0] + if '0' <= c && c <= '9' || 'A' <= c && c <= 'Z' { + return rune(c), true + } + if 'a' <= c && c <= 'z' { + return rune(c - 0x20), true + } + } + var n rune + switch k { + case "ArrowUp": + n = key.NameUpArrow + case "ArrowDown": + n = key.NameDownArrow + case "ArrowLeft": + n = key.NameLeftArrow + case "ArrowRight": + n = key.NameRightArrow + case "Escape": + n = key.NameEscape + case "Enter": + n = key.NameReturn + case "Backspace": + n = key.NameDeleteBackward + case "Delete": + n = key.NameDeleteForward + case "Home": + n = key.NameHome + case "End": + n = key.NameEnd + case "PageUp": + n = key.NamePageUp + case "PageDown": + n = key.NamePageDown + default: + return 0, false + } + return n, true +}