mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-02 07:57:29 +00:00
a3b9c7818f
Replace CreateWindow with NewWindow that immediately creates a Window ready to use. Drop the Windows channel of windows created by the system. For iOS and Android where the system creates the windows, let them rendezvous with the window created in the first NewWindow call. Android is further changed so that destroying and re-creating the Java Activity simply reconnects with the original Window. Signed-off-by: Elias Naur <mail@eliasnaur.com>
412 lines
9.7 KiB
Go
412 lines
9.7 KiB
Go
package app
|
|
|
|
import (
|
|
"errors"
|
|
"image"
|
|
"sync"
|
|
"syscall/js"
|
|
"time"
|
|
|
|
"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(win *Window, 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 = win
|
|
go func() {
|
|
w.w.setDriver(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.TextInputClose:
|
|
w.blur()
|
|
}
|
|
}
|
|
|
|
func (w *window) draw(sync bool) {
|
|
width, height, scale, cfg := w.config()
|
|
if cfg == (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, 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), 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
|
|
}
|