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  }