mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 15:45:38 +00:00
0b6dd4efd9
Match the Go error naming convention (FooError). Signed-off-by: Elias Naur <mail@eliasnaur.com>
413 lines
9.7 KiB
Go
413 lines
9.7 KiB
Go
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.focus()
|
|
w.w.event(StageEvent{StageRunning})
|
|
w.draw(true)
|
|
select {}
|
|
w.cleanup()
|
|
close(mainDone)
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func createTextArea(doc js.Value) js.Value {
|
|
tarea := doc.Call("createElement", "input")
|
|
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.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.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, "focus", func(this js.Value, args []js.Value) interface{} {
|
|
w.w.event(key.FocusEvent{Focus: true})
|
|
return nil
|
|
})
|
|
w.addEventListener(w.tarea, "blur", func(this js.Value, args []js.Value) interface{} {
|
|
w.w.event(key.FocusEvent{Focus: false})
|
|
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.EditEvent{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.ChordEvent{Name: n}
|
|
if e.Call("getModifierState", "Control").Bool() {
|
|
cmd.Modifiers |= key.ModCommand
|
|
}
|
|
if e.Call("getModifierState", "Shift").Bool() {
|
|
cmd.Modifiers |= key.ModShift
|
|
}
|
|
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(DrawEvent{
|
|
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 * monitorScale
|
|
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
|
|
}
|