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