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() {}