Bläddra i källkod

Create cross-platform app package

Daniel Salvadori 6 år sedan
förälder
incheckning
4242aa5ceb
11 ändrade filer med 1127 tillägg och 392 borttagningar
  1. 113 0
      app/app-browser.go
  2. 154 0
      app/app-desktop.go
  3. 6 0
      app/doc.go
  4. 12 0
      app/logger.go
  5. 4 16
      gls/gls-wasm.go
  6. 1 1
      gls/gls.go
  7. 0 0
      renderer/version-browser.go
  8. 0 0
      renderer/version-desktop.go
  9. 618 0
      window/canvas.go
  10. 167 318
      window/glfw.go
  11. 52 57
      window/window.go

+ 113 - 0
app/app-browser.go

@@ -0,0 +1,113 @@
+// Copyright 2016 The G3N Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build wasm
+
+package app
+
+import (
+	"fmt"
+	"github.com/g3n/engine/renderer"
+	"github.com/g3n/engine/window"
+	"syscall/js"
+	"time"
+)
+
+// Default canvas id
+const canvasId = "g3n-canvas"
+
+// Application
+type Application struct {
+	window.IWindow                    // Embedded WebGLCanvas
+	keyState       *window.KeyState   // Keep track of keyboard state
+	renderer       *renderer.Renderer // Renderer object
+	frameStart     time.Time          // Frame start time
+	frameDelta     time.Duration      // Duration of last frame
+	exit           bool
+	cbid           js.Value
+}
+
+// Application singleton
+var app *Application
+
+// App returns the Application singleton, creating it the first time.
+func App() *Application {
+
+	// Return singleton if already created
+	if app != nil {
+		return app
+	}
+	app = new(Application)
+	// Initialize window
+	err := window.Init(canvasId)
+	if err != nil {
+		panic(err)
+	}
+	app.IWindow = window.Get()
+	// TODO audio setup here
+	app.keyState = window.NewKeyState(app) // Create KeyState
+	// Create renderer and add default shaders
+	app.renderer = renderer.NewRenderer(app.Gls())
+	err = app.renderer.AddDefaultShaders()
+	if err != nil {
+		panic(fmt.Errorf("AddDefaultShaders:%v", err))
+	}
+	return app
+}
+
+// Run starts the update loop.
+// It calls the user-provided update function every frame.
+func (app *Application) Run(update func(renderer *renderer.Renderer, deltaTime time.Duration)) {
+
+	// Create channel so later we can prevent application from finishing while we wait for callbacks
+	done := make(chan bool, 0)
+
+	// Initialize frame time
+	app.frameStart = time.Now()
+
+	// Set up recurring calls to user's update function
+	var tick js.Func
+	tick = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+		// Update frame start and frame delta
+		now := time.Now()
+		app.frameDelta = now.Sub(app.frameStart)
+		app.frameStart = now
+		// Call user's update function
+		update(app.renderer, app.frameDelta)
+		// Set up new callback if not exiting
+		if !app.exit {
+			app.cbid = js.Global().Call("requestAnimationFrame", tick)
+		} else {
+			done <- true // Write to done channel to exit the app
+		}
+		return nil
+	})
+	app.cbid = js.Global().Call("requestAnimationFrame", tick)
+
+	// Read from done channel
+	// This channel will be empty (except when we want to exit the app)
+	// It keeps the app from finishing while we wait for the next call to tick()
+	<-done
+
+	// Destroy the window
+	app.IWindow.Destroy()
+}
+
+// Exit exits the app.
+func (app *Application) Exit() {
+
+	app.exit = true
+}
+
+// Renderer returns the application's renderer.
+func (app *Application) Renderer() *renderer.Renderer {
+
+	return app.renderer
+}
+
+// KeyState returns the application's KeyState.
+func (app *Application) KeyState() *window.KeyState {
+
+	return app.keyState
+}

+ 154 - 0
app/app-desktop.go

@@ -0,0 +1,154 @@
+// Copyright 2016 The G3N Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build !wasm
+
+package app
+
+import (
+	"fmt"
+	"github.com/g3n/engine/audio/al"
+	"github.com/g3n/engine/audio/vorbis"
+	"github.com/g3n/engine/renderer"
+	"github.com/g3n/engine/window"
+	"time"
+)
+
+// Desktop application defaults
+const (
+	title  = "G3N Application"
+	width  = 800
+	height = 600
+)
+
+// OnExit is the event generated by Application when the user
+// tries to close the window or the Exit() method is called.
+const OnExit = "app.OnExit"
+
+// Application
+type Application struct {
+	window.IWindow                    // Embedded GlfwWindow
+	keyState       *window.KeyState   // Keep track of keyboard state
+	renderer       *renderer.Renderer // Renderer object
+	audioDev       *al.Device         // Default audio device
+	frameStart     time.Time          // Frame start time
+	frameDelta     time.Duration      // Duration of last frame
+}
+
+// Application singleton
+var app *Application
+
+// App returns the Application singleton, creating it the first time.
+func App() *Application {
+
+	// Return singleton if already created
+	if app != nil {
+		return app
+	}
+	app = new(Application)
+	// Initialize window
+	err := window.Init(width, height, title)
+	if err != nil {
+		panic(err)
+	}
+	app.IWindow = window.Get()
+	app.openDefaultAudioDevice()           // Set up audio
+	app.keyState = window.NewKeyState(app) // Create KeyState
+	// Create renderer and add default shaders
+	app.renderer = renderer.NewRenderer(app.Gls())
+	err = app.renderer.AddDefaultShaders()
+	if err != nil {
+		panic(fmt.Errorf("AddDefaultShaders:%v", err))
+	}
+	return app
+}
+
+// Run starts the update loop.
+// It calls the user-provided update function every frame.
+func (app *Application) Run(update func(renderer *renderer.Renderer, deltaTime time.Duration)) {
+
+	// Initialize frame time
+	app.frameStart = time.Now()
+
+	// Set up recurring calls to user's update function
+	for true {
+		// If Exit() was called or there was an attempt to close the window dispatch OnExit event for subscribers.
+		// If no subscriber cancelled the event, terminates the application.
+		if app.IWindow.(*window.GlfwWindow).ShouldClose() {
+			canceled := app.Dispatch(OnExit, nil) // TODO implement the same in app-browser
+			if canceled {
+				app.IWindow.(*window.GlfwWindow).SetShouldClose(false)
+			} else {
+				break
+			}
+		}
+		// Update frame start and frame delta
+		now := time.Now()
+		app.frameDelta = now.Sub(app.frameStart)
+		app.frameStart = now
+		// Call user's update function
+		update(app.renderer, app.frameDelta)
+		// Swap buffers and poll events
+		app.IWindow.(*window.GlfwWindow).SwapBuffers()
+		app.IWindow.(*window.GlfwWindow).PollEvents()
+	}
+
+	// Close default audio device
+	if app.audioDev != nil {
+		al.CloseDevice(app.audioDev)
+	}
+	// Destroy window
+	app.Destroy()
+}
+
+// Exit requests to terminate the application
+// Application will dispatch OnQuit events to registered subscribers which
+// can cancel the process by calling CancelDispatch().
+func (app *Application) Exit() {
+
+	app.IWindow.(*window.GlfwWindow).SetShouldClose(true)
+}
+
+// Renderer returns the application's renderer.
+func (app *Application) Renderer() *renderer.Renderer {
+
+	return app.renderer
+}
+
+// KeyState returns the application's KeyState.
+func (app *Application) KeyState() *window.KeyState {
+
+	return app.keyState
+}
+
+// openDefaultAudioDevice opens the default audio device setting it to the current context
+func (app *Application) openDefaultAudioDevice() error {
+
+	// Opens default audio device
+	var err error
+	app.audioDev, err = al.OpenDevice("")
+	if err != nil {
+		return fmt.Errorf("opening OpenAL default device: %s", err)
+	}
+	// Check for OpenAL effects extension support
+	var attribs []int
+	if al.IsExtensionPresent("ALC_EXT_EFX") {
+		attribs = []int{al.MAX_AUXILIARY_SENDS, 4}
+	}
+	// Create audio context
+	acx, err := al.CreateContext(app.audioDev, attribs)
+	if err != nil {
+		return fmt.Errorf("creating OpenAL context: %s", err)
+	}
+	// Makes the context the current one
+	err = al.MakeContextCurrent(acx)
+	if err != nil {
+		return fmt.Errorf("setting OpenAL context current: %s", err)
+	}
+	// Logs audio library versions
+	fmt.Println("LOGGING")
+	log.Info("%s version: %s", al.GetString(al.Vendor), al.GetString(al.Version))
+	log.Info("%s", vorbis.VersionString())
+	return nil
+}

