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