Major improvements to UnifiedPlayer: 1. GetFrameImage() now works when paused for responsive UI updates 2. Play() method properly starts FFmpeg process 3. Frame display loop runs continuously for smooth video display 4. Disabled audio temporarily to fix video playback fundamentals 5. Simplified FFmpeg command to focus on video stream only Player now: - Generates video frames correctly - Shows video when paused - Has responsive progress tracking - Starts playback properly Next steps: Re-enable audio playback once video is stable
1099 lines
27 KiB
Go
1099 lines
27 KiB
Go
//go:build wasm
|
|
|
|
package glfw
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"runtime"
|
|
"syscall/js"
|
|
)
|
|
|
|
const (
|
|
True int = 1
|
|
False int = 0
|
|
DontCare int = -1
|
|
)
|
|
|
|
var document = js.Global().Get("document")
|
|
|
|
var contextWatcher ContextWatcher
|
|
|
|
func Init(cw ContextWatcher) error {
|
|
contextWatcher = cw
|
|
return nil
|
|
}
|
|
|
|
func Terminate() error {
|
|
return nil
|
|
}
|
|
|
|
//gocyclo:ignore
|
|
func CreateWindow(_, _ int, title string, monitor *Monitor, share *Window) (*Window, error) {
|
|
// THINK: Consider https://developer.mozilla.org/en-US/docs/Web/API/Window.open?
|
|
body := document.Get("body")
|
|
if body.Equal(js.Null()) {
|
|
body = document.Call("createElement", "body")
|
|
document.Set("body", body)
|
|
}
|
|
|
|
body.Get("style").Call("setProperty", "margin", "0")
|
|
|
|
canvas := document.Call("createElement", "canvas")
|
|
|
|
body.Call("appendChild", canvas)
|
|
|
|
// HACK: Go fullscreen /* canvas being sized asynchronously, we are using body the window inner Width/Height */?
|
|
width := js.Global().Get("innerWidth").Int()
|
|
height := js.Global().Get("innerHeight").Int()
|
|
|
|
devicePixelRatio := js.Global().Get("devicePixelRatio").Float()
|
|
canvas.Set("width", int(float64(width)*devicePixelRatio+0.5)) // Nearest non-negative int.
|
|
canvas.Set("height", int(float64(height)*devicePixelRatio+0.5)) // Nearest non-negative int.
|
|
|
|
// Use dvw and dvh if supported; otherwise, fall back to vw and vh.
|
|
style := canvas.Get("style")
|
|
setProperty := style.Get("setProperty").Call("bind", style)
|
|
setProperty.Invoke("width", "100vw")
|
|
setProperty.Invoke("width", "100dvw")
|
|
setProperty.Invoke("height", "100vh")
|
|
setProperty.Invoke("height", "100dvh")
|
|
|
|
document.Set("title", title)
|
|
|
|
// Use glfw hints.
|
|
attrs := defaultAttributes()
|
|
attrs.Alpha = (hints[AlphaBits] > 0)
|
|
if _, ok := hints[DepthBits]; ok {
|
|
attrs.Depth = (hints[DepthBits] > 0)
|
|
}
|
|
attrs.Stencil = (hints[StencilBits] > 0)
|
|
attrs.Antialias = (hints[Samples] > 0)
|
|
attrs.PremultipliedAlpha = (hints[PremultipliedAlpha] > 0)
|
|
attrs.PreserveDrawingBuffer = (hints[PreserveDrawingBuffer] > 0)
|
|
attrs.PreferLowPowerToHighPerformance = (hints[PreferLowPowerToHighPerformance] > 0)
|
|
attrs.FailIfMajorPerformanceCaveat = (hints[FailIfMajorPerformanceCaveat] > 0)
|
|
|
|
// Create GL context.
|
|
context, err := newContext(canvas, attrs)
|
|
if context.Equal(js.Value{}) {
|
|
return nil, err
|
|
}
|
|
|
|
w := &Window{
|
|
canvas: canvas,
|
|
context: context,
|
|
devicePixelRatio: devicePixelRatio,
|
|
}
|
|
|
|
if w.canvas.Get("requestPointerLock").Equal(js.Undefined()) ||
|
|
document.Get("exitPointerLock").Equal(js.Undefined()) {
|
|
|
|
w.missing.pointerLock = true
|
|
}
|
|
if w.canvas.Get("webkitRequestFullscreen").Equal(js.Undefined()) ||
|
|
document.Get("webkitExitFullscreen").Equal(js.Undefined()) {
|
|
|
|
w.missing.fullscreen = true
|
|
}
|
|
|
|
if monitor != nil {
|
|
if w.missing.fullscreen {
|
|
log.Println("warning: Fullscreen API unsupported")
|
|
} else {
|
|
w.requestFullscreen = true
|
|
}
|
|
}
|
|
|
|
w.eventHandlerCleanups = make([]func(), 0, 11)
|
|
newJsFuncFrom := func(handler func(this js.Value, args []js.Value) any) js.Func {
|
|
function := js.FuncOf(handler)
|
|
w.eventHandlerCleanups = append(w.eventHandlerCleanups, function.Release)
|
|
return function
|
|
}
|
|
|
|
addGlobalEventListener := js.Global().Get("addEventListener").Call("bind", js.Global())
|
|
|
|
addGlobalEventListener.Invoke("focus", newJsFuncFrom(func(this js.Value, args []js.Value) any {
|
|
if w.focusCallback != nil {
|
|
w.focusCallback(w, true)
|
|
}
|
|
return nil
|
|
}))
|
|
|
|
addGlobalEventListener.Invoke("blur", newJsFuncFrom(func(this js.Value, args []js.Value) any {
|
|
if w.focusCallback != nil {
|
|
w.focusCallback(w, false)
|
|
}
|
|
return nil
|
|
}))
|
|
|
|
addGlobalEventListener.Invoke("resize", newJsFuncFrom(func(this js.Value, args []js.Value) any {
|
|
// HACK: Go fullscreen?
|
|
w.devicePixelRatio = js.Global().Get("devicePixelRatio").Float()
|
|
widthScaled, heightScaled := w.GetSize()
|
|
canvas.Set("width", widthScaled)
|
|
canvas.Set("height", heightScaled)
|
|
|
|
if w.framebufferSizeCallback != nil {
|
|
// TODO: Callbacks may be blocking so they need to happen asyncronously. However,
|
|
// GLFW API promises the callbacks will occur from one thread (i.e., sequentially), so may want to do that.
|
|
widthFramebuffer, heightFramebuffer := w.GetFramebufferSize()
|
|
go w.framebufferSizeCallback(w, widthFramebuffer, heightFramebuffer)
|
|
}
|
|
if w.sizeCallback != nil {
|
|
go w.sizeCallback(w, widthScaled, heightScaled)
|
|
}
|
|
return nil
|
|
}))
|
|
|
|
addDocumentEventListener := document.Get("addEventListener").Call("bind", document)
|
|
|
|
addDocumentEventListener.Invoke("keydown", newJsFuncFrom(func(this js.Value, args []js.Value) any {
|
|
ke := args[0]
|
|
w.goFullscreenIfRequested()
|
|
|
|
action := Press
|
|
if ke.Get("repeat").Bool() {
|
|
action = Repeat
|
|
}
|
|
|
|
key := toKey(ke)
|
|
|
|
// Extend slice if needed.
|
|
neededSize := int(key) + 1
|
|
if neededSize > len(w.keys) {
|
|
w.keys = append(w.keys, make([]Action, neededSize-len(w.keys))...)
|
|
}
|
|
w.keys[key] = action
|
|
mods := toModifierKey(ke)
|
|
if w.keyCallback != nil {
|
|
go w.keyCallback(w, key, -1, action, mods)
|
|
}
|
|
|
|
if w.charCallback != nil && mods < 2 {
|
|
keyStr := ke.Get("key").String()
|
|
if len(keyStr) == 1 {
|
|
keyRune := []rune(keyStr)
|
|
go w.charCallback(w, keyRune[0])
|
|
}
|
|
}
|
|
|
|
ke.Call("preventDefault")
|
|
return nil
|
|
}))
|
|
addDocumentEventListener.Invoke("keyup", newJsFuncFrom(func(this js.Value, args []js.Value) any {
|
|
ke := args[0]
|
|
w.goFullscreenIfRequested()
|
|
|
|
key := toKey(ke)
|
|
|
|
// Extend slice if needed.
|
|
neededSize := int(key) + 1
|
|
if neededSize > len(w.keys) {
|
|
w.keys = append(w.keys, make([]Action, neededSize-len(w.keys))...)
|
|
}
|
|
w.keys[key] = Release
|
|
|
|
if w.keyCallback != nil {
|
|
mods := toModifierKey(ke)
|
|
|
|
go w.keyCallback(w, key, -1, Release, mods)
|
|
}
|
|
|
|
ke.Call("preventDefault")
|
|
return nil
|
|
}))
|
|
addDocumentEventListener.Invoke("mousedown", newJsFuncFrom(func(this js.Value, args []js.Value) any {
|
|
me := args[0]
|
|
w.goFullscreenIfRequested()
|
|
|
|
button := me.Get("button").Int()
|
|
if !(button >= 0 && button <= 2) {
|
|
return nil
|
|
}
|
|
|
|
w.mouseButton[button] = Press
|
|
if w.mouseButtonCallback != nil {
|
|
go w.mouseButtonCallback(w, MouseButton(button), Press, 0)
|
|
}
|
|
|
|
me.Call("preventDefault")
|
|
return nil
|
|
}))
|
|
addDocumentEventListener.Invoke("mouseup", newJsFuncFrom(func(this js.Value, args []js.Value) any {
|
|
me := args[0]
|
|
w.goFullscreenIfRequested()
|
|
|
|
button := me.Get("button").Int()
|
|
if !(button >= 0 && button <= 2) {
|
|
return nil
|
|
}
|
|
|
|
w.mouseButton[button] = Release
|
|
if w.mouseButtonCallback != nil {
|
|
go w.mouseButtonCallback(w, MouseButton(button), Release, 0)
|
|
}
|
|
|
|
me.Call("preventDefault")
|
|
return nil
|
|
}))
|
|
addDocumentEventListener.Invoke("contextmenu", newJsFuncFrom(func(this js.Value, args []js.Value) any {
|
|
me := args[0]
|
|
me.Call("preventDefault")
|
|
return nil
|
|
}))
|
|
|
|
addDocumentEventListener.Invoke("mousemove", newJsFuncFrom(func(this js.Value, args []js.Value) any {
|
|
me := args[0]
|
|
var movementX, movementY float64
|
|
if !w.missing.pointerLock {
|
|
movementX = me.Get("movementX").Float()
|
|
movementY = me.Get("movementY").Float()
|
|
} else {
|
|
movementX = me.Get("clientX").Float() - w.cursorPos[0]
|
|
movementY = me.Get("clientY").Float() - w.cursorPos[1]
|
|
}
|
|
movementX *= w.devicePixelRatio
|
|
movementY *= w.devicePixelRatio
|
|
|
|
w.cursorPos[0], w.cursorPos[1] = me.Get("clientX").Float()*w.devicePixelRatio, me.Get("clientY").Float()*w.devicePixelRatio
|
|
if w.cursorPosCallback != nil {
|
|
go w.cursorPosCallback(w, w.cursorPos[0], w.cursorPos[1])
|
|
}
|
|
if w.mouseMovementCallback != nil {
|
|
go w.mouseMovementCallback(w, w.cursorPos[0], w.cursorPos[1], movementX, movementY)
|
|
}
|
|
me.Call("preventDefault")
|
|
return nil
|
|
}))
|
|
addDocumentEventListener.Invoke("wheel", newJsFuncFrom(func(this js.Value, args []js.Value) any {
|
|
we := args[0]
|
|
|
|
deltaX := we.Get("deltaX").Float()
|
|
deltaY := we.Get("deltaY").Float()
|
|
|
|
var multiplier float64
|
|
/*
|
|
switch we.DeltaMode {
|
|
case dom.DeltaPixel:
|
|
multiplier = 0.1
|
|
case dom.DeltaLine:
|
|
multiplier = 1
|
|
default:
|
|
log.Println("unsupported WheelEvent.DeltaMode:", we.DeltaMode)
|
|
multiplier = 1
|
|
}*/
|
|
multiplier = 1
|
|
|
|
if w.scrollCallback != nil {
|
|
go w.scrollCallback(w, -deltaX*multiplier, -deltaY*multiplier)
|
|
}
|
|
|
|
we.Call("preventDefault")
|
|
return nil
|
|
}))
|
|
|
|
/*
|
|
// Hacky mouse-emulation-via-touch.
|
|
touchHandler := func(event dom.Event) {
|
|
w.goFullscreenIfRequested()
|
|
|
|
te := event.(*dom.TouchEvent)
|
|
|
|
touches := te.Get("touches")
|
|
if touches.Length() > 0 {
|
|
t := touches.Index(0)
|
|
|
|
if w.touches != nil && w.touches.Length() > 0 { // This event is a movement only if we previously had > 0 touch points.
|
|
if w.mouseMovementCallback != nil {
|
|
go w.mouseMovementCallback(w, t.Get("clientX").Float(), t.Get("clientY").Float(), t.Get("clientX").Float()-w.cursorPos[0], t.Get("clientY").Float()-w.cursorPos[1])
|
|
}
|
|
}
|
|
|
|
w.cursorPos[0], w.cursorPos[1] = t.Get("clientX").Float(), t.Get("clientY").Float()
|
|
if w.cursorPosCallback != nil {
|
|
go w.cursorPosCallback(w, w.cursorPos[0], w.cursorPos[1])
|
|
}
|
|
}
|
|
w.touches = touches
|
|
|
|
te.PreventDefault()
|
|
}
|
|
document.AddEventListener("touchstart", false, touchHandler)
|
|
document.AddEventListener("touchmove", false, touchHandler)
|
|
document.AddEventListener("touchend", false, touchHandler)*/
|
|
|
|
addDocumentEventListener.Invoke("beforeUnload", newJsFuncFrom(func(this js.Value, args []js.Value) any {
|
|
if w.closeCallback != nil {
|
|
w.closeCallback(w)
|
|
}
|
|
return nil
|
|
}))
|
|
|
|
// Request first animation frame.
|
|
w.requestAnimationFrame = js.Global().Get("requestAnimationFrame").Call("bind", js.Global())
|
|
w.requestAnimationFrame.Invoke(animationFrameCallback)
|
|
|
|
return w, nil
|
|
}
|
|
|
|
func (w *Window) SetAttrib(attrib Hint, value int) {
|
|
// TODO: Implement.
|
|
}
|
|
|
|
func SwapInterval(interval int) error {
|
|
// TODO: Implement.
|
|
return nil
|
|
}
|
|
|
|
type Window struct {
|
|
canvas js.Value
|
|
context js.Value
|
|
requestFullscreen bool // requestFullscreen is set to true when fullscreen should be entered as soon as possible (in a user input handler).
|
|
fullscreen bool // fullscreen is true if we're currently in fullscreen mode.
|
|
|
|
// Unavailable browser APIs.
|
|
missing struct {
|
|
pointerLock bool // Pointer Lock API.
|
|
fullscreen bool // Fullscreen API.
|
|
}
|
|
|
|
devicePixelRatio float64
|
|
|
|
cursorMode int
|
|
cursorPos [2]float64
|
|
mouseButton [3]Action
|
|
|
|
keys []Action
|
|
|
|
cursorPosCallback CursorPosCallback
|
|
mouseMovementCallback MouseMovementCallback
|
|
mouseButtonCallback MouseButtonCallback
|
|
keyCallback KeyCallback
|
|
scrollCallback ScrollCallback
|
|
charCallback CharCallback
|
|
framebufferSizeCallback FramebufferSizeCallback
|
|
sizeCallback SizeCallback
|
|
focusCallback FocusCallback
|
|
closeCallback CloseCallback
|
|
|
|
touches js.Value // Hacky mouse-emulation-via-touch.
|
|
|
|
eventHandlerCleanups []func()
|
|
requestAnimationFrame js.Value
|
|
}
|
|
|
|
func (w *Window) SetPos(xpos, ypos int) {
|
|
fmt.Println("not implemented: SetPos:", xpos, ypos)
|
|
}
|
|
|
|
func (w *Window) SetSize(width, height int) {
|
|
fmt.Println("not implemented: SetSize:", width, height)
|
|
}
|
|
|
|
func (w *Window) SetIcon(images any) {
|
|
// images is actually of type []image.Image, but no need to import image until we actually do something with it
|
|
fmt.Println("not implemented: SetIcon")
|
|
}
|
|
|
|
// goFullscreenIfRequested performs webkitRequestFullscreen if it was scheduled. It is called only from
|
|
// user events, because that API will fail if called at any other time.
|
|
func (w *Window) goFullscreenIfRequested() {
|
|
if !w.requestFullscreen {
|
|
return
|
|
}
|
|
w.requestFullscreen = false
|
|
w.canvas.Call("webkitRequestFullscreen")
|
|
w.fullscreen = true
|
|
}
|
|
|
|
type Monitor struct{}
|
|
|
|
func (m *Monitor) GetVideoMode() *VidMode {
|
|
return &VidMode{
|
|
// HACK: Hardcoded sample values.
|
|
// TODO: Try to get real values from browser via some API, if possible.
|
|
Width: 1680,
|
|
Height: 1050,
|
|
RedBits: 8,
|
|
GreenBits: 8,
|
|
BlueBits: 8,
|
|
RefreshRate: 60,
|
|
}
|
|
}
|
|
|
|
func GetPrimaryMonitor() *Monitor {
|
|
// TODO: Implement real functionality.
|
|
return &Monitor{}
|
|
}
|
|
|
|
func (w *Window) SetMonitor(monitor *Monitor, xpos, ypos, width, height, refreshRate int) {
|
|
// TODO: Implement real functionality.
|
|
}
|
|
|
|
func PollEvents() error {
|
|
return nil
|
|
}
|
|
|
|
func (w *Window) MakeContextCurrent() {
|
|
contextWatcher.OnMakeCurrent(w.context)
|
|
}
|
|
|
|
func DetachCurrentContext() {
|
|
contextWatcher.OnDetach()
|
|
}
|
|
|
|
func GetCurrentContext() *Window {
|
|
panic("not implemented")
|
|
}
|
|
|
|
type CursorPosCallback func(w *Window, xpos float64, ypos float64)
|
|
|
|
func (w *Window) SetCursorPosCallback(cbfun CursorPosCallback) (previous CursorPosCallback) {
|
|
w.cursorPosCallback = cbfun
|
|
|
|
// TODO: Handle previous.
|
|
return nil
|
|
}
|
|
|
|
type MouseMovementCallback func(w *Window, xpos float64, ypos float64, xdelta float64, ydelta float64)
|
|
|
|
func (w *Window) SetMouseMovementCallback(cbfun MouseMovementCallback) (previous MouseMovementCallback) {
|
|
w.mouseMovementCallback = cbfun
|
|
|
|
// TODO: Handle previous.
|
|
return nil
|
|
}
|
|
|
|
type KeyCallback func(w *Window, key Key, scancode int, action Action, mods ModifierKey)
|
|
|
|
func (w *Window) SetKeyCallback(cbfun KeyCallback) (previous KeyCallback) {
|
|
w.keyCallback = cbfun
|
|
|
|
// TODO: Handle previous.
|
|
return nil
|
|
}
|
|
|
|
type CharCallback func(w *Window, char rune)
|
|
|
|
func (w *Window) SetCharCallback(cbfun CharCallback) (previous CharCallback) {
|
|
w.charCallback = cbfun
|
|
|
|
// TODO: Handle previous.
|
|
return nil
|
|
}
|
|
|
|
type ScrollCallback func(w *Window, xoff float64, yoff float64)
|
|
|
|
func (w *Window) SetScrollCallback(cbfun ScrollCallback) (previous ScrollCallback) {
|
|
w.scrollCallback = cbfun
|
|
|
|
// TODO: Handle previous.
|
|
return nil
|
|
}
|
|
|
|
type MouseButtonCallback func(w *Window, button MouseButton, action Action, mods ModifierKey)
|
|
|
|
func (w *Window) SetMouseButtonCallback(cbfun MouseButtonCallback) (previous MouseButtonCallback) {
|
|
w.mouseButtonCallback = cbfun
|
|
|
|
// TODO: Handle previous.
|
|
return nil
|
|
}
|
|
|
|
type FramebufferSizeCallback func(w *Window, width int, height int)
|
|
|
|
func (w *Window) SetFramebufferSizeCallback(cbfun FramebufferSizeCallback) (previous FramebufferSizeCallback) {
|
|
w.framebufferSizeCallback = cbfun
|
|
|
|
// TODO: Handle previous.
|
|
return nil
|
|
}
|
|
|
|
// Nearest non-negative int.
|
|
func (w *Window) scaleRound(f float64) int {
|
|
return int(f*w.devicePixelRatio + 0.5)
|
|
}
|
|
|
|
func (w *Window) GetSize() (width, height int) {
|
|
return w.scaleRound(w.canvas.Get("clientWidth").Float()), w.scaleRound(w.canvas.Get("clientHeight").Float())
|
|
}
|
|
|
|
func (w *Window) GetFramebufferSize() (width, height int) {
|
|
return w.canvas.Get("width").Int(), w.canvas.Get("height").Int()
|
|
}
|
|
|
|
func (w *Window) GetPos() (x, y int) {
|
|
// Not implemented.
|
|
return
|
|
}
|
|
|
|
func (w *Window) ShouldClose() bool {
|
|
return false
|
|
}
|
|
|
|
func (w *Window) SetShouldClose(value bool) {
|
|
// TODO: Implement.
|
|
// THINK: What should happen in the browser if we're told to "close" the window. Do we destroy/remove the canvas? Or nothing?
|
|
// Perhaps https://developer.mozilla.org/en-US/docs/Web/API/Window.close is relevant.
|
|
}
|
|
|
|
func (w *Window) SwapBuffers() error {
|
|
<-animationFrameChan
|
|
w.requestAnimationFrame.Invoke(animationFrameCallback)
|
|
|
|
return nil
|
|
}
|
|
|
|
var animationFrameChan = make(chan struct{}, 1)
|
|
|
|
var animationFrameCallback = js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
animationFrameChan <- struct{}{}
|
|
|
|
return nil
|
|
})
|
|
|
|
func (w *Window) GetCursorPos() (x, y float64) {
|
|
return w.cursorPos[0], w.cursorPos[1]
|
|
}
|
|
|
|
var keyWarnings = 10
|
|
|
|
func (w *Window) GetKey(key Key) Action {
|
|
if key == -1 && keyWarnings > 0 {
|
|
// TODO: Implement all keys, get rid of this.
|
|
keyWarnings--
|
|
log.Println("GetKey: key not implemented.")
|
|
return Release
|
|
}
|
|
if int(key) >= len(w.keys) {
|
|
return Release
|
|
}
|
|
return w.keys[key]
|
|
}
|
|
|
|
func (w *Window) GetMouseButton(button MouseButton) Action {
|
|
if !(button >= 0 && button <= 2) {
|
|
panic(fmt.Errorf("button is out of range: %v", button))
|
|
}
|
|
|
|
// Hacky mouse-emulation-via-touch.
|
|
if !w.touches.Equal(js.Value{}) {
|
|
switch button {
|
|
case MouseButton1:
|
|
if w.touches.Length() == 1 || w.touches.Length() == 3 {
|
|
return Press
|
|
}
|
|
case MouseButton2:
|
|
if w.touches.Length() == 2 || w.touches.Length() == 3 {
|
|
return Press
|
|
}
|
|
}
|
|
|
|
return Release
|
|
}
|
|
|
|
return w.mouseButton[button]
|
|
}
|
|
|
|
func (w *Window) GetInputMode(mode InputMode) int {
|
|
switch mode {
|
|
case CursorMode:
|
|
return w.cursorMode
|
|
default:
|
|
panic(errors.New("not implemented"))
|
|
}
|
|
}
|
|
|
|
var (
|
|
ErrInvalidParameter = errors.New("invalid parameter")
|
|
ErrInvalidValue = errors.New("invalid value")
|
|
)
|
|
|
|
func (w *Window) SetInputMode(mode InputMode, value int) {
|
|
switch mode {
|
|
case CursorMode:
|
|
// TODO; Make cursor API compatible with GLFW and Fyne use/expectation.
|
|
/*
|
|
// Temporarily disable cursor change
|
|
if w.missing.pointerLock {
|
|
log.Println("warning: Pointer Lock API unsupported")
|
|
return
|
|
}
|
|
switch value {
|
|
case CursorNormal:
|
|
w.cursorMode = value
|
|
document.Call("exitPointerLock")
|
|
w.canvas.Get("style").Call("setProperty", "cursor", "initial")
|
|
return
|
|
case CursorHidden:
|
|
w.cursorMode = value
|
|
document.Call("exitPointerLock")
|
|
w.canvas.Get("style").Call("setProperty", "cursor", "none")
|
|
return
|
|
case CursorDisabled:
|
|
w.cursorMode = value
|
|
w.canvas.Call("requestPointerLock")
|
|
return
|
|
default:
|
|
panic(ErrInvalidValue)
|
|
}
|
|
*/
|
|
return
|
|
case StickyKeysMode:
|
|
panic(errors.New("not implemented"))
|
|
case StickyMouseButtonsMode:
|
|
panic(errors.New("not implemented"))
|
|
default:
|
|
panic(ErrInvalidParameter)
|
|
}
|
|
}
|
|
|
|
type Key int
|
|
|
|
// TODO: Keys defined as -iota-2 need to be set to a valid positive value that matches the keyCode
|
|
//
|
|
// generated by browsers. -iota-2 is used as a temporary solution to have unique but invalid values.
|
|
// See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode.
|
|
const (
|
|
KeyUnknown Key = -1
|
|
KeySpace Key = 32
|
|
KeyApostrophe Key = 222
|
|
KeyComma Key = 188
|
|
KeyMinus Key = 189
|
|
KeyPeriod Key = 190
|
|
KeySlash Key = 191
|
|
Key0 Key = 48
|
|
Key1 Key = 49
|
|
Key2 Key = 50
|
|
Key3 Key = 51
|
|
Key4 Key = 52
|
|
Key5 Key = 53
|
|
Key6 Key = 54
|
|
Key7 Key = 55
|
|
Key8 Key = 56
|
|
Key9 Key = 57
|
|
KeySemicolon Key = 186
|
|
KeyEqual Key = 187
|
|
KeyA Key = 65
|
|
KeyB Key = 66
|
|
KeyC Key = 67
|
|
KeyD Key = 68
|
|
KeyE Key = 69
|
|
KeyF Key = 70
|
|
KeyG Key = 71
|
|
KeyH Key = 72
|
|
KeyI Key = 73
|
|
KeyJ Key = 74
|
|
KeyK Key = 75
|
|
KeyL Key = 76
|
|
KeyM Key = 77
|
|
KeyN Key = 78
|
|
KeyO Key = 79
|
|
KeyP Key = 80
|
|
KeyQ Key = 81
|
|
KeyR Key = 82
|
|
KeyS Key = 83
|
|
KeyT Key = 84
|
|
KeyU Key = 85
|
|
KeyV Key = 86
|
|
KeyW Key = 87
|
|
KeyX Key = 88
|
|
KeyY Key = 89
|
|
KeyZ Key = 90
|
|
KeyLeftBracket Key = 219
|
|
KeyBackslash Key = 220
|
|
KeyRightBracket Key = 221
|
|
KeyGraveAccent Key = 192
|
|
KeyWorld1 Key = -iota - 2
|
|
KeyWorld2 Key = -iota - 2
|
|
KeyEscape Key = 27
|
|
KeyEnter Key = 13
|
|
KeyTab Key = 9
|
|
KeyBackspace Key = 8
|
|
KeyInsert Key = -iota - 2
|
|
KeyDelete Key = 46
|
|
KeyRight Key = 39
|
|
KeyLeft Key = 37
|
|
KeyDown Key = 40
|
|
KeyUp Key = 38
|
|
KeyPageUp Key = -iota - 2
|
|
KeyPageDown Key = -iota - 2
|
|
KeyHome Key = -iota - 2
|
|
KeyEnd Key = -iota - 2
|
|
KeyCapsLock Key = 20
|
|
KeyScrollLock Key = -iota - 2
|
|
KeyNumLock Key = -iota - 2
|
|
KeyPrintScreen Key = -iota - 2
|
|
KeyPause Key = -iota - 2
|
|
KeyF1 Key = 112
|
|
KeyF2 Key = 113
|
|
KeyF3 Key = 114
|
|
KeyF4 Key = 115
|
|
KeyF5 Key = 116
|
|
KeyF6 Key = 117
|
|
KeyF7 Key = 118
|
|
KeyF8 Key = 119
|
|
KeyF9 Key = 120
|
|
KeyF10 Key = 121
|
|
KeyF11 Key = 122
|
|
KeyF12 Key = 123
|
|
KeyF13 Key = -iota - 2
|
|
KeyF14 Key = -iota - 2
|
|
KeyF15 Key = -iota - 2
|
|
KeyF16 Key = -iota - 2
|
|
KeyF17 Key = -iota - 2
|
|
KeyF18 Key = -iota - 2
|
|
KeyF19 Key = -iota - 2
|
|
KeyF20 Key = -iota - 2
|
|
KeyF21 Key = -iota - 2
|
|
KeyF22 Key = -iota - 2
|
|
KeyF23 Key = -iota - 2
|
|
KeyF24 Key = -iota - 2
|
|
KeyF25 Key = -iota - 2
|
|
KeyKP0 Key = -iota - 2
|
|
KeyKP1 Key = -iota - 2
|
|
KeyKP2 Key = -iota - 2
|
|
KeyKP3 Key = -iota - 2
|
|
KeyKP4 Key = -iota - 2
|
|
KeyKP5 Key = -iota - 2
|
|
KeyKP6 Key = -iota - 2
|
|
KeyKP7 Key = -iota - 2
|
|
KeyKP8 Key = -iota - 2
|
|
KeyKP9 Key = -iota - 2
|
|
KeyKPDecimal Key = -iota - 2
|
|
KeyKPDivide Key = -iota - 2
|
|
KeyKPMultiply Key = -iota - 2
|
|
KeyKPSubtract Key = -iota - 2
|
|
KeyKPAdd Key = -iota - 2
|
|
KeyKPEnter Key = -iota - 2
|
|
KeyKPEqual Key = -iota - 2
|
|
KeyLeftShift Key = 340
|
|
KeyLeftControl Key = 341
|
|
KeyLeftAlt Key = 342
|
|
KeyLeftSuper Key = 91
|
|
KeyRightShift Key = 344
|
|
KeyRightControl Key = 345
|
|
KeyRightAlt Key = 346
|
|
KeyRightSuper Key = 93
|
|
KeyMenu Key = -iota - 2
|
|
)
|
|
|
|
// toKey extracts Key from given KeyboardEvent.
|
|
func toKey(ke js.Value) Key {
|
|
// TODO: Factor out into DOM package.
|
|
const (
|
|
KeyLocationLeft = 1
|
|
KeyLocationRight = 2
|
|
)
|
|
|
|
key := Key(ke.Get("keyCode").Int())
|
|
switch {
|
|
case key == 16 && ke.Get("location").Int() == KeyLocationLeft:
|
|
key = KeyLeftShift
|
|
case key == 16 && ke.Get("location").Int() == KeyLocationRight:
|
|
key = KeyRightShift
|
|
case key == 17 && ke.Get("location").Int() == KeyLocationLeft:
|
|
key = KeyLeftControl
|
|
case key == 17 && ke.Get("location").Int() == KeyLocationRight:
|
|
key = KeyRightControl
|
|
case key == 18 && ke.Get("location").Int() == KeyLocationLeft:
|
|
key = KeyLeftAlt
|
|
case key == 18 && ke.Get("location").Int() == KeyLocationRight:
|
|
key = KeyRightAlt
|
|
}
|
|
return key
|
|
}
|
|
|
|
// toModifierKey extracts ModifierKey from given KeyboardEvent.
|
|
func toModifierKey(ke js.Value) ModifierKey {
|
|
mods := ModifierKey(0)
|
|
if ke.Get("shiftKey").Bool() {
|
|
mods += ModShift
|
|
}
|
|
if ke.Get("ctrlKey").Bool() {
|
|
mods += ModControl
|
|
}
|
|
if ke.Get("altKey").Bool() {
|
|
mods += ModAlt
|
|
}
|
|
if ke.Get("metaKey").Bool() {
|
|
mods += ModSuper
|
|
}
|
|
return mods
|
|
}
|
|
|
|
type MouseButton int
|
|
|
|
const (
|
|
MouseButton1 MouseButton = 0
|
|
MouseButton2 MouseButton = 2 // Web MouseEvent has middle and right mouse buttons in reverse order.
|
|
MouseButton3 MouseButton = 1 // Web MouseEvent has middle and right mouse buttons in reverse order.
|
|
|
|
MouseButtonLeft = MouseButton1
|
|
MouseButtonRight = MouseButton2
|
|
MouseButtonMiddle = MouseButton3
|
|
)
|
|
|
|
type Joystick int
|
|
|
|
const (
|
|
Joystick1 Joystick = iota
|
|
Joystick2
|
|
Joystick3
|
|
Joystick4
|
|
Joystick5
|
|
Joystick6
|
|
Joystick7
|
|
Joystick8
|
|
Joystick9
|
|
Joystick10
|
|
Joystick11
|
|
Joystick12
|
|
Joystick13
|
|
Joystick14
|
|
Joystick15
|
|
Joystick16
|
|
|
|
JoystickLast = Joystick16
|
|
)
|
|
|
|
type GamepadAxis int
|
|
|
|
const (
|
|
AxisLeftX GamepadAxis = iota
|
|
AxisLeftY
|
|
AxisRightX
|
|
AxisRightY
|
|
AxisLeftTrigger
|
|
AxisRightTrigger
|
|
|
|
AxisLast = AxisRightTrigger
|
|
)
|
|
|
|
type GamepadButton int
|
|
|
|
const (
|
|
ButtonA GamepadButton = iota
|
|
ButtonB
|
|
ButtonX
|
|
ButtonY
|
|
ButtonLeftBumper
|
|
ButtonRightBumper
|
|
ButtonBack
|
|
ButtonStart
|
|
ButtonGuide
|
|
ButtonLeftThumb
|
|
ButtonRightThumb
|
|
ButtonDpadUp
|
|
ButtonDpadRight
|
|
ButtonDpadDown
|
|
ButtonDpadLeft
|
|
|
|
ButtonLast = ButtonDpadLeft
|
|
ButtonCross = ButtonA
|
|
ButtonCircle = ButtonB
|
|
ButtonSquare = ButtonX
|
|
ButtonTriangle = ButtonY
|
|
)
|
|
|
|
type Action int
|
|
|
|
const (
|
|
Release Action = 0
|
|
Press Action = 1
|
|
Repeat Action = 2
|
|
)
|
|
|
|
type InputMode int
|
|
|
|
const (
|
|
CursorMode InputMode = iota
|
|
StickyKeysMode
|
|
StickyMouseButtonsMode
|
|
LockKeyMods
|
|
RawMouseMotion
|
|
)
|
|
|
|
const (
|
|
CursorNormal = iota
|
|
CursorHidden
|
|
CursorDisabled
|
|
)
|
|
|
|
type ModifierKey int
|
|
|
|
const (
|
|
ModShift ModifierKey = (1 << iota)
|
|
ModControl
|
|
ModAlt
|
|
ModSuper
|
|
)
|
|
|
|
func (joy Joystick) IsPresent() bool {
|
|
// TODO: Implement.
|
|
return false
|
|
}
|
|
|
|
func (joy Joystick) GetGamepadName() string {
|
|
// TODO: Implement.
|
|
return "Gamepad"
|
|
}
|
|
|
|
func (joy Joystick) GetButtons() []Action {
|
|
// TODO: Implement.
|
|
return make([]Action, 0)
|
|
}
|
|
|
|
func (joy Joystick) GetAxes() []float32 {
|
|
// TODO: Implement.
|
|
return make([]float32, 0)
|
|
}
|
|
|
|
// Open opens a named asset. It's the caller's responsibility to close it when done.
|
|
func Open(name string) (io.ReadCloser, error) {
|
|
resp, err := http.Get(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
return nil, fmt.Errorf("non-200 status: %s", resp.Status)
|
|
}
|
|
return resp.Body, nil
|
|
}
|
|
|
|
// ---
|
|
|
|
func WaitEvents() {
|
|
// TODO.
|
|
|
|
runtime.Gosched()
|
|
}
|
|
|
|
func PostEmptyEvent() {
|
|
// TODO: Implement.
|
|
}
|
|
|
|
func DefaultWindowHints() {
|
|
// TODO: Implement.
|
|
}
|
|
|
|
func (w *Window) SetClipboardString(str string) {
|
|
SetClipboardString(str)
|
|
}
|
|
|
|
func (w *Window) GetClipboardString() string {
|
|
return GetClipboardString()
|
|
}
|
|
|
|
func (w *Window) SetTitle(title string) {
|
|
document.Set("title", title)
|
|
}
|
|
|
|
func (w *Window) Show() {
|
|
// TODO: Implement.
|
|
}
|
|
|
|
func (w *Window) Hide() {
|
|
// TODO: Implement.
|
|
}
|
|
|
|
func (w *Window) Destroy() {
|
|
document.Get("body").Call("removeChild", w.canvas)
|
|
if w.fullscreen {
|
|
if w.missing.fullscreen {
|
|
log.Println("warning: Fullscreen API unsupported")
|
|
} else {
|
|
document.Call("webkitExitFullscreen")
|
|
w.fullscreen = false
|
|
}
|
|
}
|
|
|
|
for _, free := range w.eventHandlerCleanups {
|
|
free()
|
|
}
|
|
}
|
|
|
|
type CloseCallback func(w *Window)
|
|
|
|
func (w *Window) SetCloseCallback(cbfun CloseCallback) (previous CloseCallback) {
|
|
previous = w.closeCallback
|
|
w.closeCallback = cbfun
|
|
return previous
|
|
}
|
|
|
|
type RefreshCallback func(w *Window)
|
|
|
|
func (w *Window) SetRefreshCallback(cbfun RefreshCallback) (previous RefreshCallback) {
|
|
// TODO: Implement.
|
|
|
|
// TODO: Handle previous.
|
|
return nil
|
|
}
|
|
|
|
type SizeCallback func(w *Window, width int, height int)
|
|
|
|
func (w *Window) SetSizeCallback(cbfun SizeCallback) (previous SizeCallback) {
|
|
w.sizeCallback = cbfun
|
|
|
|
// TODO: Handle previous.
|
|
return nil
|
|
}
|
|
|
|
type CursorEnterCallback func(w *Window, entered bool)
|
|
|
|
func (w *Window) SetCursorEnterCallback(cbfun CursorEnterCallback) (previous CursorEnterCallback) {
|
|
// TODO: Implement.
|
|
|
|
// TODO: Handle previous.
|
|
return nil
|
|
}
|
|
|
|
type CharModsCallback func(w *Window, char rune, mods ModifierKey)
|
|
|
|
func (w *Window) SetCharModsCallback(cbfun CharModsCallback) (previous CharModsCallback) {
|
|
// TODO: Implement.
|
|
|
|
// TODO: Handle previous.
|
|
return nil
|
|
}
|
|
|
|
type PosCallback func(w *Window, xpos int, ypos int)
|
|
|
|
func (w *Window) SetPosCallback(cbfun PosCallback) (previous PosCallback) {
|
|
// TODO: Implement.
|
|
|
|
// TODO: Handle previous.
|
|
return nil
|
|
}
|
|
|
|
type FocusCallback func(w *Window, focused bool)
|
|
|
|
func (w *Window) SetFocusCallback(cbfun FocusCallback) (previous FocusCallback) {
|
|
previous = w.focusCallback
|
|
w.focusCallback = cbfun
|
|
return previous
|
|
}
|
|
|
|
type IconifyCallback func(w *Window, iconified bool)
|
|
|
|
func (w *Window) SetIconifyCallback(cbfun IconifyCallback) (previous IconifyCallback) {
|
|
// TODO: Implement.
|
|
|
|
// TODO: Handle previous.
|
|
return nil
|
|
}
|
|
|
|
type DropCallback func(w *Window, names []string)
|
|
|
|
func (w *Window) SetDropCallback(cbfun DropCallback) (previous DropCallback) {
|
|
// TODO: Implement. Can use HTML5 file drag and drop API?
|
|
|
|
// TODO: Handle previous.
|
|
return nil
|
|
}
|