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