aboutsummaryrefslogtreecommitdiff
path: root/vendor/gioui.org/app/os_js.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/gioui.org/app/os_js.go')
-rw-r--r--vendor/gioui.org/app/os_js.go722
1 files changed, 722 insertions, 0 deletions
diff --git a/vendor/gioui.org/app/os_js.go b/vendor/gioui.org/app/os_js.go
new file mode 100644
index 0000000..5b1388c
--- /dev/null
+++ b/vendor/gioui.org/app/os_js.go
@@ -0,0 +1,722 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package app
+
+import (
+ "fmt"
+ "image"
+ "image/color"
+ "strings"
+ "syscall/js"
+ "time"
+ "unicode"
+ "unicode/utf8"
+
+ "gioui.org/internal/f32color"
+
+ "gioui.org/f32"
+ "gioui.org/io/clipboard"
+ "gioui.org/io/key"
+ "gioui.org/io/pointer"
+ "gioui.org/io/system"
+ "gioui.org/unit"
+)
+
+type ViewEvent struct{}
+
+type window struct {
+ window js.Value
+ document js.Value
+ head js.Value
+ clipboard js.Value
+ cnv js.Value
+ tarea js.Value
+ w *callbacks
+ redraw js.Func
+ clipboardCallback js.Func
+ requestAnimationFrame js.Value
+ browserHistory js.Value
+ visualViewport js.Value
+ screenOrientation js.Value
+ cleanfuncs []func()
+ touches []js.Value
+ composing bool
+ requestFocus bool
+
+ chanAnimation chan struct{}
+ chanRedraw chan struct{}
+
+ config Config
+ inset f32.Point
+ scale float32
+ animating bool
+ // animRequested tracks whether a requestAnimationFrame callback
+ // is pending.
+ animRequested bool
+ wakeups chan struct{}
+}
+
+func newWindow(win *callbacks, options []Option) error {
+ doc := js.Global().Get("document")
+ cont := getContainer(doc)
+ cnv := createCanvas(doc)
+ cont.Call("appendChild", cnv)
+ tarea := createTextArea(doc)
+ cont.Call("appendChild", tarea)
+ w := &window{
+ cnv: cnv,
+ document: doc,
+ tarea: tarea,
+ window: js.Global().Get("window"),
+ head: doc.Get("head"),
+ clipboard: js.Global().Get("navigator").Get("clipboard"),
+ wakeups: make(chan struct{}, 1),
+ }
+ w.requestAnimationFrame = w.window.Get("requestAnimationFrame")
+ w.browserHistory = w.window.Get("history")
+ w.visualViewport = w.window.Get("visualViewport")
+ if w.visualViewport.IsUndefined() {
+ w.visualViewport = w.window
+ }
+ if screen := w.window.Get("screen"); screen.Truthy() {
+ w.screenOrientation = screen.Get("orientation")
+ }
+ w.chanAnimation = make(chan struct{}, 1)
+ w.chanRedraw = make(chan struct{}, 1)
+ w.redraw = w.funcOf(func(this js.Value, args []js.Value) interface{} {
+ w.chanAnimation <- struct{}{}
+ return nil
+ })
+ w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} {
+ content := args[0].String()
+ go win.Event(clipboard.Event{Text: content})
+ return nil
+ })
+ w.addEventListeners()
+ w.addHistory()
+ w.w = win
+
+ go func() {
+ defer w.cleanup()
+ w.w.SetDriver(w)
+ w.Configure(options)
+ w.blur()
+ w.w.Event(system.StageEvent{Stage: system.StageRunning})
+ w.resize()
+ w.draw(true)
+ for {
+ select {
+ case <-w.wakeups:
+ w.w.Event(wakeupEvent{})
+ case <-w.chanAnimation:
+ w.animCallback()
+ case <-w.chanRedraw:
+ w.draw(true)
+ }
+ }
+ }()
+ return nil
+}
+
+func getContainer(doc js.Value) js.Value {
+ cont := doc.Call("getElementById", "giowindow")
+ if !cont.IsNull() {
+ return cont
+ }
+ cont = doc.Call("createElement", "DIV")
+ doc.Get("body").Call("appendChild", cont)
+ return cont
+}
+
+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.visualViewport, "resize", func(this js.Value, args []js.Value) interface{} {
+ w.resize()
+ w.chanRedraw <- struct{}{}
+ return nil
+ })
+ w.addEventListener(w.window, "contextmenu", func(this js.Value, args []js.Value) interface{} {
+ args[0].Call("preventDefault")
+ return nil
+ })
+ w.addEventListener(w.window, "popstate", func(this js.Value, args []js.Value) interface{} {
+ ev := &system.CommandEvent{Type: system.CommandBack}
+ w.w.Event(ev)
+ if ev.Cancel {
+ return w.browserHistory.Call("forward")
+ }
+
+ return w.browserHistory.Call("back")
+ })
+ w.addEventListener(w.document, "visibilitychange", func(this js.Value, args []js.Value) interface{} {
+ ev := system.StageEvent{}
+ switch w.document.Get("visibilityState").String() {
+ case "hidden", "prerender", "unloaded":
+ ev.Stage = system.StagePaused
+ default:
+ ev.Stage = system.StageRunning
+ }
+ w.w.Event(ev)
+ 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])
+ if w.requestFocus {
+ w.focus()
+ w.requestFocus = false
+ }
+ 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.Scroll, 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])
+ if w.requestFocus {
+ w.focus() // iOS can only focus inside a Touch event.
+ w.requestFocus = false
+ }
+ 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})
+ w.blur()
+ return nil
+ })
+ w.addEventListener(w.tarea, "keydown", func(this js.Value, args []js.Value) interface{} {
+ w.keyEvent(args[0], key.Press)
+ return nil
+ })
+ w.addEventListener(w.tarea, "keyup", func(this js.Value, args []js.Value) interface{} {
+ w.keyEvent(args[0], key.Release)
+ 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
+ })
+ w.addEventListener(w.tarea, "paste", func(this js.Value, args []js.Value) interface{} {
+ if w.clipboard.IsUndefined() {
+ return nil
+ }
+ // Prevents duplicated-paste, since "paste" is already handled through Clipboard API.
+ args[0].Call("preventDefault")
+ return nil
+ })
+}
+
+func (w *window) addHistory() {
+ w.browserHistory.Call("pushState", nil, nil, w.window.Get("location").Get("href"))
+}
+
+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")
+ w.requestFocus = false
+}
+
+func (w *window) focus() {
+ w.tarea.Call("focus")
+ w.requestFocus = true
+}
+
+func (w *window) keyboard(hint key.InputHint) {
+ var m string
+ switch hint {
+ case key.HintAny:
+ m = "text"
+ case key.HintText:
+ m = "text"
+ case key.HintNumeric:
+ m = "decimal"
+ case key.HintEmail:
+ m = "email"
+ case key.HintURL:
+ m = "url"
+ case key.HintTelephone:
+ m = "tel"
+ default:
+ m = "text"
+ }
+ w.tarea.Set("inputMode", m)
+}
+
+func (w *window) keyEvent(e js.Value, ks key.State) {
+ k := e.Get("key").String()
+ if n, ok := translateKey(k); ok {
+ cmd := key.Event{
+ Name: n,
+ Modifiers: modifiersFor(e),
+ State: ks,
+ }
+ w.w.Event(cmd)
+ }
+}
+
+// modifiersFor returns the modifier set for a DOM MouseEvent or
+// KeyEvent.
+func modifiersFor(e js.Value) key.Modifiers {
+ var mods key.Modifiers
+ if e.Get("getModifierState").IsUndefined() {
+ // Some browsers doesn't support getModifierState.
+ return mods
+ }
+ if e.Call("getModifierState", "Alt").Bool() {
+ mods |= key.ModAlt
+ }
+ if e.Call("getModifierState", "Control").Bool() {
+ mods |= key.ModCtrl
+ }
+ if e.Call("getModifierState", "Shift").Bool() {
+ mods |= key.ModShift
+ }
+ return mods
+}
+
+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")
+ scale := w.scale
+ var mods key.Modifiers
+ if e.Get("shiftKey").Bool() {
+ mods |= key.ModShift
+ }
+ if e.Get("altKey").Bool() {
+ mods |= key.ModAlt
+ }
+ if e.Get("ctrlKey").Bool() {
+ mods |= key.ModCtrl
+ }
+ 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,
+ Modifiers: mods,
+ })
+ }
+}
+
+func (w *window) touchIDFor(touch js.Value) pointer.ID {
+ id := touch.Get("identifier")
+ for i, id2 := range w.touches {
+ if id2.Equal(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()
+ scale := w.scale
+ 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
+ jbtns := e.Get("buttons").Int()
+ var btns pointer.Buttons
+ if jbtns&1 != 0 {
+ btns |= pointer.ButtonPrimary
+ }
+ if jbtns&2 != 0 {
+ btns |= pointer.ButtonSecondary
+ }
+ if jbtns&4 != 0 {
+ btns |= pointer.ButtonTertiary
+ }
+ w.w.Event(pointer.Event{
+ Type: typ,
+ Source: pointer.Mouse,
+ Buttons: btns,
+ Position: pos,
+ Scroll: scroll,
+ Time: t,
+ Modifiers: modifiersFor(e),
+ })
+}
+
+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 during cleanup.
+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() {
+ anim := w.animating
+ w.animRequested = anim
+ if anim {
+ w.requestAnimationFrame.Invoke(w.redraw)
+ }
+ if anim {
+ w.draw(false)
+ }
+}
+
+func (w *window) SetAnimating(anim bool) {
+ w.animating = anim
+ if anim && !w.animRequested {
+ w.animRequested = true
+ w.requestAnimationFrame.Invoke(w.redraw)
+ }
+}
+
+func (w *window) ReadClipboard() {
+ if w.clipboard.IsUndefined() {
+ return
+ }
+ if w.clipboard.Get("readText").IsUndefined() {
+ return
+ }
+ w.clipboard.Call("readText", w.clipboard).Call("then", w.clipboardCallback)
+}
+
+func (w *window) WriteClipboard(s string) {
+ if w.clipboard.IsUndefined() {
+ return
+ }
+ if w.clipboard.Get("writeText").IsUndefined() {
+ return
+ }
+ w.clipboard.Call("writeText", s)
+}
+
+func (w *window) Configure(options []Option) {
+ prev := w.config
+ cnf := w.config
+ cnf.apply(unit.Metric{}, options)
+ if prev.Title != cnf.Title {
+ w.config.Title = cnf.Title
+ w.document.Set("title", cnf.Title)
+ }
+ if prev.Mode != cnf.Mode {
+ w.windowMode(cnf.Mode)
+ }
+ if prev.NavigationColor != cnf.NavigationColor {
+ w.config.NavigationColor = cnf.NavigationColor
+ w.navigationColor(cnf.NavigationColor)
+ }
+ if prev.Orientation != cnf.Orientation {
+ w.config.Orientation = cnf.Orientation
+ w.orientation(cnf.Orientation)
+ }
+ if w.config != prev {
+ w.w.Event(ConfigEvent{Config: w.config})
+ }
+}
+
+func (w *window) Raise() {}
+
+func (w *window) SetCursor(name pointer.CursorName) {
+ style := w.cnv.Get("style")
+ style.Set("cursor", string(name))
+}
+
+func (w *window) Wakeup() {
+ select {
+ case w.wakeups <- struct{}{}:
+ default:
+ }
+}
+
+func (w *window) ShowTextInput(show bool) {
+ // Run in a goroutine to avoid a deadlock if the
+ // focus change result in an event.
+ go func() {
+ if show {
+ w.focus()
+ } else {
+ w.blur()
+ }
+ }()
+}
+
+func (w *window) SetInputHint(mode key.InputHint) {
+ w.keyboard(mode)
+}
+
+// Close the window. Not implemented for js.
+func (w *window) Close() {}
+
+// Maximize the window. Not implemented for js.
+func (w *window) Maximize() {}
+
+// Center the window. Not implemented for js.
+func (w *window) Center() {}
+
+func (w *window) resize() {
+ w.scale = float32(w.window.Get("devicePixelRatio").Float())
+
+ rect := w.cnv.Call("getBoundingClientRect")
+ size := image.Point{
+ X: int(float32(rect.Get("width").Float()) * w.scale),
+ Y: int(float32(rect.Get("height").Float()) * w.scale),
+ }
+ if size != w.config.Size {
+ w.config.Size = size
+ w.w.Event(ConfigEvent{Config: w.config})
+ }
+
+ if vx, vy := w.visualViewport.Get("width"), w.visualViewport.Get("height"); !vx.IsUndefined() && !vy.IsUndefined() {
+ w.inset.X = float32(w.config.Size.X) - float32(vx.Float())*w.scale
+ w.inset.Y = float32(w.config.Size.Y) - float32(vy.Float())*w.scale
+ }
+
+ if w.config.Size.X == 0 || w.config.Size.Y == 0 {
+ return
+ }
+
+ w.cnv.Set("width", w.config.Size.X)
+ w.cnv.Set("height", w.config.Size.Y)
+}
+
+func (w *window) draw(sync bool) {
+ size, insets, metric := w.getConfig()
+ if metric == (unit.Metric{}) || size.X == 0 || size.Y == 0 {
+ return
+ }
+ w.w.Event(frameEvent{
+ FrameEvent: system.FrameEvent{
+ Now: time.Now(),
+ Size: size,
+ Insets: insets,
+ Metric: metric,
+ },
+ Sync: sync,
+ })
+}
+
+func (w *window) getConfig() (image.Point, system.Insets, unit.Metric) {
+ return image.Pt(w.config.Size.X, w.config.Size.Y), system.Insets{
+ Bottom: unit.Px(w.inset.Y),
+ Right: unit.Px(w.inset.X),
+ }, unit.Metric{
+ PxPerDp: w.scale,
+ PxPerSp: w.scale,
+ }
+}
+
+func (w *window) windowMode(mode WindowMode) {
+ switch mode {
+ case Windowed:
+ if !w.document.Get("fullscreenElement").Truthy() {
+ return // Browser is already Windowed.
+ }
+ if !w.document.Get("exitFullscreen").Truthy() {
+ return // Browser doesn't support such feature.
+ }
+ w.document.Call("exitFullscreen")
+ w.config.Mode = Windowed
+ case Fullscreen:
+ elem := w.document.Get("documentElement")
+ if !elem.Get("requestFullscreen").Truthy() {
+ return // Browser doesn't support such feature.
+ }
+ elem.Call("requestFullscreen")
+ w.config.Mode = Fullscreen
+ }
+}
+
+func (w *window) orientation(mode Orientation) {
+ if j := w.screenOrientation; !j.Truthy() || !j.Get("unlock").Truthy() || !j.Get("lock").Truthy() {
+ return // Browser don't support Screen Orientation API.
+ }
+
+ switch mode {
+ case AnyOrientation:
+ w.screenOrientation.Call("unlock")
+ case LandscapeOrientation:
+ w.screenOrientation.Call("lock", "landscape").Call("then", w.redraw)
+ case PortraitOrientation:
+ w.screenOrientation.Call("lock", "portrait").Call("then", w.redraw)
+ }
+}
+
+func (w *window) navigationColor(c color.NRGBA) {
+ theme := w.head.Call("querySelector", `meta[name="theme-color"]`)
+ if !theme.Truthy() {
+ theme = w.document.Call("createElement", "meta")
+ theme.Set("name", "theme-color")
+ w.head.Call("appendChild", theme)
+ }
+ rgba := f32color.NRGBAToRGBA(c)
+ theme.Set("content", fmt.Sprintf("#%06X", []uint8{rgba.R, rgba.G, rgba.B}))
+}
+
+func osMain() {
+ select {}
+}
+
+func translateKey(k string) (string, bool) {
+ var n string
+ 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
+ case "Tab":
+ n = key.NameTab
+ case " ":
+ n = key.NameSpace
+ case "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12":
+ n = k
+ default:
+ r, s := utf8.DecodeRuneInString(k)
+ // If there is exactly one printable character, return that.
+ if s == len(k) && unicode.IsPrint(r) {
+ return strings.ToUpper(k), true
+ }
+ return "", false
+ }
+ return n, true
+}
+
+func (_ ViewEvent) ImplementsEvent() {}