github.com/cybriq/giocore@v0.0.7-0.20210703034601-cfb9cb5f3900/app/internal/wm/os_js.go (about)

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