mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 23:55:39 +00:00
47c5859d87
We are going to encourage a model where pointers to a central (program global) Configs are passed to widgets at setup time, and not pass Configs at every frame. That way, the global Gonfig can change, but the pointers won't need updating. This change only switches the Draw event's Config pointer to a value to avoid tempting programs to use the event Config instead of updating their own central Configs. 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(ChangeStage{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.Focus{Focus: true})
|
|
return nil
|
|
})
|
|
w.addEventListener(w.tarea, "blur", func(this js.Value, args []js.Value) interface{} {
|
|
w.w.event(key.Focus{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.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
|
|
}
|
|
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(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 * 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
|
|
}
|