github.com/rajveermalviya/gamen@v0.1.2-0.20220930195403-9be15877c1aa/internal/web/window.go (about) 1 //go:build js 2 3 package web 4 5 import ( 6 "sync" 7 "sync/atomic" 8 "syscall/js" 9 10 "github.com/rajveermalviya/gamen/cursors" 11 "github.com/rajveermalviya/gamen/dpi" 12 "github.com/rajveermalviya/gamen/events" 13 "github.com/rajveermalviya/gamen/internal/common/atomicx" 14 ) 15 16 var windowCounter uint64 17 18 type Window struct { 19 d *Display 20 destroyOnce sync.Once 21 22 canvas js.Value 23 listeners map[string]js.Func 24 25 currentCursorIcon string 26 27 // callbacks 28 resizedCb atomicx.Pointer[events.WindowResizedCallback] 29 closeRequestedCb atomicx.Pointer[events.WindowCloseRequestedCallback] 30 focusedCb atomicx.Pointer[events.WindowFocusedCallback] 31 unfocusedCb atomicx.Pointer[events.WindowUnfocusedCallback] 32 cursorEnteredCb atomicx.Pointer[events.WindowCursorEnteredCallback] 33 cursorLeftCb atomicx.Pointer[events.WindowCursorLeftCallback] 34 cursorMovedCb atomicx.Pointer[events.WindowCursorMovedCallback] 35 mouseWheelCb atomicx.Pointer[events.WindowMouseScrollCallback] 36 mouseInputCb atomicx.Pointer[events.WindowMouseInputCallback] 37 modifiersChangedCb atomicx.Pointer[events.WindowModifiersChangedCallback] 38 keyboardInputCb atomicx.Pointer[events.WindowKeyboardInputCallback] 39 receivedCharacterCb atomicx.Pointer[events.WindowReceivedCharacterCallback] 40 } 41 42 func NewWindow(d *Display) (*Window, error) { 43 id := atomic.AddUint64(&windowCounter, 1) 44 45 document := js.Global().Get("document") 46 canvas := document.Call("createElement", "canvas") 47 canvas.Call("setAttribute", "tabindex", "0") 48 49 setCanvasSize(canvas, dpi.LogicalSize[float64]{ 50 Width: 640, 51 Height: 480, 52 }) 53 54 w := &Window{ 55 d: d, 56 canvas: canvas, 57 currentCursorIcon: "auto", 58 listeners: make(map[string]js.Func, 11), 59 } 60 61 setHandlers(w) 62 63 d.windows[id] = w 64 return w, nil 65 } 66 67 func (w *Window) Destroy() { 68 w.destroyOnce.Do(func() { 69 w.resizedCb.Store(nil) 70 w.closeRequestedCb.Store(nil) 71 w.focusedCb.Store(nil) 72 w.unfocusedCb.Store(nil) 73 w.cursorEnteredCb.Store(nil) 74 w.cursorLeftCb.Store(nil) 75 w.cursorMovedCb.Store(nil) 76 w.mouseWheelCb.Store(nil) 77 w.mouseInputCb.Store(nil) 78 w.modifiersChangedCb.Store(nil) 79 w.keyboardInputCb.Store(nil) 80 w.receivedCharacterCb.Store(nil) 81 82 for event, listener := range w.listeners { 83 w.canvas.Call("removeEventListener", event, listener) 84 listener.Release() 85 } 86 w.listeners = nil 87 88 w.canvas.Call("remove") 89 }) 90 } 91 92 func (w *Window) WebCanvas() js.Value { return w.canvas } 93 94 func (w *Window) SetTitle(title string) { 95 js.Global().Get("document").Set("title", title) 96 } 97 98 func (w *Window) InnerSize() dpi.PhysicalSize[uint32] { 99 return dpi.PhysicalSize[uint32]{ 100 Width: uint32(w.canvas.Get("width").Int()), 101 Height: uint32(w.canvas.Get("height").Int()), 102 } 103 } 104 105 func (w *Window) SetInnerSize(size dpi.Size[uint32]) { 106 old := w.InnerSize() 107 setCanvasSize(w.canvas, dpi.CastSize[uint32, float64](size)) 108 new := w.InnerSize() 109 110 if old != new { 111 go func() { 112 w.d.eventCallbacksChan <- func() { 113 if cb := w.resizedCb.Load(); cb != nil { 114 if cb := (*cb); cb != nil { 115 cb(new.Width, new.Height, scaleFactor()) 116 } 117 } 118 } 119 }() 120 } 121 } 122 123 func (w *Window) SetCursorIcon(icon cursors.Icon) { 124 if icon == cursors.Default { 125 w.currentCursorIcon = "auto" 126 } else { 127 w.currentCursorIcon = icon.String() 128 } 129 130 w.canvas.Get("style").Call("setProperty", "cursor", w.currentCursorIcon) 131 } 132 133 func (w *Window) SetCursorVisible(visible bool) { 134 var icon string 135 if visible { 136 icon = w.currentCursorIcon 137 } else { 138 icon = "none" 139 } 140 141 w.canvas.Get("style").Call("setProperty", "cursor", icon) 142 } 143 144 func (w *Window) SetFullscreen(fullscreen bool) { 145 if fullscreen { 146 w.canvas.Call("requestFullscreen") 147 } else { 148 js.Global().Get("document").Call("exitFullscreen") 149 } 150 } 151 func (w *Window) Fullscreen() bool { 152 el := js.Global().Get("document").Get("fullscreenElement") 153 if w.canvas.Equal(el) { 154 return true 155 } 156 return false 157 } 158 159 func (*Window) SetMinInnerSize(dpi.Size[uint32]) {} 160 func (*Window) SetMaxInnerSize(dpi.Size[uint32]) {} 161 func (*Window) Maximized() bool { return false } 162 func (*Window) SetMinimized() {} 163 func (*Window) SetMaximized(bool) {} 164 func (*Window) DragWindow() {} 165 func (*Window) SetDecorations(bool) {} 166 func (*Window) Decorated() bool { return false } 167 168 func (w *Window) addListener(eventName string, f func(event js.Value)) { 169 listener := js.FuncOf(func(this js.Value, args []js.Value) any { 170 if len(args) == 0 { 171 return js.Undefined() 172 } 173 174 event := args[0] 175 event.Call("stopPropagation") 176 177 f(event) 178 179 return js.Undefined() 180 }) 181 182 w.canvas.Call("addEventListener", eventName, listener) 183 184 w.listeners[eventName] = listener 185 } 186 187 func setHandlers(w *Window) { 188 w.addListener("blur", func(event js.Value) { 189 w.d.eventCallbacksChan <- func() { 190 if cb := w.unfocusedCb.Load(); cb != nil { 191 if cb := (*cb); cb != nil { 192 cb() 193 } 194 } 195 } 196 }) 197 198 w.addListener("focus", func(event js.Value) { 199 w.d.eventCallbacksChan <- func() { 200 if cb := w.focusedCb.Load(); cb != nil { 201 if cb := (*cb); cb != nil { 202 cb() 203 } 204 } 205 } 206 }) 207 208 w.addListener("pointerover", func(event js.Value) { 209 w.d.eventCallbacksChan <- func() { 210 if cb := w.cursorEnteredCb.Load(); cb != nil { 211 if cb := (*cb); cb != nil { 212 cb() 213 } 214 } 215 } 216 }) 217 218 w.addListener("pointerout", func(event js.Value) { 219 w.d.eventCallbacksChan <- func() { 220 if cb := w.cursorLeftCb.Load(); cb != nil { 221 if cb := (*cb); cb != nil { 222 cb() 223 } 224 } 225 } 226 }) 227 228 w.addListener("pointermove", func(event js.Value) { 229 physicalPosition := dpi.LogicalPosition[float64]{ 230 X: event.Get("offsetX").Float(), 231 Y: event.Get("offsetY").Float(), 232 }.ToPhysical(scaleFactor()) 233 234 w.d.eventCallbacksChan <- func() { 235 if cb := w.cursorMovedCb.Load(); cb != nil { 236 if cb := (*cb); cb != nil { 237 cb(physicalPosition.X, physicalPosition.Y) 238 } 239 } 240 } 241 }) 242 243 w.addListener("wheel", func(event js.Value) { 244 event.Call("preventDefault") 245 246 const ( 247 DOM_DELTA_PIXEL = 0x00 248 DOM_DELTA_LINE = 0x01 249 DOM_DELTA_PAGE = 0x02 250 ) 251 252 var delta events.MouseScrollDelta 253 var axis events.MouseScrollAxis 254 var value float64 255 256 switch event.Get("deltaMode").Int() { 257 case DOM_DELTA_PIXEL: 258 delta = events.MouseScrollDeltaPixel 259 260 x := event.Get("deltaX").Float() 261 y := event.Get("deltaY").Float() 262 263 if x != 0 { 264 axis = events.MouseScrollAxisHorizontal 265 value = scaleFactor() * -x 266 } else if y != 0 { 267 axis = events.MouseScrollAxisVertical 268 value = scaleFactor() * -y 269 } 270 271 case DOM_DELTA_LINE: 272 delta = events.MouseScrollDeltaLine 273 274 x := event.Get("deltaX").Float() 275 y := event.Get("deltaY").Float() 276 277 if x != 0 { 278 axis = events.MouseScrollAxisHorizontal 279 value = -x 280 } else if y != 0 { 281 axis = events.MouseScrollAxisVertical 282 value = -y 283 } 284 285 default: 286 return 287 } 288 289 w.d.eventCallbacksChan <- func() { 290 if cb := w.mouseWheelCb.Load(); cb != nil { 291 if cb := (*cb); cb != nil { 292 cb(delta, axis, value) 293 } 294 } 295 } 296 }) 297 298 w.addListener("pointerdown", func(event js.Value) { 299 physicalPosition := dpi.LogicalPosition[float64]{ 300 X: event.Get("offsetX").Float(), 301 Y: event.Get("offsetY").Float(), 302 }.ToPhysical(scaleFactor()) 303 304 var button events.MouseButton 305 306 i := event.Get("button").Int() 307 switch i { 308 case 0: 309 button = events.MouseButtonLeft 310 case 1: 311 button = events.MouseButtonMiddle 312 case 2: 313 button = events.MouseButtonRight 314 315 default: 316 button = events.MouseButton(i - 3) 317 } 318 319 w.d.eventCallbacksChan <- func() { 320 if cb := w.cursorMovedCb.Load(); cb != nil { 321 if cb := (*cb); cb != nil { 322 cb(physicalPosition.X, physicalPosition.Y) 323 } 324 } 325 if cb := w.mouseInputCb.Load(); cb != nil { 326 if cb := (*cb); cb != nil { 327 cb(events.ButtonStatePressed, button) 328 } 329 } 330 } 331 }) 332 333 w.addListener("pointerup", func(event js.Value) { 334 var button events.MouseButton 335 336 i := event.Get("button").Int() 337 switch i { 338 case 0: 339 button = events.MouseButtonLeft 340 case 1: 341 button = events.MouseButtonMiddle 342 case 2: 343 button = events.MouseButtonRight 344 345 default: 346 button = events.MouseButton(i - 3) 347 } 348 349 w.d.eventCallbacksChan <- func() { 350 if cb := w.mouseInputCb.Load(); cb != nil { 351 if cb := (*cb); cb != nil { 352 cb(events.ButtonStateReleased, button) 353 } 354 } 355 } 356 }) 357 358 w.addListener("keydown", func(event js.Value) { 359 eventKey := event.Get("key").String() 360 isKeyString := len(eventKey) == 1 || !isASCII(eventKey) 361 isShortcutModifiers := (event.Get("ctrlKey").Bool() || event.Get("altKey").Bool()) && 362 !event.Call("getModifierState", "AltGr").Bool() 363 364 if !isKeyString || isShortcutModifiers { 365 event.Call("preventDefault") 366 } 367 368 scanCode := event.Get("keyCode").Int() 369 if scanCode == 0 { 370 scanCode = event.Get("charCode").Int() 371 } 372 373 w.d.eventCallbacksChan <- func() { 374 vKey, ok := mapKeyCode(event.Get("code").String()) 375 if !ok { 376 vKey = events.VirtualKey(scanCode) 377 } 378 379 if cb := w.keyboardInputCb.Load(); cb != nil { 380 if cb := (*cb); cb != nil { 381 cb( 382 events.ButtonStatePressed, 383 events.ScanCode(scanCode), 384 vKey, 385 ) 386 } 387 } 388 } 389 }) 390 391 w.addListener("keyup", func(event js.Value) { 392 event.Call("preventDefault") 393 394 scanCode := event.Get("keyCode").Int() 395 if scanCode == 0 { 396 scanCode = event.Get("charCode").Int() 397 } 398 399 w.d.eventCallbacksChan <- func() { 400 vKey, ok := mapKeyCode(event.Get("code").String()) 401 if !ok { 402 vKey = events.VirtualKey(scanCode) 403 } 404 405 if cb := w.keyboardInputCb.Load(); cb != nil { 406 if cb := (*cb); cb != nil { 407 cb( 408 events.ButtonStateReleased, 409 events.ScanCode(scanCode), 410 vKey, 411 ) 412 } 413 } 414 } 415 }) 416 417 w.addListener("keypress", func(event js.Value) { 418 event.Call("preventDefault") 419 420 key := []rune(event.Get("key").String()) 421 if len(key) == 0 { 422 return 423 } 424 char := key[0] 425 426 w.d.eventCallbacksChan <- func() { 427 if cb := w.receivedCharacterCb.Load(); cb != nil { 428 if cb := (*cb); cb != nil { 429 cb(char) 430 } 431 } 432 } 433 }) 434 } 435 436 func (w *Window) SetCloseRequestedCallback(cb events.WindowCloseRequestedCallback) { 437 w.closeRequestedCb.Store(&cb) 438 } 439 func (w *Window) SetResizedCallback(cb events.WindowResizedCallback) { 440 w.resizedCb.Store(&cb) 441 } 442 func (w *Window) SetFocusedCallback(cb events.WindowFocusedCallback) { 443 w.focusedCb.Store(&cb) 444 } 445 func (w *Window) SetUnfocusedCallback(cb events.WindowUnfocusedCallback) { 446 w.unfocusedCb.Store(&cb) 447 } 448 func (w *Window) SetCursorEnteredCallback(cb events.WindowCursorEnteredCallback) { 449 w.cursorEnteredCb.Store(&cb) 450 } 451 func (w *Window) SetCursorLeftCallback(cb events.WindowCursorLeftCallback) { 452 w.cursorLeftCb.Store(&cb) 453 } 454 func (w *Window) SetCursorMovedCallback(cb events.WindowCursorMovedCallback) { 455 w.cursorMovedCb.Store(&cb) 456 } 457 func (w *Window) SetMouseScrollCallback(cb events.WindowMouseScrollCallback) { 458 w.mouseWheelCb.Store(&cb) 459 } 460 func (w *Window) SetMouseInputCallback(cb events.WindowMouseInputCallback) { 461 w.mouseInputCb.Store(&cb) 462 } 463 func (w *Window) SetTouchInputCallback(cb events.WindowTouchInputCallback) { 464 // TODO: 465 } 466 func (w *Window) SetModifiersChangedCallback(cb events.WindowModifiersChangedCallback) { 467 w.modifiersChangedCb.Store(&cb) 468 } 469 func (w *Window) SetKeyboardInputCallback(cb events.WindowKeyboardInputCallback) { 470 w.keyboardInputCb.Store(&cb) 471 } 472 func (w *Window) SetReceivedCharacterCallback(cb events.WindowReceivedCharacterCallback) { 473 w.receivedCharacterCb.Store(&cb) 474 }