+ 6 - 0
app/doc.go

@@ -0,0 +1,6 @@
+// Copyright 2016 The G3N Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package app implements a cross-platform G3N app.
+package app

+ 12 - 0
app/logger.go

@@ -0,0 +1,12 @@
+// Copyright 2016 The G3N Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package app
+
+import (
+	"github.com/g3n/engine/util/logger"
+)
+
+// Package logger
+var log = logger.New("APP", logger.Default)

+ 4 - 16
gls/gls-wasm.go

@@ -99,11 +99,12 @@ const (
 // which encapsulates the state of an WebGL context.
 // This should be called only after an active WebGL context
 // is established, such as by creating a new window.
-func New() (*GLS, error) {
+func New(webglCtx js.Value) (*GLS, error) {
 
 	gs := new(GLS)
 	gs.reset()
-	gs.checkErrors = true
+	gs.checkErrors = false
+	gs.gl = webglCtx
 
 	// Create js.Value storage maps
 	gs.programMap = make(map[uint32]js.Value)
@@ -125,24 +126,10 @@ func New() (*GLS, error) {
 	gs.uniformMapIndex = 1
 	gs.vertexArrayMapIndex = 1
 
-	// Create canvas and get reference to WebGL context
-	doc := js.Global().Get("document")
-	gs.canvas = doc.Call("createElement", "canvas")
-	gs.gl = gs.canvas.Call("getContext", "webgl2")
-	if gs.gl == js.Undefined() {
-		return nil, fmt.Errorf("Browser doesn't support WebGL2")
-	}
-
 	gs.setDefaultState()
 	return gs, nil
 }
 
-// Canvas returns the associated WebGL canvas.
-func (gs *GLS) Canvas() js.Value {
-
-	return gs.canvas
-}
-
 // SetCheckErrors enables/disables checking for errors after the
 // call of any WebGL function. It is enabled by default but
 // could be disabled after an application is stable to improve the performance.
@@ -531,6 +518,7 @@ func (gs *GLS) FrontFace(mode uint32) {
 	gs.frontFace = mode
 }
 
+// GenBuffer generates a ​buffer object name.
 func (gs *GLS) GenBuffer() uint32 {
 
 	gs.bufferMap[gs.bufferMapIndex] = gs.gl.Call("createBuffer")

+ 1 - 1
gls/gls.go

@@ -452,7 +452,7 @@ func (gs *GLS) FrontFace(mode uint32) {
 	gs.frontFace = mode
 }
 
-// GenBuffer generates a​buffer object name.
+// GenBuffer generates a ​buffer object name.
 func (gs *GLS) GenBuffer() uint32 {
 
 	var buf uint32

renderer/version-wasm.go → renderer/version-browser.go


renderer/version.go → renderer/version-desktop.go


+ 618 - 0
window/canvas.go

@@ -0,0 +1,618 @@
+// Copyright 2016 The G3N Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build wasm
+
+package window
+
+import (
+	"fmt"
+	"github.com/g3n/engine/core"
+	"github.com/g3n/engine/gls"
+	_ "image/png"
+	"syscall/js"
+)
+
+// Keycodes
+const (
+	KeyUnknown = Key(iota)
+	KeySpace
+	KeyApostrophe
+	KeyComma
+	KeyMinus
+	KeyPeriod
+	KeySlash
+	Key0
+	Key1
+	Key2
+	Key3
+	Key4
+	Key5
+	Key6
+	Key7
+	Key8
+	Key9
+	KeySemicolon
+	KeyEqual
+	KeyA
+	KeyB
+	KeyC
+	KeyD
+	KeyE
+	KeyF
+	KeyG
+	KeyH
+	KeyI
+	KeyJ
+	KeyK
+	KeyL
+	KeyM
+	KeyN
+	KeyO
+	KeyP
+	KeyQ
+	KeyR
+	KeyS
+	KeyT
+	KeyU
+	KeyV
+	KeyW
+	KeyX
+	KeyY
+	KeyZ
+	KeyLeftBracket
+	KeyBackslash
+	KeyRightBracket
+	KeyGraveAccent
+	KeyWorld1
+	KeyWorld2
+	KeyEscape
+	KeyEnter
+	KeyTab
+	KeyBackspace
+	KeyInsert
+	KeyDelete
+	KeyRight
+	KeyLeft
+	KeyDown
+	KeyUp
+	KeyPageUp
+	KeyPageDown
+	KeyHome
+	KeyEnd
+	KeyCapsLock
+	KeyScrollLock
+	KeyNumLock
+	KeyPrintScreen
+	KeyPause
+	KeyF1
+	KeyF2
+	KeyF3
+	KeyF4
+	KeyF5
+	KeyF6
+	KeyF7
+	KeyF8
+	KeyF9
+	KeyF10
+	KeyF11
+	KeyF12
+	KeyF13
+	KeyF14
+	KeyF15
+	KeyF16
+	KeyF17
+	KeyF18
+	KeyF19
+	KeyF20
+	KeyF21
+	KeyF22
+	KeyF23
+	KeyF24
+	KeyF25
+	KeyKP0
+	KeyKP1
+	KeyKP2
+	KeyKP3
+	KeyKP4
+	KeyKP5
+	KeyKP6
+	KeyKP7
+	KeyKP8
+	KeyKP9
+	KeyKPDecimal
+	KeyKPDivide
+	KeyKPMultiply
+	KeyKPSubtract
+	KeyKPAdd
+	KeyKPEnter
+	KeyKPEqual
+	KeyLeftShift
+	KeyLeftControl
+	KeyLeftAlt
+	KeyLeftSuper // Meta in Javascript
+	KeyRightShift
+	KeyRightControl
+	KeyRightAlt
+	KeyRightSuper
+	KeyMenu
+	KeyLast
+)
+
+var keyMap = map[string]Key{
+	//"KeyUnknown":  KeyUnknown, TODO emit when key is not in map
+	"Space":  KeySpace,
+	"Quote":  KeyApostrophe,
+	"Comma":  KeyComma,
+	"Minus":  KeyMinus,
+	"Period": KeyPeriod,
+	"Slash":  KeySlash,
+
+	"Digit0": Key0,
+	"Digit1": Key1,
+	"Digit2": Key2,
+	"Digit3": Key3,
+	"Digit4": Key4,
+	"Digit5": Key5,
+	"Digit6": Key6,
+	"Digit7": Key7,
+	"Digit8": Key8,
+	"Digit9": Key9,
+
+	"Semicolon": KeySemicolon,
+	"Equal":     KeyEqual,
+
+	"KeyA": KeyA,
+	"KeyB": KeyB,
+	"KeyC": KeyC,
+	"KeyD": KeyD,
+	"KeyE": KeyE,
+	"KeyF": KeyF,
+	"KeyG": KeyG,
+	"KeyH": KeyH,
+	"KeyI": KeyI,
+	"KeyJ": KeyJ,
+	"KeyK": KeyK,
+	"KeyL": KeyL,
+	"KeyM": KeyM,
+	"KeyN": KeyN,
+	"KeyO": KeyO,
+	"KeyP": KeyP,
+	"KeyQ": KeyQ,
+	"KeyR": KeyR,
+	"KeyS": KeyS,
+	"KeyT": KeyT,
+	"KeyU": KeyU,
+	"KeyV": KeyV,
+	"KeyW": KeyW,
+	"KeyX": KeyX,
+	"KeyY": KeyY,
+	"KeyZ": KeyZ,
+
+	"BracketLeft":  KeyLeftBracket,
+	"Backslash":    KeyBackslash,
+	"BracketRight": KeyRightBracket,
+	"Backquote":    KeyGraveAccent,
+	//"KeyWorld1": 	KeyWorld1,
+	//"KeyWorld2": 	KeyWorld2,
+
+	"Escape":      KeyEscape,
+	"Enter":       KeyEnter,
+	"Tab":         KeyTab,
+	"Backspace":   KeyBackspace,
+	"Insert":      KeyInsert,
+	"Delete":      KeyDelete,
+	"ArrowRight":  KeyRight,
+	"ArrowLeft":   KeyLeft,
+	"ArrowDown":   KeyDown,
+	"ArrowUp":     KeyUp,
+	"PageUp":      KeyPageUp,
+	"PageDown":    KeyPageDown,
+	"Home":        KeyHome,
+	"End":         KeyEnd,
+	"CapsLock":    KeyCapsLock,
+	"ScrollLock":  KeyScrollLock,
+	"NumLock":     KeyNumLock,
+	"PrintScreen": KeyPrintScreen,
+	"Pause":       KeyPause,
+
+	"F1":  KeyF1,
+	"F2":  KeyF2,
+	"F3":  KeyF3,
+	"F4":  KeyF4,
+	"F5":  KeyF5,
+	"F6":  KeyF6,
+	"F7":  KeyF7,
+	"F8":  KeyF8,
+	"F9":  KeyF9,
+	"F10": KeyF10,
+	"F11": KeyF11,
+	"F12": KeyF12,
+	"F13": KeyF13,
+	"F14": KeyF14,
+	"F15": KeyF15,
+	"F16": KeyF16,
+	"F17": KeyF17,
+	"F18": KeyF18,
+	"F19": KeyF19,
+	"F20": KeyF20,
+	"F21": KeyF21,
+	"F22": KeyF22,
+	"F23": KeyF23,
+	"F24": KeyF24,
+	"F25": KeyF25,
+
+	"Numpad0": KeyKP0,
+	"Numpad1": KeyKP1,
+	"Numpad2": KeyKP2,
+	"Numpad3": KeyKP3,
+	"Numpad4": KeyKP4,
+	"Numpad5": KeyKP5,
+	"Numpad6": KeyKP6,
+	"Numpad7": KeyKP7,
+	"Numpad8": KeyKP8,
+	"Numpad9": KeyKP9,
+
+	"NumpadDecimal":  KeyKPDecimal,
+	"NumpadDivide":   KeyKPDivide,
+	"NumpadMultiply": KeyKPMultiply,
+	"NumpadSubtract": KeyKPSubtract,
+	"NumpadAdd":      KeyKPAdd,
+	"NumpadEnter":    KeyKPEnter,
+	"NumpadEqual":    KeyKPEqual,
+
+	"ShiftLeft":    KeyLeftShift,
+	"ControlLeft":  KeyLeftControl,
+	"AltLeft":      KeyLeftAlt,
+	"MetaLeft":     KeyLeftSuper,
+	"ShitRight":    KeyRightShift,
+	"ControlRight": KeyRightControl,
+	"AltRight":     KeyRightAlt,
+	"MetaRight":    KeyRightSuper,
+	"Menu":         KeyMenu,
+}
+
+// Modifier keys
+const (
+	ModShift = ModifierKey(1 << iota) // Bitmask
+	ModControl
+	ModAlt
+	ModSuper // Meta in Javascript
+)
+
+// Mouse buttons
+const (
+	//MouseButton1      = MouseButton(0)
+	//MouseButton2      = MouseButton(0)
+	//MouseButton3      = MouseButton(0)
+	//MouseButton4      = MouseButton(0)
+	//MouseButton5      = MouseButton(0)
+	//MouseButton6      = MouseButton(0)
+	//MouseButton7      = MouseButton(0)
+	//MouseButton8      = MouseButton(0)
+	//MouseButtonLast   = MouseButton(0)
+	MouseButtonLeft   = MouseButton(0)
+	MouseButtonRight  = MouseButton(2)
+	MouseButtonMiddle = MouseButton(1)
+)
+
+// Actions
+const (
+	Release = Action(iota) // Release indicates that the key or mouse button was released
+	Press                  // Press indicates that the key or mouse button was pressed
+	Repeat                 // Repeat indicates that the key was held down until it repeated
+)
+
+// Input modes
+const (
+	CursorInputMode             = InputMode(iota) // See Cursor mode values
+	StickyKeysInputMode                           // Value can be either 1 or 0
+	StickyMouseButtonsInputMode                   // Value can be either 1 or 0
+)
+
+// Cursor mode values
+const (
+	CursorNormal = CursorMode(iota)
+	CursorHidden
+	CursorDisabled
+)
+
+// WebGlCanvas is a browser-based WebGL canvas.
+type WebGlCanvas struct {
+	core.Dispatcher          // Embedded event dispatcher
+	canvas          js.Value // Associated WebGL canvas
+	gls             *gls.GLS // Associated WebGL state
+
+	// Events
+	keyEv    KeyEvent
+	charEv   CharEvent
+	mouseEv  MouseEvent
+	posEv    PosEvent
+	sizeEv   SizeEvent
+	cursorEv CursorEvent
+	scrollEv ScrollEvent
+
+	// Callbacks
+	onCtxMenu  js.Func
+	keyDown    js.Func
+	keyUp      js.Func
+	mouseDown  js.Func
+	mouseUp    js.Func
+	mouseMove  js.Func
+	mouseWheel js.Func
+	winResize  js.Func
+}
+
+// Init initializes the WebGlCanvas singleton.
+// If canvasId is provided, the pre-existing WebGlCanvas with that id is used.
+// If canvasId is the empty string then it creates a new WebGL canvas.
+func Init(canvasId string) error {
+
+	// Panic if already created
+	if win != nil {
+		panic(fmt.Errorf("can only call window.Init() once"))
+	}
+
+	// Create wrapper window with dispatcher
+	w := new(WebGlCanvas)
+	w.Dispatcher.Initialize()
+
+	// Create or get WebGlCanvas
+	doc := js.Global().Get("document")
+	if canvasId == "" {
+		w.canvas = doc.Call("createElement", "WebGlCanvas")
+	} else {
+		w.canvas = doc.Call("getElementById", canvasId)
+		if w.canvas == js.Null() {
+			panic(fmt.Sprintf("Cannot find canvas with provided id: %s", canvasId))
+		}
+	}
+
+	// Get reference to WebGL context
+	webglCtx := w.canvas.Call("getContext", "webgl2")
+	if webglCtx == js.Undefined() {
+		return fmt.Errorf("Browser doesn't support WebGL2")
+	}
+
+	// Create WebGL state
+	gl, err := gls.New(webglCtx)
+	if err != nil {
+		return err
+	}
+	w.gls = gl
+
+	// Disable right-click context menu on the canvas
+	w.onCtxMenu = js.FuncOf(func(this js.Value, args []js.Value) interface{} { return false })
+	w.canvas.Set("oncontextmenu", w.onCtxMenu)
+
+	// TODO scaling/hidpi (device pixel ratio)
+
+	// Set up key callbacks to dispatch events
+	w.keyDown = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+		event := args[0]
+		eventCode := event.Get("code").String()
+		w.keyEv.Keycode = Key(keyMap[eventCode])
+		w.keyEv.Action = Action(Press)
+		w.keyEv.Mods = getModifiers(event)
+		w.Dispatch(OnKeyDown, &w.keyEv)
+		return nil
+	})
+	js.Global().Call("addEventListener", "keydown", w.keyDown)
+
+	w.keyUp = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+		event := args[0]
+		eventCode := event.Get("code").String()
+		w.keyEv.Keycode = Key(keyMap[eventCode])
+		w.keyEv.Action = Action(Release)
+		w.keyEv.Mods = getModifiers(event)
+		w.Dispatch(OnKeyUp, &w.keyEv)
+		return nil
+	})
+	js.Global().Call("addEventListener", "keyup", w.keyUp)
+
+	// Set up mouse button callbacks to dispatch events // TODO make these comments consistent
+
+	w.mouseDown = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+		event := args[0]
+		button := MouseButton(event.Get("button").Int())
+		xpos := event.Get("offsetX").Int()
+		ypos := event.Get("offsetY").Int()
+		w.mouseEv.Button = MouseButton(button)
+		w.mouseEv.Action = Action(Press)
+		w.mouseEv.Mods = getModifiers(event)
+		w.mouseEv.Xpos = float32(xpos) //* float32(w.scaleX) TODO
+		w.mouseEv.Ypos = float32(ypos) //* float32(w.scaleY)
+		w.Dispatch(OnMouseDown, &w.mouseEv)
+		return nil
+	})
+	w.canvas.Call("addEventListener", "mousedown", w.mouseDown)
+
+	w.mouseUp = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+		event := args[0]
+		button := MouseButton(event.Get("button").Int())
+		xpos := event.Get("offsetX").Float()
+		ypos := event.Get("offsetY").Float()
+		w.mouseEv.Button = MouseButton(button)
+		w.mouseEv.Action = Action(Release)
+		w.mouseEv.Mods = getModifiers(event)
+		w.mouseEv.Xpos = float32(xpos) //* float32(w.scaleX) TODO
+		w.mouseEv.Ypos = float32(ypos) //* float32(w.scaleY)
+		w.Dispatch(OnMouseUp, &w.mouseEv)
+		return nil
+	})
+	w.canvas.Call("addEventListener", "mouseup", w.mouseUp)
+
+	w.mouseMove = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+		event := args[0]
+		xpos := event.Get("offsetX").Float()
+		ypos := event.Get("offsetY").Float()
+		w.cursorEv.Xpos = float32(xpos) //* float32(w.scaleX) TODO
+		w.cursorEv.Ypos = float32(ypos) //* float32(w.scaleY)
+		w.cursorEv.Mods = getModifiers(event)
+		w.Dispatch(OnCursor, &w.cursorEv)
+		return nil
+	})
+	w.canvas.Call("addEventListener", "mousemove", w.mouseMove)
+
+	w.mouseWheel = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+		event := args[0]
+		event.Call("preventDefault")
+		xoff := event.Get("deltaX").Float()
+		yoff := event.Get("deltaY").Float()
+		w.scrollEv.Xoffset = -float32(xoff) / 100.0
+		w.scrollEv.Yoffset = -float32(yoff) / 100.0
+		w.scrollEv.Mods = getModifiers(event)
+		w.Dispatch(OnScroll, &w.scrollEv)
+		return nil
+	})
+	w.canvas.Call("addEventListener", "wheel", w.mouseWheel)
+
+	w.winResize = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+		w.sizeEv.Width = w.canvas.Get("width").Int()
+		w.sizeEv.Height = w.canvas.Get("height").Int()
+		// TODO device pixel ratio
+		//fbw, fbh := x.GetFramebufferSize()
+		//w.scaleX = float64(fbw) / float64(width)
+		//w.scaleY = float64(fbh) / float64(height)
+		w.Dispatch(OnWindowSize, &w.sizeEv)
+		return nil
+	})
+	js.Global().Get("window").Call("addEventListener", "resize", w.winResize)
+
+	//// Set char callback TODO
+	//w.SetCharModsCallback(func(x *glfw.Window, char rune, mods glfw.ModifierKey) {	//
+	//	w.charEv.Char = char
+	//	w.charEv.Mods = ModifierKey(mods)
+	//	w.Dispatch(OnChar, &w.charEv)
+	//})
+
+	win = w // Set singleton
+	return nil
+}
+
+// getModifiers extracts a ModifierKey bitmask from a Javascript event object.
+func getModifiers(event js.Value) ModifierKey {
+
+	shiftKey := event.Get("shiftKey").Bool()
+	ctrlKey := event.Get("ctrlKey").Bool()
+	altKey := event.Get("altKey").Bool()
+	metaKey := event.Get("metaKey").Bool()
+	var mods ModifierKey
+	if shiftKey {
+		mods = mods | ModShift
+	}
+	if ctrlKey {
+		mods = mods | ModControl
+	}
+	if altKey {
+		mods = mods | ModAlt
+	}
+	if metaKey {
+		mods = mods | ModSuper
+	}
+	return mods
+}
+
+// Canvas returns the associated WebGL WebGlCanvas.
+func (w *WebGlCanvas) Canvas() js.Value {
+
+	return w.canvas
+}
+
+// Gls returns the associated OpenGL state
+func (w *WebGlCanvas) Gls() *gls.GLS {
+
+	return w.gls
+}
+
+// FullScreen returns whether this canvas is fullscreen
+func (w *WebGlCanvas) FullScreen() bool {
+
+	// TODO
+	return false
+}
+
+// SetFullScreen sets this window full screen state for the primary monitor
+func (w *WebGlCanvas) SetFullScreen(full bool) {
+
+	// TODO
+	// Make it so that the first user interaction (e.g. click) should set the canvas as fullscreen.
+}
+
+// Destroy destroys the WebGL canvas and removes all event listeners.
+func (w *WebGlCanvas) Destroy() {
+
+	// Remove event listeners
+	w.canvas.Set("oncontextmenu", js.Null())
+	js.Global().Call("removeEventListener", "keydown", w.keyDown)
+	js.Global().Call("removeEventListener", "keyup", w.keyUp)
+	w.canvas.Call("removeEventListener", "mousedown", w.mouseDown)
+	w.canvas.Call("removeEventListener", "mouseup", w.mouseUp)
+	w.canvas.Call("removeEventListener", "mousemove", w.mouseMove)
+	w.canvas.Call("removeEventListener", "wheel", w.mouseWheel)
+	js.Global().Get("window").Call("removeEventListener", "resize", w.winResize)
+
+	// Release callbacks
+	w.onCtxMenu.Release()
+	w.keyDown.Release()
+	w.keyUp.Release()
+	w.mouseDown.Release()
+	w.mouseUp.Release()
+	w.mouseMove.Release()
+	w.mouseWheel.Release()
+	w.winResize.Release()
+}
+
+// GetFramebufferSize returns the framebuffer size.
+func (w *WebGlCanvas) GetFramebufferSize() (width int, height int) {
+
+	// TODO device pixel ratio
+	return w.canvas.Get("width").Int(), w.canvas.Get("height").Int()
+}
+
+// GetSize returns this window's size in screen coordinates.
+func (w *WebGlCanvas) GetSize() (width int, height int) {
+
+	return w.canvas.Get("width").Int(), w.canvas.Get("height").Int()
+}
+
+// SetSize sets the size, in screen coordinates, of the canvas.
+func (w *WebGlCanvas) SetSize(width int, height int) {
+
+	w.canvas.Set("width", width)
+	w.canvas.Set("height", height)
+}
+
+// Scale returns this window's DPI scale factor (FramebufferSize / Size)
+func (w *WebGlCanvas) GetScale() (x float64, y float64) {
+
+	// TODO device pixel ratio
+	return 1, 1
+}
+
+// CreateCursor creates a new custom cursor and returns an int handle.
+func (w *WebGlCanvas) CreateCursor(imgFile string, xhot, yhot int) (Cursor, error) {
+
+	// TODO
+	return 0, nil
+}
+
+// SetCursor sets the window's cursor to a standard one
+func (w *WebGlCanvas) SetCursor(cursor Cursor) {
+
+	// TODO
+}
+
+// DisposeAllCursors deletes all existing custom cursors.
+func (w *WebGlCanvas) DisposeAllCustomCursors() {
+
+	// TODO
+}
+
+// SetInputMode changes specified input to specified state
+//func (w *WebGlCanvas) SetInputMode(mode InputMode, state int) {
+//
+//	// TODO
+//	// Hide cursor etc
+//}

