mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
993ec907be
Signed-off-by: Elias Naur <mail@eliasnaur.com>
825 lines
20 KiB
Go
825 lines
20 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"io"
|
|
"strings"
|
|
"syscall/js"
|
|
"time"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"gioui.org/internal/f32color"
|
|
"gioui.org/op"
|
|
|
|
"gioui.org/f32"
|
|
"gioui.org/io/event"
|
|
"gioui.org/io/key"
|
|
"gioui.org/io/pointer"
|
|
"gioui.org/io/system"
|
|
"gioui.org/io/transfer"
|
|
"gioui.org/unit"
|
|
)
|
|
|
|
type JSViewEvent struct {
|
|
Element js.Value
|
|
}
|
|
|
|
type contextStatus int
|
|
|
|
const (
|
|
contextStatusOkay contextStatus = iota
|
|
contextStatusLost
|
|
contextStatusRestored
|
|
)
|
|
|
|
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
|
|
|
|
config Config
|
|
inset f32.Point
|
|
scale float32
|
|
animating bool
|
|
// animRequested tracks whether a requestAnimationFrame callback
|
|
// is pending.
|
|
animRequested bool
|
|
wakeups chan struct{}
|
|
|
|
contextStatus contextStatus
|
|
}
|
|
|
|
func newWindow(win *callbacks, options []Option) {
|
|
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: win,
|
|
}
|
|
w.w.SetDriver(w)
|
|
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.redraw = w.funcOf(func(this js.Value, args []js.Value) interface{} {
|
|
w.draw(false)
|
|
return nil
|
|
})
|
|
w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} {
|
|
content := args[0].String()
|
|
w.processEvent(transfer.DataEvent{
|
|
Type: "application/text",
|
|
Open: func() io.ReadCloser {
|
|
return io.NopCloser(strings.NewReader(content))
|
|
},
|
|
})
|
|
return nil
|
|
})
|
|
w.addEventListeners()
|
|
w.addHistory()
|
|
|
|
w.Configure(options)
|
|
w.blur()
|
|
w.processEvent(JSViewEvent{Element: cont})
|
|
w.resize()
|
|
w.draw(true)
|
|
}
|
|
|
|
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.cnv, "webglcontextlost", func(this js.Value, args []js.Value) interface{} {
|
|
args[0].Call("preventDefault")
|
|
w.contextStatus = contextStatusLost
|
|
return nil
|
|
})
|
|
w.addEventListener(w.cnv, "webglcontextrestored", func(this js.Value, args []js.Value) interface{} {
|
|
args[0].Call("preventDefault")
|
|
w.contextStatus = contextStatusRestored
|
|
|
|
// Resize is required to force update the canvas content when restored.
|
|
w.cnv.Set("width", 0)
|
|
w.cnv.Set("height", 0)
|
|
w.resize()
|
|
w.draw(true)
|
|
return nil
|
|
})
|
|
w.addEventListener(w.visualViewport, "resize", func(this js.Value, args []js.Value) interface{} {
|
|
w.resize()
|
|
w.draw(true)
|
|
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{} {
|
|
if w.processEvent(key.Event{Name: key.NameBack}) {
|
|
return w.browserHistory.Call("forward")
|
|
}
|
|
return w.browserHistory.Call("back")
|
|
})
|
|
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()
|
|
// horizontal scroll if shift is pressed.
|
|
if e.Get("shiftKey").Bool() {
|
|
dx, dy = dy, dx
|
|
}
|
|
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.processEvent(pointer.Event{
|
|
Kind: pointer.Cancel,
|
|
Source: pointer.Touch,
|
|
})
|
|
return nil
|
|
})
|
|
w.addEventListener(w.tarea, "focus", func(this js.Value, args []js.Value) interface{} {
|
|
w.config.Focused = true
|
|
w.processEvent(ConfigEvent{Config: w.config})
|
|
return nil
|
|
})
|
|
w.addEventListener(w.tarea, "blur", func(this js.Value, args []js.Value) interface{} {
|
|
w.config.Focused = false
|
|
w.processEvent(ConfigEvent{Config: w.config})
|
|
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.EditorInsert(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"
|
|
case key.HintPassword:
|
|
m = "password"
|
|
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.processEvent(cmd)
|
|
}
|
|
}
|
|
|
|
func (w *window) ProcessEvent(e event.Event) {
|
|
w.processEvent(e)
|
|
}
|
|
|
|
func (w *window) processEvent(e event.Event) bool {
|
|
if !w.w.ProcessEvent(e) {
|
|
return false
|
|
}
|
|
select {
|
|
case w.wakeups <- struct{}{}:
|
|
default:
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (w *window) Event() event.Event {
|
|
for {
|
|
evt, ok := w.w.nextEvent()
|
|
if ok {
|
|
if _, destroy := evt.(DestroyEvent); destroy {
|
|
w.cleanup()
|
|
}
|
|
return evt
|
|
}
|
|
<-w.wakeups
|
|
}
|
|
}
|
|
|
|
func (w *window) Invalidate() {
|
|
w.w.Invalidate()
|
|
}
|
|
|
|
func (w *window) Run(f func()) {
|
|
f()
|
|
}
|
|
|
|
func (w *window) Frame(frame *op.Ops) {
|
|
w.w.ProcessFrame(frame, nil)
|
|
}
|
|
|
|
// 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(kind pointer.Kind, 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.processEvent(pointer.Event{
|
|
Kind: kind,
|
|
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(kind pointer.Kind, 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.processEvent(pointer.Event{
|
|
Kind: kind,
|
|
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) EditorStateChanged(old, new editorState) {}
|
|
|
|
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(mime string, s []byte) {
|
|
if w.clipboard.IsUndefined() {
|
|
return
|
|
}
|
|
if w.clipboard.Get("writeText").IsUndefined() {
|
|
return
|
|
}
|
|
w.clipboard.Call("writeText", string(s))
|
|
}
|
|
|
|
func (w *window) Configure(options []Option) {
|
|
prev := w.config
|
|
cnf := w.config
|
|
cnf.apply(unit.Metric{}, options)
|
|
// Decorations are never disabled.
|
|
cnf.Decorated = true
|
|
|
|
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 cnf.Decorated != prev.Decorated {
|
|
w.config.Decorated = cnf.Decorated
|
|
}
|
|
w.processEvent(ConfigEvent{Config: w.config})
|
|
}
|
|
|
|
func (w *window) Perform(system.Action) {}
|
|
|
|
var webCursor = [...]string{
|
|
pointer.CursorDefault: "default",
|
|
pointer.CursorNone: "none",
|
|
pointer.CursorText: "text",
|
|
pointer.CursorVerticalText: "vertical-text",
|
|
pointer.CursorPointer: "pointer",
|
|
pointer.CursorCrosshair: "crosshair",
|
|
pointer.CursorAllScroll: "all-scroll",
|
|
pointer.CursorColResize: "col-resize",
|
|
pointer.CursorRowResize: "row-resize",
|
|
pointer.CursorGrab: "grab",
|
|
pointer.CursorGrabbing: "grabbing",
|
|
pointer.CursorNotAllowed: "not-allowed",
|
|
pointer.CursorWait: "wait",
|
|
pointer.CursorProgress: "progress",
|
|
pointer.CursorNorthWestResize: "nw-resize",
|
|
pointer.CursorNorthEastResize: "ne-resize",
|
|
pointer.CursorSouthWestResize: "sw-resize",
|
|
pointer.CursorSouthEastResize: "se-resize",
|
|
pointer.CursorNorthSouthResize: "ns-resize",
|
|
pointer.CursorEastWestResize: "ew-resize",
|
|
pointer.CursorWestResize: "w-resize",
|
|
pointer.CursorEastResize: "e-resize",
|
|
pointer.CursorNorthResize: "n-resize",
|
|
pointer.CursorSouthResize: "s-resize",
|
|
pointer.CursorNorthEastSouthWestResize: "nesw-resize",
|
|
pointer.CursorNorthWestSouthEastResize: "nwse-resize",
|
|
}
|
|
|
|
func (w *window) SetCursor(cursor pointer.Cursor) {
|
|
style := w.cnv.Get("style")
|
|
style.Set("cursor", webCursor[cursor])
|
|
}
|
|
|
|
func (w *window) ShowTextInput(show bool) {
|
|
// Run in a goroutine to avoid a deadlock if the
|
|
// focus change result in an event.
|
|
if show {
|
|
w.focus()
|
|
} else {
|
|
w.blur()
|
|
}
|
|
}
|
|
|
|
func (w *window) SetInputHint(mode key.InputHint) {
|
|
w.keyboard(mode)
|
|
}
|
|
|
|
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.processEvent(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) {
|
|
if w.contextStatus == contextStatusLost {
|
|
return
|
|
}
|
|
anim := w.animating
|
|
w.animRequested = anim
|
|
if anim {
|
|
w.requestAnimationFrame.Invoke(w.redraw)
|
|
} else if !sync {
|
|
return
|
|
}
|
|
size, insets, metric := w.getConfig()
|
|
if metric == (unit.Metric{}) || size.X == 0 || size.Y == 0 {
|
|
return
|
|
}
|
|
|
|
w.processEvent(frameEvent{
|
|
FrameEvent: FrameEvent{
|
|
Now: time.Now(),
|
|
Size: size,
|
|
Insets: insets,
|
|
Metric: metric,
|
|
},
|
|
Sync: sync,
|
|
})
|
|
}
|
|
|
|
func (w *window) getConfig() (image.Point, Insets, unit.Metric) {
|
|
invscale := unit.Dp(1. / w.scale)
|
|
return image.Pt(w.config.Size.X, w.config.Size.Y),
|
|
Insets{
|
|
Bottom: unit.Dp(w.inset.Y) * invscale,
|
|
Right: unit.Dp(w.inset.X) * invscale,
|
|
}, 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) (key.Name, bool) {
|
|
var n key.Name
|
|
|
|
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":
|
|
n = key.NameF1
|
|
case "F2":
|
|
n = key.NameF2
|
|
case "F3":
|
|
n = key.NameF3
|
|
case "F4":
|
|
n = key.NameF4
|
|
case "F5":
|
|
n = key.NameF5
|
|
case "F6":
|
|
n = key.NameF6
|
|
case "F7":
|
|
n = key.NameF7
|
|
case "F8":
|
|
n = key.NameF8
|
|
case "F9":
|
|
n = key.NameF9
|
|
case "F10":
|
|
n = key.NameF10
|
|
case "F11":
|
|
n = key.NameF11
|
|
case "F12":
|
|
n = key.NameF12
|
|
case "Control":
|
|
n = key.NameCtrl
|
|
case "Shift":
|
|
n = key.NameShift
|
|
case "Alt":
|
|
n = key.NameAlt
|
|
case "OS":
|
|
n = key.NameSuper
|
|
default:
|
|
r, s := utf8.DecodeRuneInString(k)
|
|
// If there is exactly one printable character, return that.
|
|
if s == len(k) && unicode.IsPrint(r) {
|
|
return key.Name(strings.ToUpper(k)), true
|
|
}
|
|
return "", false
|
|
}
|
|
return n, true
|
|
}
|
|
|
|
func (JSViewEvent) implementsViewEvent() {}
|
|
func (JSViewEvent) ImplementsEvent() {}
|