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