github.com/gop9/olt@v0.0.0-20200202132135-d956aad50b08/gio/app/internal/window/os_js.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package window 4 5 import ( 6 "image" 7 "strings" 8 "sync" 9 "syscall/js" 10 "time" 11 "unicode" 12 "unicode/utf8" 13 14 "github.com/gop9/olt/gio/f32" 15 "github.com/gop9/olt/gio/io/key" 16 "github.com/gop9/olt/gio/io/pointer" 17 "github.com/gop9/olt/gio/io/system" 18 ) 19 20 type window struct { 21 window js.Value 22 cnv js.Value 23 tarea js.Value 24 w Callbacks 25 redraw js.Func 26 requestAnimationFrame js.Value 27 cleanfuncs []func() 28 touches []js.Value 29 composing bool 30 31 mu sync.Mutex 32 scale float32 33 animating bool 34 } 35 36 var mainDone = make(chan struct{}) 37 38 func NewWindow(win Callbacks, opts *Options) error { 39 doc := js.Global().Get("document") 40 cont := getContainer(doc) 41 cnv := createCanvas(doc) 42 cont.Call("appendChild", cnv) 43 tarea := createTextArea(doc) 44 cont.Call("appendChild", tarea) 45 w := &window{ 46 cnv: cnv, 47 tarea: tarea, 48 window: js.Global().Get("window"), 49 } 50 w.requestAnimationFrame = w.window.Get("requestAnimationFrame") 51 w.redraw = w.funcOf(func(this js.Value, args []js.Value) interface{} { 52 w.animCallback() 53 return nil 54 }) 55 w.addEventListeners() 56 w.w = win 57 go func() { 58 w.w.SetDriver(w) 59 w.focus() 60 w.w.Event(system.StageEvent{Stage: system.StageRunning}) 61 w.draw(true) 62 select {} 63 w.cleanup() 64 close(mainDone) 65 }() 66 return nil 67 } 68 69 func getContainer(doc js.Value) js.Value { 70 cont := doc.Call("getElementById", "giowindow") 71 if !cont.IsNull() { 72 return cont 73 } 74 cont = doc.Call("createElement", "DIV") 75 doc.Get("body").Call("appendChild", cont) 76 return cont 77 } 78 79 func createTextArea(doc js.Value) js.Value { 80 tarea := doc.Call("createElement", "input") 81 style := tarea.Get("style") 82 style.Set("width", "1px") 83 style.Set("height", "1px") 84 style.Set("opacity", "0") 85 style.Set("border", "0") 86 style.Set("padding", "0") 87 tarea.Set("autocomplete", "off") 88 tarea.Set("autocorrect", "off") 89 tarea.Set("autocapitalize", "off") 90 tarea.Set("spellcheck", false) 91 return tarea 92 } 93 94 func createCanvas(doc js.Value) js.Value { 95 cnv := doc.Call("createElement", "canvas") 96 style := cnv.Get("style") 97 style.Set("position", "fixed") 98 style.Set("width", "100%") 99 style.Set("height", "100%") 100 return cnv 101 } 102 103 func (w *window) cleanup() { 104 // Cleanup in the opposite order of 105 // construction. 106 for i := len(w.cleanfuncs) - 1; i >= 0; i-- { 107 w.cleanfuncs[i]() 108 } 109 w.cleanfuncs = nil 110 } 111 112 func (w *window) addEventListeners() { 113 w.addEventListener(w.window, "resize", func(this js.Value, args []js.Value) interface{} { 114 w.draw(true) 115 return nil 116 }) 117 w.addEventListener(w.cnv, "mousemove", func(this js.Value, args []js.Value) interface{} { 118 w.pointerEvent(pointer.Move, 0, 0, args[0]) 119 return nil 120 }) 121 w.addEventListener(w.cnv, "mousedown", func(this js.Value, args []js.Value) interface{} { 122 w.pointerEvent(pointer.Press, 0, 0, args[0]) 123 return nil 124 }) 125 w.addEventListener(w.cnv, "mouseup", func(this js.Value, args []js.Value) interface{} { 126 w.pointerEvent(pointer.Release, 0, 0, args[0]) 127 return nil 128 }) 129 w.addEventListener(w.cnv, "wheel", func(this js.Value, args []js.Value) interface{} { 130 e := args[0] 131 dx, dy := e.Get("deltaX").Float(), e.Get("deltaY").Float() 132 mode := e.Get("deltaMode").Int() 133 switch mode { 134 case 0x01: // DOM_DELTA_LINE 135 dx *= 10 136 dy *= 10 137 case 0x02: // DOM_DELTA_PAGE 138 dx *= 120 139 dy *= 120 140 } 141 w.pointerEvent(pointer.Move, float32(dx), float32(dy), e) 142 return nil 143 }) 144 w.addEventListener(w.cnv, "touchstart", func(this js.Value, args []js.Value) interface{} { 145 w.touchEvent(pointer.Press, args[0]) 146 return nil 147 }) 148 w.addEventListener(w.cnv, "touchend", func(this js.Value, args []js.Value) interface{} { 149 w.touchEvent(pointer.Release, args[0]) 150 return nil 151 }) 152 w.addEventListener(w.cnv, "touchmove", func(this js.Value, args []js.Value) interface{} { 153 w.touchEvent(pointer.Move, args[0]) 154 return nil 155 }) 156 w.addEventListener(w.cnv, "touchcancel", func(this js.Value, args []js.Value) interface{} { 157 // Cancel all touches even if only one touch was cancelled. 158 for i := range w.touches { 159 w.touches[i] = js.Null() 160 } 161 w.touches = w.touches[:0] 162 w.w.Event(pointer.Event{ 163 Type: pointer.Cancel, 164 Source: pointer.Touch, 165 }) 166 return nil 167 }) 168 w.addEventListener(w.tarea, "focus", func(this js.Value, args []js.Value) interface{} { 169 w.w.Event(key.FocusEvent{Focus: true}) 170 return nil 171 }) 172 w.addEventListener(w.tarea, "blur", func(this js.Value, args []js.Value) interface{} { 173 w.w.Event(key.FocusEvent{Focus: false}) 174 return nil 175 }) 176 w.addEventListener(w.tarea, "keydown", func(this js.Value, args []js.Value) interface{} { 177 w.keyEvent(args[0]) 178 return nil 179 }) 180 w.addEventListener(w.tarea, "compositionstart", func(this js.Value, args []js.Value) interface{} { 181 w.composing = true 182 return nil 183 }) 184 w.addEventListener(w.tarea, "compositionend", func(this js.Value, args []js.Value) interface{} { 185 w.composing = false 186 w.flushInput() 187 return nil 188 }) 189 w.addEventListener(w.tarea, "input", func(this js.Value, args []js.Value) interface{} { 190 if w.composing { 191 return nil 192 } 193 w.flushInput() 194 return nil 195 }) 196 } 197 198 func (w *window) flushInput() { 199 val := w.tarea.Get("value").String() 200 w.tarea.Set("value", "") 201 w.w.Event(key.EditEvent{Text: string(val)}) 202 } 203 204 func (w *window) blur() { 205 w.tarea.Call("blur") 206 } 207 208 func (w *window) focus() { 209 w.tarea.Call("focus") 210 } 211 212 func (w *window) keyEvent(e js.Value) { 213 k := e.Get("key").String() 214 if n, ok := translateKey(k); ok { 215 cmd := key.Event{Name: n} 216 if e.Call("getModifierState", "Alt").Bool() { 217 cmd.Modifiers |= key.ModAlt 218 } 219 if e.Call("getModifierState", "Control").Bool() { 220 cmd.Modifiers |= key.ModCtrl 221 } 222 if e.Call("getModifierState", "Shift").Bool() { 223 cmd.Modifiers |= key.ModShift 224 } 225 w.w.Event(cmd) 226 } 227 } 228 229 func (w *window) touchEvent(typ pointer.Type, e js.Value) { 230 e.Call("preventDefault") 231 t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond 232 changedTouches := e.Get("changedTouches") 233 n := changedTouches.Length() 234 rect := w.cnv.Call("getBoundingClientRect") 235 w.mu.Lock() 236 scale := w.scale 237 w.mu.Unlock() 238 for i := 0; i < n; i++ { 239 touch := changedTouches.Index(i) 240 pid := w.touchIDFor(touch) 241 x, y := touch.Get("clientX").Float(), touch.Get("clientY").Float() 242 x -= rect.Get("left").Float() 243 y -= rect.Get("top").Float() 244 pos := f32.Point{ 245 X: float32(x) * scale, 246 Y: float32(y) * scale, 247 } 248 w.w.Event(pointer.Event{ 249 Type: typ, 250 Source: pointer.Touch, 251 Position: pos, 252 PointerID: pid, 253 Time: t, 254 }) 255 } 256 } 257 258 func (w *window) touchIDFor(touch js.Value) pointer.ID { 259 id := touch.Get("identifier") 260 for i, id2 := range w.touches { 261 if id2.Equal(id) { 262 return pointer.ID(i) 263 } 264 } 265 pid := pointer.ID(len(w.touches)) 266 w.touches = append(w.touches, id) 267 return pid 268 } 269 270 func (w *window) pointerEvent(typ pointer.Type, dx, dy float32, e js.Value) { 271 e.Call("preventDefault") 272 x, y := e.Get("clientX").Float(), e.Get("clientY").Float() 273 rect := w.cnv.Call("getBoundingClientRect") 274 x -= rect.Get("left").Float() 275 y -= rect.Get("top").Float() 276 w.mu.Lock() 277 scale := w.scale 278 w.mu.Unlock() 279 pos := f32.Point{ 280 X: float32(x) * scale, 281 Y: float32(y) * scale, 282 } 283 scroll := f32.Point{ 284 X: dx * scale, 285 Y: dy * scale, 286 } 287 t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond 288 jbtns := e.Get("buttons").Int() 289 var btns pointer.Buttons 290 if jbtns&1 != 0 { 291 btns |= pointer.ButtonLeft 292 } 293 if jbtns&2 != 0 { 294 btns |= pointer.ButtonRight 295 } 296 if jbtns&4 != 0 { 297 btns |= pointer.ButtonMiddle 298 } 299 w.w.Event(pointer.Event{ 300 Type: typ, 301 Source: pointer.Mouse, 302 Buttons: btns, 303 Position: pos, 304 Scroll: scroll, 305 Time: t, 306 }) 307 } 308 309 func (w *window) addEventListener(this js.Value, event string, f func(this js.Value, args []js.Value) interface{}) { 310 jsf := w.funcOf(f) 311 this.Call("addEventListener", event, jsf) 312 w.cleanfuncs = append(w.cleanfuncs, func() { 313 this.Call("removeEventListener", event, jsf) 314 }) 315 } 316 317 // funcOf is like js.FuncOf but adds the js.Func to a list of 318 // functions to be released up. 319 func (w *window) funcOf(f func(this js.Value, args []js.Value) interface{}) js.Func { 320 jsf := js.FuncOf(f) 321 w.cleanfuncs = append(w.cleanfuncs, jsf.Release) 322 return jsf 323 } 324 325 func (w *window) animCallback() { 326 w.mu.Lock() 327 anim := w.animating 328 if anim { 329 w.requestAnimationFrame.Invoke(w.redraw) 330 } 331 w.mu.Unlock() 332 if anim { 333 w.draw(false) 334 } 335 } 336 337 func (w *window) SetAnimating(anim bool) { 338 w.mu.Lock() 339 defer w.mu.Unlock() 340 if anim && !w.animating { 341 w.requestAnimationFrame.Invoke(w.redraw) 342 } 343 w.animating = anim 344 } 345 346 func (w *window) ShowTextInput(show bool) { 347 // Run in a goroutine to avoid a deadlock if the 348 // focus change result in an event. 349 go func() { 350 if show { 351 w.focus() 352 } else { 353 w.blur() 354 } 355 }() 356 } 357 358 func (w *window) draw(sync bool) { 359 width, height, scale, cfg := w.config() 360 if cfg == (config{}) || width == 0 || height == 0 { 361 return 362 } 363 w.mu.Lock() 364 w.scale = float32(scale) 365 w.mu.Unlock() 366 cfg.now = time.Now() 367 w.w.Event(FrameEvent{ 368 FrameEvent: system.FrameEvent{ 369 Size: image.Point{ 370 X: width, 371 Y: height, 372 }, 373 Config: &cfg, 374 }, 375 Sync: sync, 376 }) 377 } 378 379 func (w *window) config() (int, int, float32, config) { 380 rect := w.cnv.Call("getBoundingClientRect") 381 width, height := rect.Get("width").Float(), rect.Get("height").Float() 382 scale := w.window.Get("devicePixelRatio").Float() 383 width *= scale 384 height *= scale 385 iw, ih := int(width+.5), int(height+.5) 386 // Adjust internal size of canvas if necessary. 387 if cw, ch := w.cnv.Get("width").Int(), w.cnv.Get("height").Int(); iw != cw || ih != ch { 388 w.cnv.Set("width", iw) 389 w.cnv.Set("height", ih) 390 } 391 return iw, ih, float32(scale), config{ 392 pxPerDp: float32(scale), 393 pxPerSp: float32(scale), 394 } 395 } 396 397 func Main() { 398 <-mainDone 399 } 400 401 func translateKey(k string) (string, bool) { 402 var n string 403 switch k { 404 case "ArrowUp": 405 n = key.NameUpArrow 406 case "ArrowDown": 407 n = key.NameDownArrow 408 case "ArrowLeft": 409 n = key.NameLeftArrow 410 case "ArrowRight": 411 n = key.NameRightArrow 412 case "Escape": 413 n = key.NameEscape 414 case "Enter": 415 n = key.NameReturn 416 case "Backspace": 417 n = key.NameDeleteBackward 418 case "Delete": 419 n = key.NameDeleteForward 420 case "Home": 421 n = key.NameHome 422 case "End": 423 n = key.NameEnd 424 case "PageUp": 425 n = key.NamePageUp 426 case "PageDown": 427 n = key.NamePageDown 428 case "Tab": 429 n = key.NameTab 430 case " ": 431 n = "Space" 432 case "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12": 433 n = k 434 default: 435 r, s := utf8.DecodeRuneInString(k) 436 // If there is exactly one printable character, return that. 437 if s == len(k) && unicode.IsPrint(r) { 438 return strings.ToUpper(k), true 439 } 440 return "", false 441 } 442 return n, true 443 }