github.com/Seikaijyu/gio@v0.0.1/app/os_js.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  package app
     4  
     5  import (
     6  	"fmt"
     7  	"image"
     8  	"image/color"
     9  	"strings"
    10  	"syscall/js"
    11  	"time"
    12  	"unicode"
    13  	"unicode/utf8"
    14  
    15  	"github.com/Seikaijyu/gio/internal/f32color"
    16  
    17  	"github.com/Seikaijyu/gio/f32"
    18  	"github.com/Seikaijyu/gio/io/clipboard"
    19  	"github.com/Seikaijyu/gio/io/key"
    20  	"github.com/Seikaijyu/gio/io/pointer"
    21  	"github.com/Seikaijyu/gio/io/system"
    22  	"github.com/Seikaijyu/gio/unit"
    23  )
    24  
    25  type ViewEvent struct {
    26  	Element js.Value
    27  }
    28  
    29  type contextStatus int
    30  
    31  const (
    32  	contextStatusOkay contextStatus = iota
    33  	contextStatusLost
    34  	contextStatusRestored
    35  )
    36  
    37  type window struct {
    38  	window                js.Value
    39  	document              js.Value
    40  	head                  js.Value
    41  	clipboard             js.Value
    42  	cnv                   js.Value
    43  	tarea                 js.Value
    44  	w                     *callbacks
    45  	redraw                js.Func
    46  	clipboardCallback     js.Func
    47  	requestAnimationFrame js.Value
    48  	browserHistory        js.Value
    49  	visualViewport        js.Value
    50  	screenOrientation     js.Value
    51  	cleanfuncs            []func()
    52  	touches               []js.Value
    53  	composing             bool
    54  	requestFocus          bool
    55  
    56  	chanAnimation chan struct{}
    57  	chanRedraw    chan struct{}
    58  
    59  	config    Config
    60  	inset     f32.Point
    61  	scale     float32
    62  	animating bool
    63  	// animRequested tracks whether a requestAnimationFrame callback
    64  	// is pending.
    65  	animRequested bool
    66  	wakeups       chan struct{}
    67  
    68  	contextStatus contextStatus
    69  }
    70  
    71  func newWindow(win *callbacks, options []Option) error {
    72  	doc := js.Global().Get("document")
    73  	cont := getContainer(doc)
    74  	cnv := createCanvas(doc)
    75  	cont.Call("appendChild", cnv)
    76  	tarea := createTextArea(doc)
    77  	cont.Call("appendChild", tarea)
    78  	w := &window{
    79  		cnv:       cnv,
    80  		document:  doc,
    81  		tarea:     tarea,
    82  		window:    js.Global().Get("window"),
    83  		head:      doc.Get("head"),
    84  		clipboard: js.Global().Get("navigator").Get("clipboard"),
    85  		wakeups:   make(chan struct{}, 1),
    86  	}
    87  	w.requestAnimationFrame = w.window.Get("requestAnimationFrame")
    88  	w.browserHistory = w.window.Get("history")
    89  	w.visualViewport = w.window.Get("visualViewport")
    90  	if w.visualViewport.IsUndefined() {
    91  		w.visualViewport = w.window
    92  	}
    93  	if screen := w.window.Get("screen"); screen.Truthy() {
    94  		w.screenOrientation = screen.Get("orientation")
    95  	}
    96  	w.chanAnimation = make(chan struct{}, 1)
    97  	w.chanRedraw = make(chan struct{}, 1)
    98  	w.redraw = w.funcOf(func(this js.Value, args []js.Value) interface{} {
    99  		w.chanAnimation <- struct{}{}
   100  		return nil
   101  	})
   102  	w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} {
   103  		content := args[0].String()
   104  		go win.Event(clipboard.Event{Text: content})
   105  		return nil
   106  	})
   107  	w.addEventListeners()
   108  	w.addHistory()
   109  	w.w = win
   110  
   111  	go func() {
   112  		defer w.cleanup()
   113  		w.w.SetDriver(w)
   114  		w.Configure(options)
   115  		w.blur()
   116  		w.w.Event(ViewEvent{Element: cont})
   117  		w.w.Event(system.StageEvent{Stage: system.StageRunning})
   118  		w.resize()
   119  		w.draw(true)
   120  		for {
   121  			select {
   122  			case <-w.wakeups:
   123  				w.w.Event(wakeupEvent{})
   124  			case <-w.chanAnimation:
   125  				w.animCallback()
   126  			case <-w.chanRedraw:
   127  				w.draw(true)
   128  			}
   129  		}
   130  	}()
   131  	return nil
   132  }
   133  
   134  func getContainer(doc js.Value) js.Value {
   135  	cont := doc.Call("getElementById", "giowindow")
   136  	if !cont.IsNull() {
   137  		return cont
   138  	}
   139  	cont = doc.Call("createElement", "DIV")
   140  	doc.Get("body").Call("appendChild", cont)
   141  	return cont
   142  }
   143  
   144  func createTextArea(doc js.Value) js.Value {
   145  	tarea := doc.Call("createElement", "input")
   146  	style := tarea.Get("style")
   147  	style.Set("width", "1px")
   148  	style.Set("height", "1px")
   149  	style.Set("opacity", "0")
   150  	style.Set("border", "0")
   151  	style.Set("padding", "0")
   152  	tarea.Set("autocomplete", "off")
   153  	tarea.Set("autocorrect", "off")
   154  	tarea.Set("autocapitalize", "off")
   155  	tarea.Set("spellcheck", false)
   156  	return tarea
   157  }
   158  
   159  func createCanvas(doc js.Value) js.Value {
   160  	cnv := doc.Call("createElement", "canvas")
   161  	style := cnv.Get("style")
   162  	style.Set("position", "fixed")
   163  	style.Set("width", "100%")
   164  	style.Set("height", "100%")
   165  	return cnv
   166  }
   167  
   168  func (w *window) cleanup() {
   169  	// Cleanup in the opposite order of
   170  	// construction.
   171  	for i := len(w.cleanfuncs) - 1; i >= 0; i-- {
   172  		w.cleanfuncs[i]()
   173  	}
   174  	w.cleanfuncs = nil
   175  }
   176  
   177  func (w *window) addEventListeners() {
   178  	w.addEventListener(w.cnv, "webglcontextlost", func(this js.Value, args []js.Value) interface{} {
   179  		args[0].Call("preventDefault")
   180  		w.contextStatus = contextStatusLost
   181  		return nil
   182  	})
   183  	w.addEventListener(w.cnv, "webglcontextrestored", func(this js.Value, args []js.Value) interface{} {
   184  		args[0].Call("preventDefault")
   185  		w.contextStatus = contextStatusRestored
   186  
   187  		// Resize is required to force update the canvas content when restored.
   188  		w.cnv.Set("width", 0)
   189  		w.cnv.Set("height", 0)
   190  		w.resize()
   191  		w.requestRedraw()
   192  		return nil
   193  	})
   194  	w.addEventListener(w.visualViewport, "resize", func(this js.Value, args []js.Value) interface{} {
   195  		w.resize()
   196  		w.requestRedraw()
   197  		return nil
   198  	})
   199  	w.addEventListener(w.window, "contextmenu", func(this js.Value, args []js.Value) interface{} {
   200  		args[0].Call("preventDefault")
   201  		return nil
   202  	})
   203  	w.addEventListener(w.window, "popstate", func(this js.Value, args []js.Value) interface{} {
   204  		if w.w.Event(key.Event{Name: key.NameBack}) {
   205  			return w.browserHistory.Call("forward")
   206  		}
   207  		return w.browserHistory.Call("back")
   208  	})
   209  	w.addEventListener(w.document, "visibilitychange", func(this js.Value, args []js.Value) interface{} {
   210  		ev := system.StageEvent{}
   211  		switch w.document.Get("visibilityState").String() {
   212  		case "hidden", "prerender", "unloaded":
   213  			ev.Stage = system.StagePaused
   214  		default:
   215  			ev.Stage = system.StageRunning
   216  		}
   217  		w.w.Event(ev)
   218  		return nil
   219  	})
   220  	w.addEventListener(w.cnv, "mousemove", func(this js.Value, args []js.Value) interface{} {
   221  		w.pointerEvent(pointer.Move, 0, 0, args[0])
   222  		return nil
   223  	})
   224  	w.addEventListener(w.cnv, "mousedown", func(this js.Value, args []js.Value) interface{} {
   225  		w.pointerEvent(pointer.Press, 0, 0, args[0])
   226  		if w.requestFocus {
   227  			w.focus()
   228  			w.requestFocus = false
   229  		}
   230  		return nil
   231  	})
   232  	w.addEventListener(w.cnv, "mouseup", func(this js.Value, args []js.Value) interface{} {
   233  		w.pointerEvent(pointer.Release, 0, 0, args[0])
   234  		return nil
   235  	})
   236  	w.addEventListener(w.cnv, "wheel", func(this js.Value, args []js.Value) interface{} {
   237  		e := args[0]
   238  		dx, dy := e.Get("deltaX").Float(), e.Get("deltaY").Float()
   239  		// horizontal scroll if shift is pressed.
   240  		if e.Get("shiftKey").Bool() {
   241  			dx, dy = dy, dx
   242  		}
   243  		mode := e.Get("deltaMode").Int()
   244  		switch mode {
   245  		case 0x01: // DOM_DELTA_LINE
   246  			dx *= 10
   247  			dy *= 10
   248  		case 0x02: // DOM_DELTA_PAGE
   249  			dx *= 120
   250  			dy *= 120
   251  		}
   252  		w.pointerEvent(pointer.Scroll, float32(dx), float32(dy), e)
   253  		return nil
   254  	})
   255  	w.addEventListener(w.cnv, "touchstart", func(this js.Value, args []js.Value) interface{} {
   256  		w.touchEvent(pointer.Press, args[0])
   257  		if w.requestFocus {
   258  			w.focus() // iOS can only focus inside a Touch event.
   259  			w.requestFocus = false
   260  		}
   261  		return nil
   262  	})
   263  	w.addEventListener(w.cnv, "touchend", func(this js.Value, args []js.Value) interface{} {
   264  		w.touchEvent(pointer.Release, args[0])
   265  		return nil
   266  	})
   267  	w.addEventListener(w.cnv, "touchmove", func(this js.Value, args []js.Value) interface{} {
   268  		w.touchEvent(pointer.Move, args[0])
   269  		return nil
   270  	})
   271  	w.addEventListener(w.cnv, "touchcancel", func(this js.Value, args []js.Value) interface{} {
   272  		// Cancel all touches even if only one touch was cancelled.
   273  		for i := range w.touches {
   274  			w.touches[i] = js.Null()
   275  		}
   276  		w.touches = w.touches[:0]
   277  		w.w.Event(pointer.Event{
   278  			Kind:   pointer.Cancel,
   279  			Source: pointer.Touch,
   280  		})
   281  		return nil
   282  	})
   283  	w.addEventListener(w.tarea, "focus", func(this js.Value, args []js.Value) interface{} {
   284  		w.w.Event(key.FocusEvent{Focus: true})
   285  		return nil
   286  	})
   287  	w.addEventListener(w.tarea, "blur", func(this js.Value, args []js.Value) interface{} {
   288  		w.w.Event(key.FocusEvent{Focus: false})
   289  		w.blur()
   290  		return nil
   291  	})
   292  	w.addEventListener(w.tarea, "keydown", func(this js.Value, args []js.Value) interface{} {
   293  		w.keyEvent(args[0], key.Press)
   294  		return nil
   295  	})
   296  	w.addEventListener(w.tarea, "keyup", func(this js.Value, args []js.Value) interface{} {
   297  		w.keyEvent(args[0], key.Release)
   298  		return nil
   299  	})
   300  	w.addEventListener(w.tarea, "compositionstart", func(this js.Value, args []js.Value) interface{} {
   301  		w.composing = true
   302  		return nil
   303  	})
   304  	w.addEventListener(w.tarea, "compositionend", func(this js.Value, args []js.Value) interface{} {
   305  		w.composing = false
   306  		w.flushInput()
   307  		return nil
   308  	})
   309  	w.addEventListener(w.tarea, "input", func(this js.Value, args []js.Value) interface{} {
   310  		if w.composing {
   311  			return nil
   312  		}
   313  		w.flushInput()
   314  		return nil
   315  	})
   316  	w.addEventListener(w.tarea, "paste", func(this js.Value, args []js.Value) interface{} {
   317  		if w.clipboard.IsUndefined() {
   318  			return nil
   319  		}
   320  		// Prevents duplicated-paste, since "paste" is already handled through Clipboard API.
   321  		args[0].Call("preventDefault")
   322  		return nil
   323  	})
   324  }
   325  
   326  func (w *window) addHistory() {
   327  	w.browserHistory.Call("pushState", nil, nil, w.window.Get("location").Get("href"))
   328  }
   329  
   330  func (w *window) flushInput() {
   331  	val := w.tarea.Get("value").String()
   332  	w.tarea.Set("value", "")
   333  	w.w.EditorInsert(string(val))
   334  }
   335  
   336  func (w *window) blur() {
   337  	w.tarea.Call("blur")
   338  	w.requestFocus = false
   339  }
   340  
   341  func (w *window) focus() {
   342  	w.tarea.Call("focus")
   343  	w.requestFocus = true
   344  }
   345  
   346  func (w *window) keyboard(hint key.InputHint) {
   347  	var m string
   348  	switch hint {
   349  	case key.HintAny:
   350  		m = "text"
   351  	case key.HintText:
   352  		m = "text"
   353  	case key.HintNumeric:
   354  		m = "decimal"
   355  	case key.HintEmail:
   356  		m = "email"
   357  	case key.HintURL:
   358  		m = "url"
   359  	case key.HintTelephone:
   360  		m = "tel"
   361  	case key.HintPassword:
   362  		m = "password"
   363  	default:
   364  		m = "text"
   365  	}
   366  	w.tarea.Set("inputMode", m)
   367  }
   368  
   369  func (w *window) keyEvent(e js.Value, ks key.State) {
   370  	k := e.Get("key").String()
   371  	if n, ok := translateKey(k); ok {
   372  		cmd := key.Event{
   373  			Name:      n,
   374  			Modifiers: modifiersFor(e),
   375  			State:     ks,
   376  		}
   377  		w.w.Event(cmd)
   378  	}
   379  }
   380  
   381  // modifiersFor returns the modifier set for a DOM MouseEvent or
   382  // KeyEvent.
   383  func modifiersFor(e js.Value) key.Modifiers {
   384  	var mods key.Modifiers
   385  	if e.Get("getModifierState").IsUndefined() {
   386  		// Some browsers doesn't support getModifierState.
   387  		return mods
   388  	}
   389  	if e.Call("getModifierState", "Alt").Bool() {
   390  		mods |= key.ModAlt
   391  	}
   392  	if e.Call("getModifierState", "Control").Bool() {
   393  		mods |= key.ModCtrl
   394  	}
   395  	if e.Call("getModifierState", "Shift").Bool() {
   396  		mods |= key.ModShift
   397  	}
   398  	return mods
   399  }
   400  
   401  func (w *window) touchEvent(kind pointer.Kind, e js.Value) {
   402  	e.Call("preventDefault")
   403  	t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond
   404  	changedTouches := e.Get("changedTouches")
   405  	n := changedTouches.Length()
   406  	rect := w.cnv.Call("getBoundingClientRect")
   407  	scale := w.scale
   408  	var mods key.Modifiers
   409  	if e.Get("shiftKey").Bool() {
   410  		mods |= key.ModShift
   411  	}
   412  	if e.Get("altKey").Bool() {
   413  		mods |= key.ModAlt
   414  	}
   415  	if e.Get("ctrlKey").Bool() {
   416  		mods |= key.ModCtrl
   417  	}
   418  	for i := 0; i < n; i++ {
   419  		touch := changedTouches.Index(i)
   420  		pid := w.touchIDFor(touch)
   421  		x, y := touch.Get("clientX").Float(), touch.Get("clientY").Float()
   422  		x -= rect.Get("left").Float()
   423  		y -= rect.Get("top").Float()
   424  		pos := f32.Point{
   425  			X: float32(x) * scale,
   426  			Y: float32(y) * scale,
   427  		}
   428  		w.w.Event(pointer.Event{
   429  			Kind:      kind,
   430  			Source:    pointer.Touch,
   431  			Position:  pos,
   432  			PointerID: pid,
   433  			Time:      t,
   434  			Modifiers: mods,
   435  		})
   436  	}
   437  }
   438  
   439  func (w *window) touchIDFor(touch js.Value) pointer.ID {
   440  	id := touch.Get("identifier")
   441  	for i, id2 := range w.touches {
   442  		if id2.Equal(id) {
   443  			return pointer.ID(i)
   444  		}
   445  	}
   446  	pid := pointer.ID(len(w.touches))
   447  	w.touches = append(w.touches, id)
   448  	return pid
   449  }
   450  
   451  func (w *window) pointerEvent(kind pointer.Kind, dx, dy float32, e js.Value) {
   452  	e.Call("preventDefault")
   453  	x, y := e.Get("clientX").Float(), e.Get("clientY").Float()
   454  	rect := w.cnv.Call("getBoundingClientRect")
   455  	x -= rect.Get("left").Float()
   456  	y -= rect.Get("top").Float()
   457  	scale := w.scale
   458  	pos := f32.Point{
   459  		X: float32(x) * scale,
   460  		Y: float32(y) * scale,
   461  	}
   462  	scroll := f32.Point{
   463  		X: dx * scale,
   464  		Y: dy * scale,
   465  	}
   466  	t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond
   467  	jbtns := e.Get("buttons").Int()
   468  	var btns pointer.Buttons
   469  	if jbtns&1 != 0 {
   470  		btns |= pointer.ButtonPrimary
   471  	}
   472  	if jbtns&2 != 0 {
   473  		btns |= pointer.ButtonSecondary
   474  	}
   475  	if jbtns&4 != 0 {
   476  		btns |= pointer.ButtonTertiary
   477  	}
   478  	w.w.Event(pointer.Event{
   479  		Kind:      kind,
   480  		Source:    pointer.Mouse,
   481  		Buttons:   btns,
   482  		Position:  pos,
   483  		Scroll:    scroll,
   484  		Time:      t,
   485  		Modifiers: modifiersFor(e),
   486  	})
   487  }
   488  
   489  func (w *window) addEventListener(this js.Value, event string, f func(this js.Value, args []js.Value) interface{}) {
   490  	jsf := w.funcOf(f)
   491  	this.Call("addEventListener", event, jsf)
   492  	w.cleanfuncs = append(w.cleanfuncs, func() {
   493  		this.Call("removeEventListener", event, jsf)
   494  	})
   495  }
   496  
   497  // funcOf is like js.FuncOf but adds the js.Func to a list of
   498  // functions to be released during cleanup.
   499  func (w *window) funcOf(f func(this js.Value, args []js.Value) interface{}) js.Func {
   500  	jsf := js.FuncOf(f)
   501  	w.cleanfuncs = append(w.cleanfuncs, jsf.Release)
   502  	return jsf
   503  }
   504  
   505  func (w *window) animCallback() {
   506  	anim := w.animating
   507  	w.animRequested = anim
   508  	if anim {
   509  		w.requestAnimationFrame.Invoke(w.redraw)
   510  	}
   511  	if anim {
   512  		w.draw(false)
   513  	}
   514  }
   515  
   516  func (w *window) EditorStateChanged(old, new editorState) {}
   517  
   518  func (w *window) SetAnimating(anim bool) {
   519  	w.animating = anim
   520  	if anim && !w.animRequested {
   521  		w.animRequested = true
   522  		w.requestAnimationFrame.Invoke(w.redraw)
   523  	}
   524  }
   525  
   526  func (w *window) ReadClipboard() {
   527  	if w.clipboard.IsUndefined() {
   528  		return
   529  	}
   530  	if w.clipboard.Get("readText").IsUndefined() {
   531  		return
   532  	}
   533  	w.clipboard.Call("readText", w.clipboard).Call("then", w.clipboardCallback)
   534  }
   535  
   536  func (w *window) WriteClipboard(s string) {
   537  	if w.clipboard.IsUndefined() {
   538  		return
   539  	}
   540  	if w.clipboard.Get("writeText").IsUndefined() {
   541  		return
   542  	}
   543  	w.clipboard.Call("writeText", s)
   544  }
   545  
   546  func (w *window) Configure(options []Option) {
   547  	prev := w.config
   548  	cnf := w.config
   549  	cnf.apply(unit.Metric{}, options)
   550  	// Decorations are never disabled.
   551  	cnf.Decorated = true
   552  
   553  	if prev.Title != cnf.Title {
   554  		w.config.Title = cnf.Title
   555  		w.document.Set("title", cnf.Title)
   556  	}
   557  	if prev.Mode != cnf.Mode {
   558  		w.windowMode(cnf.Mode)
   559  	}
   560  	if prev.NavigationColor != cnf.NavigationColor {
   561  		w.config.NavigationColor = cnf.NavigationColor
   562  		w.navigationColor(cnf.NavigationColor)
   563  	}
   564  	if prev.Orientation != cnf.Orientation {
   565  		w.config.Orientation = cnf.Orientation
   566  		w.orientation(cnf.Orientation)
   567  	}
   568  	if cnf.Decorated != prev.Decorated {
   569  		w.config.Decorated = cnf.Decorated
   570  	}
   571  	w.w.Event(ConfigEvent{Config: w.config})
   572  }
   573  
   574  func (w *window) Perform(system.Action) {}
   575  
   576  var webCursor = [...]string{
   577  	pointer.CursorDefault:                  "default",
   578  	pointer.CursorNone:                     "none",
   579  	pointer.CursorText:                     "text",
   580  	pointer.CursorVerticalText:             "vertical-text",
   581  	pointer.CursorPointer:                  "pointer",
   582  	pointer.CursorCrosshair:                "crosshair",
   583  	pointer.CursorAllScroll:                "all-scroll",
   584  	pointer.CursorColResize:                "col-resize",
   585  	pointer.CursorRowResize:                "row-resize",
   586  	pointer.CursorGrab:                     "grab",
   587  	pointer.CursorGrabbing:                 "grabbing",
   588  	pointer.CursorNotAllowed:               "not-allowed",
   589  	pointer.CursorWait:                     "wait",
   590  	pointer.CursorProgress:                 "progress",
   591  	pointer.CursorNorthWestResize:          "nw-resize",
   592  	pointer.CursorNorthEastResize:          "ne-resize",
   593  	pointer.CursorSouthWestResize:          "sw-resize",
   594  	pointer.CursorSouthEastResize:          "se-resize",
   595  	pointer.CursorNorthSouthResize:         "ns-resize",
   596  	pointer.CursorEastWestResize:           "ew-resize",
   597  	pointer.CursorWestResize:               "w-resize",
   598  	pointer.CursorEastResize:               "e-resize",
   599  	pointer.CursorNorthResize:              "n-resize",
   600  	pointer.CursorSouthResize:              "s-resize",
   601  	pointer.CursorNorthEastSouthWestResize: "nesw-resize",
   602  	pointer.CursorNorthWestSouthEastResize: "nwse-resize",
   603  }
   604  
   605  func (w *window) SetCursor(cursor pointer.Cursor) {
   606  	style := w.cnv.Get("style")
   607  	style.Set("cursor", webCursor[cursor])
   608  }
   609  
   610  func (w *window) Wakeup() {
   611  	select {
   612  	case w.wakeups <- struct{}{}:
   613  	default:
   614  	}
   615  }
   616  
   617  func (w *window) ShowTextInput(show bool) {
   618  	// Run in a goroutine to avoid a deadlock if the
   619  	// focus change result in an event.
   620  	go func() {
   621  		if show {
   622  			w.focus()
   623  		} else {
   624  			w.blur()
   625  		}
   626  	}()
   627  }
   628  
   629  func (w *window) SetInputHint(mode key.InputHint) {
   630  	w.keyboard(mode)
   631  }
   632  
   633  func (w *window) resize() {
   634  	w.scale = float32(w.window.Get("devicePixelRatio").Float())
   635  
   636  	rect := w.cnv.Call("getBoundingClientRect")
   637  	size := image.Point{
   638  		X: int(float32(rect.Get("width").Float()) * w.scale),
   639  		Y: int(float32(rect.Get("height").Float()) * w.scale),
   640  	}
   641  	if size != w.config.Size {
   642  		w.config.Size = size
   643  		w.w.Event(ConfigEvent{Config: w.config})
   644  	}
   645  
   646  	if vx, vy := w.visualViewport.Get("width"), w.visualViewport.Get("height"); !vx.IsUndefined() && !vy.IsUndefined() {
   647  		w.inset.X = float32(w.config.Size.X) - float32(vx.Float())*w.scale
   648  		w.inset.Y = float32(w.config.Size.Y) - float32(vy.Float())*w.scale
   649  	}
   650  
   651  	if w.config.Size.X == 0 || w.config.Size.Y == 0 {
   652  		return
   653  	}
   654  
   655  	w.cnv.Set("width", w.config.Size.X)
   656  	w.cnv.Set("height", w.config.Size.Y)
   657  }
   658  
   659  func (w *window) draw(sync bool) {
   660  	if w.contextStatus == contextStatusLost {
   661  		return
   662  	}
   663  	size, insets, metric := w.getConfig()
   664  	if metric == (unit.Metric{}) || size.X == 0 || size.Y == 0 {
   665  		return
   666  	}
   667  
   668  	w.w.Event(frameEvent{
   669  		FrameEvent: system.FrameEvent{
   670  			Now:    time.Now(),
   671  			Size:   size,
   672  			Insets: insets,
   673  			Metric: metric,
   674  		},
   675  		Sync: sync,
   676  	})
   677  }
   678  
   679  func (w *window) getConfig() (image.Point, system.Insets, unit.Metric) {
   680  	invscale := unit.Dp(1. / w.scale)
   681  	return image.Pt(w.config.Size.X, w.config.Size.Y),
   682  		system.Insets{
   683  			Bottom: unit.Dp(w.inset.Y) * invscale,
   684  			Right:  unit.Dp(w.inset.X) * invscale,
   685  		}, unit.Metric{
   686  			PxPerDp: w.scale,
   687  			PxPerSp: w.scale,
   688  		}
   689  }
   690  
   691  func (w *window) windowMode(mode WindowMode) {
   692  	switch mode {
   693  	case Windowed:
   694  		if !w.document.Get("fullscreenElement").Truthy() {
   695  			return // Browser is already Windowed.
   696  		}
   697  		if !w.document.Get("exitFullscreen").Truthy() {
   698  			return // Browser doesn't support such feature.
   699  		}
   700  		w.document.Call("exitFullscreen")
   701  		w.config.Mode = Windowed
   702  	case Fullscreen:
   703  		elem := w.document.Get("documentElement")
   704  		if !elem.Get("requestFullscreen").Truthy() {
   705  			return // Browser doesn't support such feature.
   706  		}
   707  		elem.Call("requestFullscreen")
   708  		w.config.Mode = Fullscreen
   709  	}
   710  }
   711  
   712  func (w *window) orientation(mode Orientation) {
   713  	if j := w.screenOrientation; !j.Truthy() || !j.Get("unlock").Truthy() || !j.Get("lock").Truthy() {
   714  		return // Browser don't support Screen Orientation API.
   715  	}
   716  
   717  	switch mode {
   718  	case AnyOrientation:
   719  		w.screenOrientation.Call("unlock")
   720  	case LandscapeOrientation:
   721  		w.screenOrientation.Call("lock", "landscape").Call("then", w.redraw)
   722  	case PortraitOrientation:
   723  		w.screenOrientation.Call("lock", "portrait").Call("then", w.redraw)
   724  	}
   725  }
   726  
   727  func (w *window) navigationColor(c color.NRGBA) {
   728  	theme := w.head.Call("querySelector", `meta[name="theme-color"]`)
   729  	if !theme.Truthy() {
   730  		theme = w.document.Call("createElement", "meta")
   731  		theme.Set("name", "theme-color")
   732  		w.head.Call("appendChild", theme)
   733  	}
   734  	rgba := f32color.NRGBAToRGBA(c)
   735  	theme.Set("content", fmt.Sprintf("#%06X", []uint8{rgba.R, rgba.G, rgba.B}))
   736  }
   737  
   738  func (w *window) requestRedraw() {
   739  	select {
   740  	case w.chanRedraw <- struct{}{}:
   741  	default:
   742  	}
   743  }
   744  
   745  func osMain() {
   746  	select {}
   747  }
   748  
   749  func translateKey(k string) (string, bool) {
   750  	var n string
   751  
   752  	switch k {
   753  	case "ArrowUp":
   754  		n = key.NameUpArrow
   755  	case "ArrowDown":
   756  		n = key.NameDownArrow
   757  	case "ArrowLeft":
   758  		n = key.NameLeftArrow
   759  	case "ArrowRight":
   760  		n = key.NameRightArrow
   761  	case "Escape":
   762  		n = key.NameEscape
   763  	case "Enter":
   764  		n = key.NameReturn
   765  	case "Backspace":
   766  		n = key.NameDeleteBackward
   767  	case "Delete":
   768  		n = key.NameDeleteForward
   769  	case "Home":
   770  		n = key.NameHome
   771  	case "End":
   772  		n = key.NameEnd
   773  	case "PageUp":
   774  		n = key.NamePageUp
   775  	case "PageDown":
   776  		n = key.NamePageDown
   777  	case "Tab":
   778  		n = key.NameTab
   779  	case " ":
   780  		n = key.NameSpace
   781  	case "F1":
   782  		n = key.NameF1
   783  	case "F2":
   784  		n = key.NameF2
   785  	case "F3":
   786  		n = key.NameF3
   787  	case "F4":
   788  		n = key.NameF4
   789  	case "F5":
   790  		n = key.NameF5
   791  	case "F6":
   792  		n = key.NameF6
   793  	case "F7":
   794  		n = key.NameF7
   795  	case "F8":
   796  		n = key.NameF8
   797  	case "F9":
   798  		n = key.NameF9
   799  	case "F10":
   800  		n = key.NameF10
   801  	case "F11":
   802  		n = key.NameF11
   803  	case "F12":
   804  		n = key.NameF12
   805  	case "Control":
   806  		n = key.NameCtrl
   807  	case "Shift":
   808  		n = key.NameShift
   809  	case "Alt":
   810  		n = key.NameAlt
   811  	case "OS":
   812  		n = key.NameSuper
   813  	default:
   814  		r, s := utf8.DecodeRuneInString(k)
   815  		// If there is exactly one printable character, return that.
   816  		if s == len(k) && unicode.IsPrint(r) {
   817  			return strings.ToUpper(k), true
   818  		}
   819  		return "", false
   820  	}
   821  	return n, true
   822  }
   823  
   824  func (_ ViewEvent) ImplementsEvent() {}