mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
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 <mail@eliasnaur.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
+402
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user