+ 167 - 318
window/glfw.go

@@ -5,12 +5,13 @@
 package window
 
 import (
+	"bytes"
+	"fmt"
+	"github.com/g3n/engine/gui/assets"
 	"runtime"
 
-	"bytes"
 	"github.com/g3n/engine/core"
 	"github.com/g3n/engine/gls"
-	"github.com/g3n/engine/gui/assets"
 	"github.com/go-gl/glfw/v3.2/glfw"
 	"image"
 	_ "image/png"
@@ -167,18 +168,6 @@ const (
 	MouseButtonMiddle = MouseButton(glfw.MouseButtonMiddle)
 )
 
-// Standard cursors for g3n. The diagonal cursors are not standard for GLFW.
-const (
-	ArrowCursor       = StandardCursor(glfw.ArrowCursor)
-	IBeamCursor       = StandardCursor(glfw.IBeamCursor)
-	CrosshairCursor   = StandardCursor(glfw.CrosshairCursor)
-	HandCursor        = StandardCursor(glfw.HandCursor)
-	HResizeCursor     = StandardCursor(glfw.HResizeCursor)
-	VResizeCursor     = StandardCursor(glfw.VResizeCursor)
-	DiagResize1Cursor = StandardCursor(VResizeCursor + 1)
-	DiagResize2Cursor = StandardCursor(VResizeCursor + 2)
-)
-
 // Actions
 const (
 	// Release indicates that key or mouse button was released
@@ -203,31 +192,12 @@ const (
 	CursorDisabled = CursorMode(glfw.CursorDisabled)
 )
 
-// glfwManager contains data shared by all windows
-type glfwManager struct {
-	arrowCursor     *glfw.Cursor // Preallocated standard arrow cursor
-	ibeamCursor     *glfw.Cursor // Preallocated standard ibeam cursor
-	crosshairCursor *glfw.Cursor // Preallocated standard cross hair cursor
-	handCursor      *glfw.Cursor // Preallocated standard hand cursor
-	hresizeCursor   *glfw.Cursor // Preallocated standard horizontal resize cursor
-	vresizeCursor   *glfw.Cursor // Preallocated standard vertical resize cursor
-
-	// Non GLFW standard cursors (but g3n standard)
-	diag1Cursor *glfw.Cursor // Preallocated diagonal resize cursor (/)
-	diag2Cursor *glfw.Cursor // Preallocated diagonal resize cursor (\)
-
-	// User-created custom cursors
-	customCursors map[int]*glfw.Cursor
-	lastCursorKey int
-}
-
-// glfwWindow describes one glfw window
-type glfwWindow struct {
-	core.Dispatcher              // Embedded event dispatcher
-	gls             *gls.GLS     // Associated OpenGL State
-	win             *glfw.Window // Pointer to native glfw window
-	mgr             *glfwManager // Pointer to window manager
-	fullScreen      bool
+// GlfwWindow describes one glfw window
+type GlfwWindow struct {
+	*glfw.Window             // Embedded GLFW window
+	core.Dispatcher          // Embedded event dispatcher
+	gls             *gls.GLS // Associated OpenGL State
+	fullscreen      bool
 	lastX           int
 	lastY           int
 	lastWidth       int
@@ -243,30 +213,43 @@ type glfwWindow struct {
 	sizeEv   SizeEvent
 	cursorEv CursorEvent
 	scrollEv ScrollEvent
-}
 
-// glfw manager singleton
-var manager *glfwManager
+	mods ModifierKey // Current modifier keys
 
-// Manager returns the glfw window manager
-func Manager() (IWindowManager, error) {
+	// Cursors
+	cursors       map[Cursor]*glfw.Cursor
+	lastCursorKey Cursor
+}
+
+// Init initializes the GlfwWindow singleton with the specified width, height, and title.
+func Init(width, height int, title string) error {
 
-	if manager != nil {
-		return manager, nil
+	// Panic if already created
+	if win != nil {
+		panic(fmt.Errorf("can only call window.Init() once"))
 	}
 
-	// Initialize glfw
-	err := glfw.Init()
+	// OpenGL functions must be executed in the same thread where
+	// the context was created (by wmgr.CreateWindow())
+	runtime.LockOSThread()
+
+	// Create wrapper window with dispatcher
+	w := new(GlfwWindow)
+	w.Dispatcher.Initialize()
+	var err error
+
+	// Initialize GLFW
+	err = glfw.Init()
 	if err != nil {
-		return nil, err
+		return err
 	}
 
-	// Sets window hints
+	// Set window hints
 	glfw.WindowHint(glfw.ContextVersionMajor, 3)
 	glfw.WindowHint(glfw.ContextVersionMinor, 3)
 	glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
 	glfw.WindowHint(glfw.Samples, 8)
-	// Sets OpenGL forward compatible context only for OSX because it is required for OSX.
+	// Set OpenGL forward compatible context only for OSX because it is required for OSX.
 	// When this is set, glLineWidth(width) only accepts width=1.0 and generates an error
 	// for any other values although the spec says it should ignore unsupported widths
 	// and generate an error only when width <= 0.
@@ -274,180 +257,92 @@ func Manager() (IWindowManager, error) {
 		glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)
 	}
 
-	manager = new(glfwManager)
-
-	// Preallocate GLFW standard cursors
-	manager.arrowCursor = glfw.CreateStandardCursor(glfw.ArrowCursor)
-	manager.ibeamCursor = glfw.CreateStandardCursor(glfw.IBeamCursor)
-	manager.crosshairCursor = glfw.CreateStandardCursor(glfw.CrosshairCursor)
-	manager.handCursor = glfw.CreateStandardCursor(glfw.HandCursor)
-	manager.hresizeCursor = glfw.CreateStandardCursor(glfw.HResizeCursor)
-	manager.vresizeCursor = glfw.CreateStandardCursor(glfw.VResizeCursor)
-
-	// Preallocate g3n cursors (diagonal cursors)
-	cursorDiag1Png := assets.MustAsset("cursors/diag1.png")
-	cursorDiag2Png := assets.MustAsset("cursors/diag2.png")
-
-	diag1Img, _, err := image.Decode(bytes.NewReader(cursorDiag1Png))
-	diag2Img, _, err := image.Decode(bytes.NewReader(cursorDiag2Png))
-	if err != nil {
-		return nil, err
-	}
-	manager.diag1Cursor = glfw.CreateCursor(diag1Img, 8, 8)
-	manager.diag2Cursor = glfw.CreateCursor(diag2Img, 8, 8)
-
-	// Create map for custom cursors
-	manager.customCursors = make(map[int]*glfw.Cursor)
-
-	return manager, nil
-}
-
-// ScreenResolution returns the screen resolution
-func (m *glfwManager) ScreenResolution(p interface{}) (width, height int) {
-
-	mon := glfw.GetPrimaryMonitor()
-	vmode := mon.GetVideoMode()
-	return vmode.Width, vmode.Height
-}
-
-// PollEvents process events in the event queue
-func (m *glfwManager) PollEvents() {
-
-	glfw.PollEvents()
-}
-
-// SetSwapInterval sets the number of screen updates to wait from the time SwapBuffer()
-// is called before swapping the buffers and returning.
-func (m *glfwManager) SetSwapInterval(interval int) {
-
-	glfw.SwapInterval(interval)
-}
-
-// Terminate destroys any remaining window, cursors and other related objects.
-func (m *glfwManager) Terminate() {
-
-	glfw.Terminate()
-	manager = nil
-}
-
-// CreateCursor creates a new custom cursor and returns an int handle.
-func (m *glfwManager) CreateCursor(imgFile string, xhot, yhot int) (int, error) {
-
-	// Open image file
-	file, err := os.Open(imgFile)
-	if err != nil {
-		return 0, err
-	}
-	defer file.Close()
-
-	// Decodes image
-	img, _, err := image.Decode(file)
+	// Create window and set it as the current context.
+	// The window is created always as not full screen because if it is
+	// created as full screen it not possible to revert it to windowed mode.
+	// At the end of this function, the window will be set to full screen if requested.
+	w.Window, err = glfw.CreateWindow(width, height, title, nil, nil)
 	if err != nil {
-		return 0, err
+		return err
 	}
+	w.MakeContextCurrent()
 
-	cursor := glfw.CreateCursor(img, xhot, yhot)
+	// Create OpenGL state
+	w.gls, err = gls.New()
 	if err != nil {
-		return 0, err
+		return err
 	}
-	m.lastCursorKey += 1
-	m.customCursors[m.lastCursorKey] = cursor
 
-	return m.lastCursorKey, nil
-}
-
-// DisposeCursor deletes the existing custom cursor with the provided int handle.
-func (m *glfwManager) DisposeCursor(key int) {
-
-	delete(m.customCursors, key)
-}
-
-// DisposeAllCursors deletes all existing custom cursors.
-func (m *glfwManager) DisposeAllCursors() {
-
-	m.customCursors = make(map[int]*glfw.Cursor)
-	m.lastCursorKey = 0
-}
+	// Compute and store scale
+	fbw, fbh := w.GetFramebufferSize()
+	w.scaleX = float64(fbw) / float64(width)
+	w.scaleY = float64(fbh) / float64(height)
 
-// CreateWindow creates and returns a new window with the specified width and height in screen coordinates
-func (m *glfwManager) CreateWindow(width, height int, title string, fullscreen bool) (IWindow, error) {
+	// Create map for cursors
+	w.cursors = make(map[Cursor]*glfw.Cursor)
+	w.lastCursorKey = CursorLast
 
-	// Creates window and sets it as the current context.
-	// The window is created always as not full screen because if it is
-	// created as full screen it not possible to revert it to windowed mode.
-	// At the end of this function, the window will be set to full screen if requested.
-	win, err := glfw.CreateWindow(width, height, title, nil, nil)
+	// Preallocate GLFW standard cursors
+	w.cursors[ArrowCursor] = glfw.CreateStandardCursor(glfw.ArrowCursor)
+	w.cursors[IBeamCursor] = glfw.CreateStandardCursor(glfw.IBeamCursor)
+	w.cursors[CrosshairCursor] = glfw.CreateStandardCursor(glfw.CrosshairCursor)
+	w.cursors[HandCursor] = glfw.CreateStandardCursor(glfw.HandCursor)
+	w.cursors[HResizeCursor] = glfw.CreateStandardCursor(glfw.HResizeCursor)
+	w.cursors[VResizeCursor] = glfw.CreateStandardCursor(glfw.VResizeCursor)
+
+	// Preallocate extra G3N standard cursors (diagonal resize cursors)
+	cursorDiag1Png := assets.MustAsset("cursors/diag1.png") // [/]
+	cursorDiag2Png := assets.MustAsset("cursors/diag2.png") // [\]
+	diag1Img, _, err := image.Decode(bytes.NewReader(cursorDiag1Png))
+	diag2Img, _, err := image.Decode(bytes.NewReader(cursorDiag2Png))
 	if err != nil {
-		return nil, err
+		return err
 	}
-	win.MakeContextCurrent()
-
-	// Create wrapper window with dispacher
-	w := new(glfwWindow)
-	w.win = win
-	w.mgr = m
-	w.Dispatcher.Initialize()
-
-	fbw, fbh := w.FramebufferSize()
-	w.scaleX = float64(fbw) / float64(width)
-	w.scaleY = float64(fbh) / float64(height)
+	w.cursors[DiagResize1Cursor] = glfw.CreateCursor(diag1Img, 8, 8) // [/]
+	w.cursors[DiagResize2Cursor] = glfw.CreateCursor(diag2Img, 8, 8) // [\]
 
 	// Set key callback to dispatch event
-	win.SetKeyCallback(func(x *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) {
-
-		w.keyEv.W = w
+	w.SetKeyCallback(func(x *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) {
 		w.keyEv.Keycode = Key(key)
-		w.keyEv.Scancode = scancode
 		w.keyEv.Action = Action(action)
 		w.keyEv.Mods = ModifierKey(mods)
+		w.mods = w.keyEv.Mods
 		if action == glfw.Press {
+			fmt.Println("GLFW: OnKeyDown")
 			w.Dispatch(OnKeyDown, &w.keyEv)
-			return
-		}
-		if action == glfw.Release {
+		} else if action == glfw.Release {
+			fmt.Println("GLFW: OnKeyUp")
 			w.Dispatch(OnKeyUp, &w.keyEv)
-			return
-		}
-		if action == glfw.Repeat {
+		} else if action == glfw.Repeat {
+			fmt.Println("GLFW: OnKeyRepeat")
 			w.Dispatch(OnKeyRepeat, &w.keyEv)
-			return
 		}
 	})
 
 	// Set char callback
-	win.SetCharModsCallback(func(x *glfw.Window, char rune, mods glfw.ModifierKey) {
-
-		w.charEv.W = w
+	w.SetCharModsCallback(func(x *glfw.Window, char rune, mods glfw.ModifierKey) {
 		w.charEv.Char = char
 		w.charEv.Mods = ModifierKey(mods)
 		w.Dispatch(OnChar, &w.charEv)
 	})
 
 	// Set mouse button callback to dispatch event
-	win.SetMouseButtonCallback(func(x *glfw.Window, button glfw.MouseButton, action glfw.Action, mods glfw.ModifierKey) {
-
+	w.SetMouseButtonCallback(func(x *glfw.Window, button glfw.MouseButton, action glfw.Action, mods glfw.ModifierKey) {
 		xpos, ypos := x.GetCursorPos()
-
-		w.mouseEv.W = w
 		w.mouseEv.Button = MouseButton(button)
 		w.mouseEv.Action = Action(action)
 		w.mouseEv.Mods = ModifierKey(mods)
 		w.mouseEv.Xpos = float32(xpos * w.scaleX)
 		w.mouseEv.Ypos = float32(ypos * w.scaleY)
-
 		if action == glfw.Press {
 			w.Dispatch(OnMouseDown, &w.mouseEv)
-			return
-		}
-		if action == glfw.Release {
+		} else if action == glfw.Release {
 			w.Dispatch(OnMouseUp, &w.mouseEv)
-			return
 		}
 	})
 
 	// Set window size callback to dispatch event
-	win.SetSizeCallback(func(x *glfw.Window, width int, height int) {
-
+	w.SetSizeCallback(func(x *glfw.Window, width int, height int) {
 		fbw, fbh := x.GetFramebufferSize()
 		w.sizeEv.W = w
 		w.sizeEv.Width = width
@@ -458,210 +353,164 @@ func (m *glfwManager) CreateWindow(width, height int, title string, fullscreen b
 	})
 
 	// Set window position event callback to dispatch event
-	win.SetPosCallback(func(x *glfw.Window, xpos int, ypos int) {
-
-		w.posEv.W = w
+	w.SetPosCallback(func(x *glfw.Window, xpos int, ypos int) {
 		w.posEv.Xpos = xpos
 		w.posEv.Ypos = ypos
 		w.Dispatch(OnWindowPos, &w.posEv)
 	})
 
 	// Set window cursor position event callback to dispatch event
-	win.SetCursorPosCallback(func(x *glfw.Window, xpos float64, ypos float64) {
-
-		w.cursorEv.W = w
+	w.SetCursorPosCallback(func(x *glfw.Window, xpos float64, ypos float64) {
 		w.cursorEv.Xpos = float32(xpos * w.scaleX)
 		w.cursorEv.Ypos = float32(ypos * w.scaleY)
+		w.cursorEv.Mods = w.mods
 		w.Dispatch(OnCursor, &w.cursorEv)
 	})
 
 	// Set mouse wheel scroll event callback to dispatch event
-	win.SetScrollCallback(func(x *glfw.Window, xoff float64, yoff float64) {
-
-		w.scrollEv.W = w
+	w.SetScrollCallback(func(x *glfw.Window, xoff float64, yoff float64) {
 		w.scrollEv.Xoffset = float32(xoff)
 		w.scrollEv.Yoffset = float32(yoff)
+		w.scrollEv.Mods = w.mods
 		w.Dispatch(OnScroll, &w.scrollEv)
 	})
 
-	// Sets full screen if requested
-	if fullscreen {
-		w.SetFullScreen(true)
-	}
-
-	// Create OpenGL state
-	gl, err := gls.New()
-	if err != nil {
-		return nil, err
-	}
-	w.gls = gl
-
-	return w, nil
+	win = w // Set singleton
+	return nil
 }
 
-// Gls returns the associated OpenGL state
-func (w *glfwWindow) Gls() *gls.GLS {
+// Gls returns the associated OpenGL state.
+func (w *GlfwWindow) Gls() *gls.GLS {
 
 	return w.gls
 }
 
-// Manager returns the window manager and satisfies the IWindow interface
-func (w *glfwWindow) Manager() IWindowManager {
+// Fullscreen returns whether this windows is currently fullscreen.
+func (w *GlfwWindow) Fullscreen() bool {
 
-	return w.mgr
+	return w.fullscreen
 }
 
-// MakeContextCurrent makes the OpenGL context of this window current on the calling thread
-func (w *glfwWindow) MakeContextCurrent() {
-
-	w.win.MakeContextCurrent()
-}
-
-// FullScreen returns this window full screen state for the primary monitor
-func (w *glfwWindow) FullScreen() bool {
-
-	return w.fullScreen
-}
-
-// SetFullScreen sets this window full screen state for the primary monitor
-func (w *glfwWindow) SetFullScreen(full bool) {
+// SetFullscreen sets this window as fullscreen on the primary monitor
+// TODO allow for fullscreen with resolutions different than the monitor's
+func (w *GlfwWindow) SetFullscreen(full bool) {
 
 	// If already in the desired state, nothing to do
-	if w.fullScreen == full {
+	if w.fullscreen == full {
 		return
 	}
-	// Sets this window full screen for the primary monitor
+	// Set window fullscreen on the primary monitor
 	if full {
-		// Get primary monitor
+		// Get size of primary monitor
 		mon := glfw.GetPrimaryMonitor()
 		vmode := mon.GetVideoMode()
 		width := vmode.Width
 		height := vmode.Height
-		// Saves current position and size of the window
-		w.lastX, w.lastY = w.win.GetPos()
-		w.lastWidth, w.lastHeight = w.win.GetSize()
-		// Sets monitor for full screen
-		w.win.SetMonitor(mon, 0, 0, width, height, vmode.RefreshRate)
-		w.fullScreen = true
+		// Set as fullscreen on the primary monitor
+		w.SetMonitor(mon, 0, 0, width, height, vmode.RefreshRate)
+		w.fullscreen = true
+		// Save current position and size of the window
+		w.lastX, w.lastY = w.GetPos()
+		w.lastWidth, w.lastHeight = w.GetSize()
 	} else {
 		// Restore window to previous position and size
-		w.win.SetMonitor(nil, w.lastX, w.lastY, w.lastWidth, w.lastHeight, glfw.DontCare)
-		w.fullScreen = false
+		w.SetMonitor(nil, w.lastX, w.lastY, w.lastWidth, w.lastHeight, glfw.DontCare)
+		w.fullscreen = false
 	}
 }
 
 // Destroy destroys this window and its context
-func (w *glfwWindow) Destroy() {
-
-	w.win.Destroy()
-	w.win = nil
-}
-
-// SwapBuffers swaps the front and back buffers of this window.
-// If the swap interval is greater than zero,
-// the GPU driver waits the specified number of screen updates before swapping the buffers.
-func (w *glfwWindow) SwapBuffers() {
-
-	w.win.SwapBuffers()
-}
+func (w *GlfwWindow) Destroy() {
 
-// FramebufferSize returns framebuffer size of this window
-func (w *glfwWindow) FramebufferSize() (width int, height int) {
-
-	return w.win.GetFramebufferSize()
+	w.Window.Destroy()
+	glfw.Terminate()
+	runtime.UnlockOSThread() // Important when using the execution tracer
 }
 
 // Scale returns this window's DPI scale factor (FramebufferSize / Size)
-func (w *glfwWindow) Scale() (x float64, y float64) {
+func (w *GlfwWindow) GetScale() (x float64, y float64) {
 
 	return w.scaleX, w.scaleY
 }
 
-// Size returns this window's size in screen coordinates
-func (w *glfwWindow) Size() (width int, height int) {
+// ScreenResolution returns the screen resolution
+func (w *GlfwWindow) ScreenResolution(p interface{}) (width, height int) {
 
-	return w.win.GetSize()
+	mon := glfw.GetPrimaryMonitor()
+	vmode := mon.GetVideoMode()
+	return vmode.Width, vmode.Height
 }
 
-// SetSize sets the size, in screen coordinates, of the client area of this window
-func (w *glfwWindow) SetSize(width int, height int) {
+// PollEvents process events in the event queue
+func (w *GlfwWindow) PollEvents() {
 
-	w.win.SetSize(width, height)
+	glfw.PollEvents()
 }
 
-// Pos returns the position, in screen coordinates, of the upper-left corner of the client area of this window
-func (w *glfwWindow) Pos() (xpos, ypos int) {
+// SetSwapInterval sets the number of screen updates to wait from the time SwapBuffer()
+// is called before swapping the buffers and returning.
+func (w *GlfwWindow) SetSwapInterval(interval int) {
 
-	return w.win.GetPos()
+	glfw.SwapInterval(interval)
 }
 
-// SetPos sets the position, in screen coordinates, of the upper-left corner of the client area of this window.
-// If the window is a full screen window, this function does nothing.
-func (w *glfwWindow) SetPos(xpos, ypos int) {
+// SetCursor sets the window's cursor.
+func (w *GlfwWindow) SetCursor(cursor Cursor) {
 
-	w.win.SetPos(xpos, ypos)
+	cur, ok := w.cursors[cursor]
+	if !ok {
+		panic("Invalid cursor")
+	}
+	w.Window.SetCursor(cur)
 }
 
-// SetTitle sets this window title, encoded as UTF-8
-func (w *glfwWindow) SetTitle(title string) {
-
-	w.win.SetTitle(title)
-}
+// CreateCursor creates a new custom cursor and returns an int handle.
+func (w *GlfwWindow) CreateCursor(imgFile string, xhot, yhot int) (Cursor, error) {
 
-// ShouldClose returns the current state of this window  should close flag
-func (w *glfwWindow) ShouldClose() bool {
+	// Open image file
+	file, err := os.Open(imgFile)
+	if err != nil {
+		return 0, err
+	}
+	defer file.Close()
+	// Decode image
+	img, _, err := image.Decode(file)
+	if err != nil {
+		return 0, err
+	}
+	// Create and store cursor
+	w.lastCursorKey += 1
+	w.cursors[Cursor(w.lastCursorKey)] = glfw.CreateCursor(img, xhot, yhot)
 
-	return w.win.ShouldClose()
+	return w.lastCursorKey, nil
 }
 
-// SetShouldClose sets the state of this windows should close flag
-func (w *glfwWindow) SetShouldClose(v bool) {
-
-	w.win.SetShouldClose(v)
-}
+// DisposeCursor deletes the existing custom cursor with the provided int handle.
+func (w *GlfwWindow) DisposeCursor(cursor Cursor) {
 
-// SetStandardCursor sets the window's cursor to a standard one
-func (w *glfwWindow) SetStandardCursor(cursor StandardCursor) {
-
-	switch cursor {
-	case ArrowCursor:
-		w.win.SetCursor(w.mgr.arrowCursor)
-	case IBeamCursor:
-		w.win.SetCursor(w.mgr.ibeamCursor)
-	case CrosshairCursor:
-		w.win.SetCursor(w.mgr.crosshairCursor)
-	case HandCursor:
-		w.win.SetCursor(w.mgr.handCursor)
-	case HResizeCursor:
-		w.win.SetCursor(w.mgr.hresizeCursor)
-	case VResizeCursor:
-		w.win.SetCursor(w.mgr.vresizeCursor)
-	// Non-GLFW cursors (but standard cursors for g3n)
-	case DiagResize1Cursor:
-		w.win.SetCursor(w.mgr.diag1Cursor)
-	case DiagResize2Cursor:
-		w.win.SetCursor(w.mgr.diag2Cursor)
-	default:
-		panic("Invalid cursor")
+	if cursor <= CursorLast {
+		panic("Can't dispose standard cursor")
 	}
+	w.cursors[cursor].Destroy()
+	delete(w.cursors, cursor)
 }
 
-// SetStandardCursor sets this window's cursor to a custom, user-created one
-func (w *glfwWindow) SetCustomCursor(key int) {
-
-	w.win.SetCursor(w.mgr.customCursors[key])
-}
-
-// SetInputMode changes specified input to specified state
-// Reference: http://www.glfw.org/docs/latest/group__input.html#gaa92336e173da9c8834558b54ee80563b
-func (w *glfwWindow) SetInputMode(mode InputMode, state int) {
+// DisposeAllCursors deletes all existing custom cursors.
+func (w *GlfwWindow) DisposeAllCustomCursors() {
 
-	w.win.SetInputMode(glfw.InputMode(mode), state)
+	// Destroy and delete all custom cursors
+	for key := range w.cursors {
+		if key > CursorLast {
+			w.cursors[key].Destroy()
+			delete(w.cursors, key)
+		}
+	}
+	// Set the next cursor key as the last standard cursor key + 1
+	w.lastCursorKey = CursorLast
 }
 
-// SetCursorPos sets cursor position in window coordinates
-// Reference: http://www.glfw.org/docs/latest/group__input.html#ga04b03af936d906ca123c8f4ee08b39e7
-func (w *glfwWindow) SetCursorPos(xpos, ypos float64) {
-
-	w.win.SetCursorPos(xpos, ypos)
-}
+// Center centers the window on the screen.
+//func (w *GlfwWindow) Center() {
+//
+//	// TODO
+//}

+ 52 - 57
window/window.go

@@ -2,64 +2,50 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// Package window abstracts the OpenGL Window manager
-// Currently only "glfw" is supported
+// Package window abstracts a system window.
+// Depending on the build tags it can be a GLFW desktop window or a browser WebGlCanvas.
 package window
 
 import (
+	"fmt"
 	"github.com/g3n/engine/core"
 	"github.com/g3n/engine/gls"
 )
 
-// IWindowManager is the interface for all window managers
-type IWindowManager interface {
-	ScreenResolution(interface{}) (width, height int)
-	CreateWindow(width, height int, title string, full bool) (IWindow, error)
-	CreateCursor(imgFile string, xhot, yhot int) (int, error)
-	DisposeCursor(key int)
-	DisposeAllCursors()
-	SetSwapInterval(interval int)
-	PollEvents()
-	Terminate()
+// IWindow singleton
+var win IWindow
+
+// Get returns the IWindow singleton.
+func Get() IWindow {
+	// Return singleton if already created
+	if win != nil {
+		return win
+	}
+	panic(fmt.Errorf("need to call window.Init() first"))
 }
 
 // IWindow is the interface for all windows
 type IWindow interface {
 	core.IDispatcher
 	Gls() *gls.GLS
-	Manager() IWindowManager
-	MakeContextCurrent()
-	FramebufferSize() (width int, height int)
-	Scale() (x float64, y float64)
-	Size() (width int, height int)
-	SetSize(width int, height int)
-	Pos() (xpos, ypos int)
-	SetPos(xpos, ypos int)
-	SetTitle(title string)
-	SetStandardCursor(cursor StandardCursor)
-	SetCustomCursor(int)
-	SetInputMode(mode InputMode, state int)
-	SetCursorPos(xpos, ypos float64)
-	ShouldClose() bool
-	SetShouldClose(bool)
-	FullScreen() bool
-	SetFullScreen(bool)
-	SwapBuffers()
+	GetFramebufferSize() (width int, height int)
+	GetSize() (width int, height int)
+	GetScale() (x float64, y float64)
+	CreateCursor(imgFile string, xhot, yhot int) (Cursor, error)
+	SetCursor(cursor Cursor)
+	DisposeAllCustomCursors()
 	Destroy()
 }
 
 // Key corresponds to a keyboard key.
 type Key int
 
-// ModifierKey corresponds to a modifier key.
+// ModifierKey corresponds to a set of modifier keys (bitmask).
 type ModifierKey int
 
 // MouseButton corresponds to a mouse button.
 type MouseButton int
 
-// StandardCursor corresponds to a g3n standard cursor icon.
-type StandardCursor int
-
 // Action corresponds to a key or button action.
 type Action int
 
@@ -69,56 +55,65 @@ type InputMode int
 // InputMode corresponds to an input mode.
 type CursorMode int
 
+// Cursor corresponds to a g3n standard or user-created cursor icon.
+type Cursor int
+
+// Standard cursors for G3N.
+const (
+	ArrowCursor = Cursor(iota)
+	IBeamCursor
+	CrosshairCursor
+	HandCursor
+	HResizeCursor
+	VResizeCursor
+	DiagResize1Cursor
+	DiagResize2Cursor
+	CursorLast = DiagResize2Cursor
+)
+
 //
-// Window event names using for dispatch and subscribe
+// Window event names used for dispatch and subscribe
 //
 const (
-	OnWindowPos  = "win.OnWindowPos"
-	OnWindowSize = "win.OnWindowSize"
-	OnKeyUp      = "win.OnKeyUp"
-	OnKeyDown    = "win.OnKeyDown"
-	OnKeyRepeat  = "win.OnKeyRepeat"
-	OnChar       = "win.OnChar"
-	OnCursor     = "win.OnCursor"
-	OnMouseUp    = "win.OnMouseUp"
-	OnMouseDown  = "win.OnMouseDown"
-	OnScroll     = "win.OnScroll"
-	OnFrame      = "win.OnFrame"
+	OnWindowPos  = "w.OnWindowPos"
+	OnWindowSize = "w.OnWindowSize"
+	OnKeyUp      = "w.OnKeyUp"
+	OnKeyDown    = "w.OnKeyDown"
+	OnKeyRepeat  = "w.OnKeyRepeat"
+	OnChar       = "w.OnChar"
+	OnCursor     = "w.OnCursor"
+	OnMouseUp    = "w.OnMouseUp"
+	OnMouseDown  = "w.OnMouseDown"
+	OnScroll     = "w.OnScroll"
 )
 
 // PosEvent describes a windows position changed event
 type PosEvent struct {
-	W    IWindow
 	Xpos int
 	Ypos int
 }
 
 // SizeEvent describers a window size changed event
 type SizeEvent struct {
-	W      IWindow
 	Width  int
 	Height int
 }
 
 // KeyEvent describes a window key event
 type KeyEvent struct {
-	W        IWindow
-	Keycode  Key
-	Scancode int
-	Action   Action
-	Mods     ModifierKey
+	Keycode Key
+	Action  Action
+	Mods    ModifierKey
 }
 
 // CharEvent describes a window char event
 type CharEvent struct {
-	W    IWindow
 	Char rune
 	Mods ModifierKey
 }
 
 // MouseEvent describes a mouse event over the window
 type MouseEvent struct {
-	W      IWindow
 	Xpos   float32
 	Ypos   float32
 	Button MouseButton
@@ -128,14 +123,14 @@ type MouseEvent struct {
 
 // CursorEvent describes a cursor position changed event
 type CursorEvent struct {
-	W    IWindow
 	Xpos float32
 	Ypos float32
+	Mods ModifierKey
 }
 
 // ScrollEvent describes a scroll event
 type ScrollEvent struct {
-	W       IWindow
 	Xoffset float32
 	Yoffset float32
+	Mods    ModifierKey
 }