mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-11-10 13:07:26 +01:00
bda11b0e17
This is a preparation to introduce RunGame function. Updates # 943 (Fix this line before committing)
1119 lines
26 KiB
Go
1119 lines
26 KiB
Go
// Copyright 2015 Hajime Hoshi
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// +build darwin freebsd linux windows
|
|
// +build !js
|
|
// +build !android
|
|
// +build !ios
|
|
|
|
package glfw
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"image"
|
|
"os"
|
|
"runtime"
|
|
"sync"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"github.com/hajimehoshi/ebiten/internal/devicescale"
|
|
"github.com/hajimehoshi/ebiten/internal/driver"
|
|
"github.com/hajimehoshi/ebiten/internal/glfw"
|
|
"github.com/hajimehoshi/ebiten/internal/hooks"
|
|
"github.com/hajimehoshi/ebiten/internal/thread"
|
|
)
|
|
|
|
type UserInterface struct {
|
|
title string
|
|
window *glfw.Window
|
|
|
|
// windowWidth and windowHeight represents a window size.
|
|
// The unit is device-dependent pixels.
|
|
windowWidth int
|
|
windowHeight int
|
|
|
|
running bool
|
|
toChangeSize bool
|
|
origPosX int
|
|
origPosY int
|
|
runnableInBackground bool
|
|
vsync bool
|
|
|
|
lastDeviceScaleFactor float64
|
|
|
|
initMonitor *glfw.Monitor
|
|
initFullscreenWidthInDP int
|
|
initFullscreenHeightInDP int
|
|
initFullscreen bool
|
|
initCursorMode driver.CursorMode
|
|
initWindowDecorated bool
|
|
initWindowResizable bool
|
|
initWindowPositionX int
|
|
initWindowPositionY int
|
|
initScreenTransparent bool
|
|
initIconImages []image.Image
|
|
|
|
reqWidth int
|
|
reqHeight int
|
|
|
|
graphics driver.Graphics
|
|
input Input
|
|
|
|
t *thread.Thread
|
|
m sync.RWMutex
|
|
}
|
|
|
|
const (
|
|
maxInt = int(^uint(0) >> 1)
|
|
minInt = -maxInt - 1
|
|
invalidPos = minInt
|
|
)
|
|
|
|
var (
|
|
theUI = &UserInterface{
|
|
origPosX: invalidPos,
|
|
origPosY: invalidPos,
|
|
initCursorMode: driver.CursorModeVisible,
|
|
initWindowDecorated: true,
|
|
initWindowPositionX: invalidPos,
|
|
initWindowPositionY: invalidPos,
|
|
vsync: true,
|
|
}
|
|
)
|
|
|
|
func init() {
|
|
theUI.input.ui = theUI
|
|
}
|
|
|
|
func Get() *UserInterface {
|
|
return theUI
|
|
}
|
|
|
|
func init() {
|
|
hideConsoleWindowOnWindows()
|
|
if err := initialize(); err != nil {
|
|
panic(err)
|
|
}
|
|
glfw.SetMonitorCallback(func(monitor *glfw.Monitor, event glfw.PeripheralEvent) {
|
|
cacheMonitors()
|
|
})
|
|
cacheMonitors()
|
|
}
|
|
|
|
func initialize() error {
|
|
if err := glfw.Init(); err != nil {
|
|
return err
|
|
}
|
|
glfw.WindowHint(glfw.Visible, glfw.False)
|
|
|
|
// Create a window to set the initial monitor.
|
|
w, err := glfw.CreateWindow(16, 16, "", nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if w == nil {
|
|
// This can happen on Windows Remote Desktop (#903).
|
|
panic("glfw: glfw.CreateWindow must not return nil")
|
|
}
|
|
|
|
// TODO: Fix this hack. currentMonitorImpl now requires u.window on POSIX.
|
|
theUI.window = w
|
|
theUI.initMonitor = theUI.currentMonitorFromPosition()
|
|
v := theUI.initMonitor.GetVideoMode()
|
|
theUI.initFullscreenWidthInDP = int(theUI.toDeviceIndependentPixel(float64(v.Width)))
|
|
theUI.initFullscreenHeightInDP = int(theUI.toDeviceIndependentPixel(float64(v.Height)))
|
|
theUI.window.Destroy()
|
|
theUI.window = nil
|
|
|
|
return nil
|
|
}
|
|
|
|
type cachedMonitor struct {
|
|
m *glfw.Monitor
|
|
vm *glfw.VidMode
|
|
// Pos of monitor in virtual coords
|
|
x int
|
|
y int
|
|
}
|
|
|
|
// monitors is the monitor list cache for desktop glfw compile targets.
|
|
// populated by 'cacheMonitors' which is called on init and every
|
|
// monitor config change event.
|
|
//
|
|
// monitors must be manipulated on the main thread.
|
|
var monitors []*cachedMonitor
|
|
|
|
func cacheMonitors() {
|
|
monitors = nil
|
|
ms := glfw.GetMonitors()
|
|
for _, m := range ms {
|
|
x, y := m.GetPos()
|
|
monitors = append(monitors, &cachedMonitor{
|
|
m: m,
|
|
vm: m.GetVideoMode(),
|
|
x: x,
|
|
y: y,
|
|
})
|
|
}
|
|
}
|
|
|
|
// getCachedMonitor returns a monitor for the given window x/y
|
|
// returns false if monitor is not found.
|
|
//
|
|
// getCachedMonitor must be called on the main thread.
|
|
func getCachedMonitor(wx, wy int) (*cachedMonitor, bool) {
|
|
for _, m := range monitors {
|
|
if m.x <= wx && wx < m.x+m.vm.Width && m.y <= wy && wy < m.y+m.vm.Height {
|
|
return m, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func (u *UserInterface) isRunning() bool {
|
|
u.m.RLock()
|
|
v := u.running
|
|
u.m.RUnlock()
|
|
return v
|
|
}
|
|
|
|
func (u *UserInterface) setRunning(running bool) {
|
|
u.m.Lock()
|
|
u.running = running
|
|
u.m.Unlock()
|
|
}
|
|
|
|
func (u *UserInterface) isInitFullscreen() bool {
|
|
u.m.RLock()
|
|
v := u.initFullscreen
|
|
u.m.RUnlock()
|
|
return v
|
|
}
|
|
|
|
func (u *UserInterface) setInitFullscreen(initFullscreen bool) {
|
|
u.m.Lock()
|
|
u.initFullscreen = initFullscreen
|
|
u.m.Unlock()
|
|
}
|
|
|
|
func (u *UserInterface) getInitCursorMode() driver.CursorMode {
|
|
u.m.RLock()
|
|
v := u.initCursorMode
|
|
u.m.RUnlock()
|
|
return v
|
|
}
|
|
|
|
func (u *UserInterface) setInitCursorMode(mode driver.CursorMode) {
|
|
u.m.Lock()
|
|
u.initCursorMode = mode
|
|
u.m.Unlock()
|
|
}
|
|
|
|
func (u *UserInterface) isInitWindowDecorated() bool {
|
|
u.m.RLock()
|
|
v := u.initWindowDecorated
|
|
u.m.RUnlock()
|
|
return v
|
|
}
|
|
|
|
func (u *UserInterface) setInitWindowDecorated(decorated bool) {
|
|
u.m.Lock()
|
|
u.initWindowDecorated = decorated
|
|
u.m.Unlock()
|
|
}
|
|
|
|
func (u *UserInterface) isRunnableInBackground() bool {
|
|
u.m.RLock()
|
|
v := u.runnableInBackground
|
|
u.m.RUnlock()
|
|
return v
|
|
}
|
|
|
|
func (u *UserInterface) setRunnableInBackground(runnableInBackground bool) {
|
|
u.m.Lock()
|
|
u.runnableInBackground = runnableInBackground
|
|
u.m.Unlock()
|
|
}
|
|
|
|
func (u *UserInterface) isInitWindowResizable() bool {
|
|
u.m.RLock()
|
|
v := u.initWindowResizable
|
|
u.m.RUnlock()
|
|
return v
|
|
}
|
|
|
|
func (u *UserInterface) setInitWindowResizable(resizable bool) {
|
|
u.m.Lock()
|
|
u.initWindowResizable = resizable
|
|
u.m.Unlock()
|
|
}
|
|
|
|
func (u *UserInterface) isInitScreenTransparent() bool {
|
|
u.m.RLock()
|
|
v := u.initScreenTransparent
|
|
u.m.RUnlock()
|
|
return v
|
|
}
|
|
|
|
func (u *UserInterface) setInitScreenTransparent(transparent bool) {
|
|
u.m.RLock()
|
|
u.initScreenTransparent = transparent
|
|
u.m.RUnlock()
|
|
}
|
|
|
|
func (u *UserInterface) getInitIconImages() []image.Image {
|
|
u.m.RLock()
|
|
i := u.initIconImages
|
|
u.m.RUnlock()
|
|
return i
|
|
}
|
|
|
|
func (u *UserInterface) setInitIconImages(iconImages []image.Image) {
|
|
u.m.Lock()
|
|
u.initIconImages = iconImages
|
|
u.m.Unlock()
|
|
}
|
|
|
|
func (u *UserInterface) getInitWindowPosition() (int, int) {
|
|
u.m.RLock()
|
|
defer u.m.RUnlock()
|
|
if u.initWindowPositionX != invalidPos && u.initWindowPositionY != invalidPos {
|
|
return u.initWindowPositionX, u.initWindowPositionY
|
|
}
|
|
return invalidPos, invalidPos
|
|
}
|
|
|
|
func (u *UserInterface) setInitWindowPosition(x, y int) {
|
|
u.m.Lock()
|
|
defer u.m.Unlock()
|
|
|
|
u.initWindowPositionX = x
|
|
u.initWindowPositionY = y
|
|
}
|
|
|
|
// toDeviceIndependentPixel must be called from the main thread.
|
|
func (u *UserInterface) toDeviceIndependentPixel(x float64) float64 {
|
|
return x / u.glfwScale()
|
|
}
|
|
|
|
// toDeviceDependentPixel must be called from the main thread.
|
|
func (u *UserInterface) toDeviceDependentPixel(x float64) float64 {
|
|
return x * u.glfwScale()
|
|
}
|
|
|
|
func (u *UserInterface) ScreenSizeInFullscreen() (int, int) {
|
|
if !u.isRunning() {
|
|
return u.initFullscreenWidthInDP, u.initFullscreenHeightInDP
|
|
}
|
|
|
|
var w, h int
|
|
_ = u.t.Call(func() error {
|
|
v := u.currentMonitor().GetVideoMode()
|
|
w = int(u.toDeviceIndependentPixel(float64(v.Width)))
|
|
h = int(u.toDeviceIndependentPixel(float64(v.Height)))
|
|
return nil
|
|
})
|
|
return w, h
|
|
}
|
|
|
|
// isFullscreen must be called from the main thread.
|
|
func (u *UserInterface) isFullscreen() bool {
|
|
if !u.isRunning() {
|
|
panic("glfw: isFullscreen can't be called before the main loop starts")
|
|
}
|
|
return u.window.GetMonitor() != nil
|
|
}
|
|
|
|
func (u *UserInterface) IsFullscreen() bool {
|
|
if !u.isRunning() {
|
|
return u.isInitFullscreen()
|
|
}
|
|
b := false
|
|
_ = u.t.Call(func() error {
|
|
b = u.isFullscreen()
|
|
return nil
|
|
})
|
|
return b
|
|
}
|
|
|
|
func (u *UserInterface) SetFullscreen(fullscreen bool) {
|
|
if !u.isRunning() {
|
|
u.setInitFullscreen(fullscreen)
|
|
return
|
|
}
|
|
|
|
var update bool
|
|
_ = u.t.Call(func() error {
|
|
update = u.isFullscreen() != fullscreen
|
|
return nil
|
|
})
|
|
if !update {
|
|
return
|
|
}
|
|
|
|
var w, h int
|
|
_ = u.t.Call(func() error {
|
|
w, h = u.windowWidth, u.windowHeight
|
|
return nil
|
|
})
|
|
u.setWindowSize(w, h, fullscreen, u.vsync)
|
|
}
|
|
|
|
func (u *UserInterface) SetRunnableInBackground(runnableInBackground bool) {
|
|
u.setRunnableInBackground(runnableInBackground)
|
|
}
|
|
|
|
func (u *UserInterface) IsRunnableInBackground() bool {
|
|
return u.isRunnableInBackground()
|
|
}
|
|
|
|
func (u *UserInterface) SetVsyncEnabled(enabled bool) {
|
|
if !u.isRunning() {
|
|
// In general, m is used for locking init* values.
|
|
// m is not used for updating vsync in setWindowSize so far, but
|
|
// it should be OK since any goroutines can't reach here when
|
|
// the game already starts and setWindowSize can be called.
|
|
u.m.Lock()
|
|
u.vsync = enabled
|
|
u.m.Unlock()
|
|
return
|
|
}
|
|
var w, h int
|
|
_ = u.t.Call(func() error {
|
|
w, h = u.windowWidth, u.windowHeight
|
|
return nil
|
|
})
|
|
u.setWindowSize(w, h, u.isFullscreen(), enabled)
|
|
}
|
|
|
|
func (u *UserInterface) IsVsyncEnabled() bool {
|
|
u.m.RLock()
|
|
r := u.vsync
|
|
u.m.RUnlock()
|
|
return r
|
|
}
|
|
|
|
func (u *UserInterface) SetWindowTitle(title string) {
|
|
if !u.isRunning() {
|
|
return
|
|
}
|
|
u.title = title
|
|
_ = u.t.Call(func() error {
|
|
u.window.SetTitle(title)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (u *UserInterface) SetWindowIcon(iconImages []image.Image) {
|
|
if !u.isRunning() {
|
|
u.setInitIconImages(iconImages)
|
|
return
|
|
}
|
|
_ = u.t.Call(func() error {
|
|
u.window.SetIcon(iconImages)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (u *UserInterface) CursorMode() driver.CursorMode {
|
|
if !u.isRunning() {
|
|
return u.getInitCursorMode()
|
|
}
|
|
var v driver.CursorMode
|
|
_ = u.t.Call(func() error {
|
|
mode := u.window.GetInputMode(glfw.CursorMode)
|
|
switch mode {
|
|
case glfw.CursorNormal:
|
|
v = driver.CursorModeVisible
|
|
case glfw.CursorHidden:
|
|
v = driver.CursorModeHidden
|
|
case glfw.CursorDisabled:
|
|
v = driver.CursorModeCaptured
|
|
default:
|
|
panic(fmt.Sprintf("invalid cursor mode: %d", mode))
|
|
}
|
|
return nil
|
|
})
|
|
return v
|
|
}
|
|
|
|
func (u *UserInterface) SetCursorMode(mode driver.CursorMode) {
|
|
if !u.isRunning() {
|
|
u.setInitCursorMode(mode)
|
|
return
|
|
}
|
|
_ = u.t.Call(func() error {
|
|
var c int
|
|
switch mode {
|
|
case driver.CursorModeVisible:
|
|
c = glfw.CursorNormal
|
|
case driver.CursorModeHidden:
|
|
c = glfw.CursorHidden
|
|
case driver.CursorModeCaptured:
|
|
c = glfw.CursorDisabled
|
|
default:
|
|
panic(fmt.Sprintf("invalid cursor mode: %d", mode))
|
|
}
|
|
u.window.SetInputMode(glfw.CursorMode, c)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (u *UserInterface) IsWindowDecorated() bool {
|
|
if !u.isRunning() {
|
|
return u.isInitWindowDecorated()
|
|
}
|
|
v := false
|
|
_ = u.t.Call(func() error {
|
|
v = u.window.GetAttrib(glfw.Decorated) == glfw.True
|
|
return nil
|
|
})
|
|
return v
|
|
}
|
|
|
|
func (u *UserInterface) SetWindowDecorated(decorated bool) {
|
|
if !u.isRunning() {
|
|
u.setInitWindowDecorated(decorated)
|
|
return
|
|
}
|
|
|
|
_ = u.t.Call(func() error {
|
|
v := glfw.False
|
|
if decorated {
|
|
v = glfw.True
|
|
}
|
|
u.window.SetAttrib(glfw.Decorated, v)
|
|
|
|
// The title can be lost when the decoration is gone. Recover this.
|
|
if v == glfw.True {
|
|
u.window.SetTitle(u.title)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (u *UserInterface) IsWindowResizable() bool {
|
|
if !u.isRunning() {
|
|
return u.isInitWindowResizable()
|
|
}
|
|
v := false
|
|
_ = u.t.Call(func() error {
|
|
v = u.window.GetAttrib(glfw.Resizable) == glfw.True
|
|
return nil
|
|
})
|
|
return v
|
|
}
|
|
|
|
func (u *UserInterface) SetWindowResizable(resizable bool) {
|
|
if !u.isRunning() {
|
|
u.setInitWindowResizable(resizable)
|
|
return
|
|
}
|
|
|
|
panic("glfw: SetWindowResizable can't be called after the main loop so far.")
|
|
|
|
// TODO: Now SetAttrib doesn't exist on GLFW 3.2. Revisit later (#556).
|
|
}
|
|
|
|
func (u *UserInterface) DeviceScaleFactor() float64 {
|
|
if !u.isRunning() {
|
|
return devicescale.GetAt(u.initMonitor.GetPos())
|
|
}
|
|
|
|
f := 0.0
|
|
_ = u.t.Call(func() error {
|
|
f = u.deviceScaleFactor()
|
|
return nil
|
|
})
|
|
return f
|
|
}
|
|
|
|
// deviceScaleFactor must be called from the main thread.
|
|
func (u *UserInterface) deviceScaleFactor() float64 {
|
|
// Avoid calling monitor.GetPos if we have the monitor position cached already.
|
|
if cm, ok := getCachedMonitor(u.window.GetPos()); ok {
|
|
return devicescale.GetAt(cm.x, cm.y)
|
|
}
|
|
// TODO: When is this reached?
|
|
return devicescale.GetAt(u.currentMonitor().GetPos())
|
|
}
|
|
|
|
func init() {
|
|
// Lock the main thread.
|
|
runtime.LockOSThread()
|
|
}
|
|
|
|
func (u *UserInterface) Run(width, height int, scale float64, title string, uicontext driver.UIContext, graphics driver.Graphics) error {
|
|
// Initialize the main thread first so the thread is available at u.run (#809).
|
|
u.t = thread.New()
|
|
u.graphics = graphics
|
|
u.graphics.SetThread(u.t)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
ch := make(chan error, 1)
|
|
go func() {
|
|
defer cancel()
|
|
defer close(ch)
|
|
if err := u.run(width, height, scale, title, uicontext); err != nil {
|
|
ch <- err
|
|
}
|
|
}()
|
|
|
|
u.setRunning(true)
|
|
u.t.Loop(ctx)
|
|
u.setRunning(false)
|
|
return <-ch
|
|
}
|
|
|
|
func (u *UserInterface) RunWithoutMainLoop(width, height int, scale float64, title string, context driver.UIContext, graphics driver.Graphics) <-chan error {
|
|
panic("glfw: RunWithoutMainLoop is not implemented")
|
|
}
|
|
|
|
// createWindow creates a GLFW window.
|
|
//
|
|
// createWindow must be called from the main thread.
|
|
//
|
|
// createWindow does not set the position or size so far.
|
|
func (u *UserInterface) createWindow() error {
|
|
if u.window != nil {
|
|
panic("glfw: u.window must not exist at createWindow")
|
|
}
|
|
|
|
// As a start, create a window with temporary size to create OpenGL context thread.
|
|
window, err := glfw.CreateWindow(16, 16, "", nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
u.window = window
|
|
|
|
if u.graphics.IsGL() {
|
|
u.window.MakeContextCurrent()
|
|
}
|
|
|
|
u.window.SetInputMode(glfw.StickyMouseButtonsMode, glfw.True)
|
|
u.window.SetInputMode(glfw.StickyKeysMode, glfw.True)
|
|
|
|
mode := glfw.CursorNormal
|
|
switch u.getInitCursorMode() {
|
|
case driver.CursorModeHidden:
|
|
mode = glfw.CursorHidden
|
|
case driver.CursorModeCaptured:
|
|
mode = glfw.CursorDisabled
|
|
}
|
|
u.window.SetInputMode(glfw.CursorMode, mode)
|
|
u.window.SetTitle(u.title)
|
|
// TODO: Set icons
|
|
|
|
u.window.SetSizeCallback(func(_ *glfw.Window, width, height int) {
|
|
if u.window.GetAttrib(glfw.Resizable) == glfw.False {
|
|
return
|
|
}
|
|
if u.isFullscreen() {
|
|
return
|
|
}
|
|
u.reqWidth = width
|
|
u.reqHeight = height
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (u *UserInterface) run(width, height int, scale float64, title string, context driver.UIContext) error {
|
|
var (
|
|
m *glfw.Monitor
|
|
mx, my int
|
|
v *glfw.VidMode
|
|
ww, wh int
|
|
)
|
|
|
|
if err := u.t.Call(func() error {
|
|
if u.graphics.IsGL() {
|
|
glfw.WindowHint(glfw.ContextVersionMajor, 2)
|
|
glfw.WindowHint(glfw.ContextVersionMinor, 1)
|
|
} else {
|
|
glfw.WindowHint(glfw.ClientAPI, glfw.NoAPI)
|
|
}
|
|
|
|
decorated := glfw.False
|
|
if u.isInitWindowDecorated() {
|
|
decorated = glfw.True
|
|
}
|
|
glfw.WindowHint(glfw.Decorated, decorated)
|
|
|
|
transparent := glfw.False
|
|
if u.isInitScreenTransparent() {
|
|
transparent = glfw.True
|
|
}
|
|
glfw.WindowHint(glfw.TransparentFramebuffer, transparent)
|
|
u.graphics.SetTransparent(u.isInitScreenTransparent())
|
|
|
|
resizable := glfw.False
|
|
if u.isInitWindowResizable() {
|
|
resizable = glfw.True
|
|
}
|
|
glfw.WindowHint(glfw.Resizable, resizable)
|
|
|
|
// Set the window visible explicitly or the application freezes on Wayland (#974).
|
|
if os.Getenv("WAYLAND_DISPLAY") != "" {
|
|
glfw.WindowHint(glfw.Visible, glfw.True)
|
|
}
|
|
|
|
if err := u.createWindow(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if i := u.getInitIconImages(); i != nil {
|
|
u.window.SetIcon(i)
|
|
}
|
|
|
|
// Get the monitor before showing the window.
|
|
//
|
|
// On Windows, there are two types of windows:
|
|
//
|
|
// active window: The window that has input-focus and attached to the calling thread.
|
|
// foreground window: The window that has input-focus: this can be in another process
|
|
//
|
|
// currentMonitor returns the monitor for the active window when possible and then the monitor for
|
|
// the foreground window as fallback. In the current situation, the current window is hidden and
|
|
// there is not the active window but the foreground window. After showing the current window, the
|
|
// current window will be the active window. Thus, currentMonitor result varies before and after
|
|
// showing the window.
|
|
m = u.currentMonitor()
|
|
mx, my = m.GetPos()
|
|
v = m.GetVideoMode()
|
|
|
|
ww = int(u.toDeviceDependentPixel(float64(width) * scale))
|
|
wh = int(u.toDeviceDependentPixel(float64(height) * scale))
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
// The game is in window mode (not fullscreen mode) at the first state.
|
|
// Don't refer u.initFullscreen here to avoid some GLFW problems.
|
|
u.setWindowSize(ww, wh, false, u.vsync)
|
|
|
|
_ = u.t.Call(func() error {
|
|
// Get the window size before showing it. Showing the window might change the current monitor which
|
|
// affects deviceDependentWindowSize result.
|
|
w, h := u.window.GetSize()
|
|
|
|
u.title = title
|
|
u.window.SetTitle(title)
|
|
|
|
x, y := u.getInitWindowPosition()
|
|
if x == invalidPos || y == invalidPos {
|
|
x = mx + (v.Width-w)/2
|
|
y = my + (v.Height-h)/3
|
|
}
|
|
// Adjusting the position is needed only when the monitor is primary. (#829)
|
|
if mx == 0 && my == 0 {
|
|
x, y = adjustWindowPosition(x, y)
|
|
}
|
|
u.window.SetPos(x, y)
|
|
u.window.Show()
|
|
|
|
return nil
|
|
})
|
|
|
|
var w unsafe.Pointer
|
|
_ = u.t.Call(func() error {
|
|
w = u.nativeWindow()
|
|
return nil
|
|
})
|
|
u.graphics.SetWindow(w)
|
|
return u.loop(context)
|
|
}
|
|
|
|
func (u *UserInterface) updateSize(context driver.UIContext) {
|
|
var w, h int
|
|
_ = u.t.Call(func() error {
|
|
w, h = u.windowWidth, u.windowHeight
|
|
return nil
|
|
})
|
|
u.setWindowSize(w, h, u.isFullscreen(), u.vsync)
|
|
|
|
sizeChanged := false
|
|
_ = u.t.Call(func() error {
|
|
if !u.toChangeSize {
|
|
return nil
|
|
}
|
|
|
|
u.toChangeSize = false
|
|
sizeChanged = true
|
|
return nil
|
|
})
|
|
if sizeChanged {
|
|
var w, h float64
|
|
_ = u.t.Call(func() error {
|
|
var ww, wh int
|
|
if u.isFullscreen() {
|
|
v := u.currentMonitor().GetVideoMode()
|
|
ww = v.Width
|
|
wh = v.Height
|
|
} else {
|
|
ww, wh = u.windowWidth, u.windowHeight
|
|
}
|
|
w = u.toDeviceIndependentPixel(float64(ww))
|
|
h = u.toDeviceIndependentPixel(float64(wh))
|
|
return nil
|
|
})
|
|
context.Layout(w, h)
|
|
}
|
|
}
|
|
|
|
func (u *UserInterface) update(context driver.UIContext) error {
|
|
shouldClose := false
|
|
_ = u.t.Call(func() error {
|
|
shouldClose = u.window.ShouldClose()
|
|
return nil
|
|
})
|
|
if shouldClose {
|
|
return driver.RegularTermination
|
|
}
|
|
|
|
if u.isInitFullscreen() {
|
|
var w, h int
|
|
_ = u.t.Call(func() error {
|
|
w, h = u.window.GetSize()
|
|
return nil
|
|
})
|
|
u.setWindowSize(w, h, true, u.vsync)
|
|
u.setInitFullscreen(false)
|
|
}
|
|
|
|
// This call is needed for initialization.
|
|
u.updateSize(context)
|
|
|
|
_ = u.t.Call(func() error {
|
|
glfw.PollEvents()
|
|
return nil
|
|
})
|
|
u.input.update(u.window, context)
|
|
_ = u.t.Call(func() error {
|
|
defer hooks.ResumeAudio()
|
|
|
|
for !u.isRunnableInBackground() && u.window.GetAttrib(glfw.Focused) == 0 {
|
|
hooks.SuspendAudio()
|
|
// Wait for an arbitrary period to avoid busy loop.
|
|
time.Sleep(time.Second / 60)
|
|
glfw.PollEvents()
|
|
if u.window.ShouldClose() {
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err := context.Update(func() {
|
|
// The offscreens must be updated every frame (#490).
|
|
u.updateSize(context)
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update the screen size when the window is resizable.
|
|
var w, h int
|
|
_ = u.t.Call(func() error {
|
|
w, h = u.reqWidth, u.reqHeight
|
|
return nil
|
|
})
|
|
if w != 0 || h != 0 {
|
|
u.setWindowSize(w, h, u.isFullscreen(), u.vsync)
|
|
}
|
|
_ = u.t.Call(func() error {
|
|
u.reqWidth = 0
|
|
u.reqHeight = 0
|
|
return nil
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (u *UserInterface) loop(context driver.UIContext) error {
|
|
defer func() {
|
|
_ = u.t.Call(func() error {
|
|
glfw.Terminate()
|
|
return nil
|
|
})
|
|
}()
|
|
for {
|
|
var unfocused bool
|
|
|
|
// On Windows, the focusing state might be always false (#987).
|
|
// On Windows, even if a window is in another workspace, vsync seems to work.
|
|
// Then let's assume the window is alwasy 'focused' as a workaround.
|
|
if runtime.GOOS != "windows" {
|
|
unfocused = u.window.GetAttrib(glfw.Focused) == glfw.False
|
|
}
|
|
|
|
var t1, t2 time.Time
|
|
|
|
if unfocused {
|
|
t1 = time.Now()
|
|
}
|
|
if err := u.update(context); err != nil {
|
|
return err
|
|
}
|
|
|
|
_ = u.t.Call(func() error {
|
|
u.swapBuffers()
|
|
return nil
|
|
})
|
|
if unfocused {
|
|
t2 = time.Now()
|
|
}
|
|
|
|
// When a window is not focused, SwapBuffers might return immediately and CPU might be busy.
|
|
// Mitigate this by sleeping (#982).
|
|
if unfocused {
|
|
d := t2.Sub(t1)
|
|
const wait = time.Second / 60
|
|
if d < wait {
|
|
time.Sleep(wait - d)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// swapBuffers must be called from the main thread.
|
|
func (u *UserInterface) swapBuffers() {
|
|
if u.graphics.IsGL() {
|
|
u.window.SwapBuffers()
|
|
}
|
|
}
|
|
|
|
func (u *UserInterface) setWindowSize(width, height int, fullscreen bool, vsync bool) {
|
|
windowRecreated := false
|
|
|
|
_ = u.t.Call(func() error {
|
|
if u.windowWidth == width && u.windowHeight == height && u.isFullscreen() == fullscreen && u.vsync == vsync && u.lastDeviceScaleFactor == u.deviceScaleFactor() {
|
|
return nil
|
|
}
|
|
|
|
if width < 1 {
|
|
width = 1
|
|
}
|
|
if height < 1 {
|
|
height = 1
|
|
}
|
|
|
|
u.vsync = vsync
|
|
u.lastDeviceScaleFactor = u.deviceScaleFactor()
|
|
|
|
// To make sure the current existing framebuffers are rendered,
|
|
// swap buffers here before SetSize is called.
|
|
u.swapBuffers()
|
|
|
|
if fullscreen {
|
|
if u.origPosX == invalidPos || u.origPosY == invalidPos {
|
|
u.origPosX, u.origPosY = u.window.GetPos()
|
|
}
|
|
m := u.currentMonitor()
|
|
v := m.GetVideoMode()
|
|
u.window.SetMonitor(m, 0, 0, v.Width, v.Height, v.RefreshRate)
|
|
|
|
// Swapping buffer is necesary to prevent the image lag (#1004).
|
|
// TODO: This might not work when vsync is disabled.
|
|
if u.graphics.IsGL() {
|
|
glfw.PollEvents()
|
|
u.swapBuffers()
|
|
}
|
|
} else {
|
|
if u.window.GetMonitor() != nil {
|
|
if u.graphics.IsGL() {
|
|
// When OpenGL is used, swapping buffer is enough to solve the image-lag
|
|
// issue (#1004). Rather, recreating window destroys GPU resources.
|
|
// TODO: This might not work when vsync is disabled.
|
|
u.window.SetMonitor(nil, 0, 0, 16, 16, 0)
|
|
glfw.PollEvents()
|
|
u.swapBuffers()
|
|
} else {
|
|
// Recreate the window since an image lag remains after coming back from
|
|
// fullscreen (#1004).
|
|
if u.window != nil {
|
|
u.window.Destroy()
|
|
u.window = nil
|
|
}
|
|
if err := u.createWindow(); err != nil {
|
|
// TODO: This should return an error.
|
|
panic(fmt.Sprintf("glfw: failed to recreate window: %v", err))
|
|
}
|
|
u.window.Show()
|
|
windowRecreated = true
|
|
}
|
|
}
|
|
|
|
// On Windows, giving a too small width doesn't call a callback (#165).
|
|
// To prevent hanging up, return asap if the width is too small.
|
|
// 126 is an arbitrary number and I guess this is small enough.
|
|
minWindowWidth := int(u.toDeviceDependentPixel(126))
|
|
if u.window.GetAttrib(glfw.Decorated) == glfw.False {
|
|
minWindowWidth = 1
|
|
}
|
|
if width < minWindowWidth {
|
|
width = minWindowWidth
|
|
}
|
|
|
|
if u.origPosX != invalidPos && u.origPosY != invalidPos {
|
|
x := u.origPosX
|
|
y := u.origPosY
|
|
u.window.SetPos(x, y)
|
|
// Dirty hack for macOS (#703). Rendering doesn't work correctly with one SetPos, but
|
|
// work with two or more SetPos.
|
|
if runtime.GOOS == "darwin" {
|
|
u.window.SetPos(x+1, y)
|
|
u.window.SetPos(x, y)
|
|
}
|
|
u.origPosX = invalidPos
|
|
u.origPosY = invalidPos
|
|
}
|
|
|
|
// Set the window size after the position. The order matters.
|
|
// In the opposite order, the window size might not be correct when going back from fullscreen with multi monitors.
|
|
oldW, oldH := u.window.GetSize()
|
|
newW := width
|
|
newH := height
|
|
if oldW != newW || oldH != newH {
|
|
ch := make(chan struct{})
|
|
u.window.SetFramebufferSizeCallback(func(_ *glfw.Window, _, _ int) {
|
|
u.window.SetFramebufferSizeCallback(nil)
|
|
close(ch)
|
|
})
|
|
u.window.SetSize(newW, newH)
|
|
event:
|
|
for {
|
|
glfw.PollEvents()
|
|
select {
|
|
case <-ch:
|
|
break event
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
// Window title might be lost on macOS after coming back from fullscreen.
|
|
u.window.SetTitle(u.title)
|
|
}
|
|
|
|
// As width might be updated, update windowWidth/Height here.
|
|
u.windowWidth = width
|
|
u.windowHeight = height
|
|
|
|
if u.graphics.IsGL() {
|
|
// SwapInterval is affected by the current monitor of the window.
|
|
// This needs to be called at least after SetMonitor.
|
|
// Without SwapInterval after SetMonitor, vsynch doesn't work (#375).
|
|
//
|
|
// TODO: (#405) If triple buffering is needed, SwapInterval(0) should be called,
|
|
// but is this correct? If glfw.SwapInterval(0) and the driver doesn't support triple
|
|
// buffering, what will happen?
|
|
if u.vsync {
|
|
glfw.SwapInterval(1)
|
|
} else {
|
|
glfw.SwapInterval(0)
|
|
}
|
|
}
|
|
u.graphics.SetVsyncEnabled(vsync)
|
|
|
|
u.toChangeSize = true
|
|
return nil
|
|
})
|
|
|
|
if windowRecreated {
|
|
u.graphics.SetWindow(u.nativeWindow())
|
|
}
|
|
}
|
|
|
|
// currentMonitor returns the monitor most suitable with the current window.
|
|
//
|
|
// currentMonitor must be called on the main thread.
|
|
func (u *UserInterface) currentMonitor() *glfw.Monitor {
|
|
w := u.window
|
|
if m := w.GetMonitor(); m != nil {
|
|
return m
|
|
}
|
|
// Get the monitor which the current window belongs to. This requires OS API.
|
|
return u.currentMonitorFromPosition()
|
|
}
|
|
|
|
func (u *UserInterface) SetWindowPosition(x, y int) {
|
|
if !u.isRunning() {
|
|
u.setInitWindowPosition(x, y)
|
|
return
|
|
}
|
|
_ = u.t.Call(func() error {
|
|
if u.isFullscreen() {
|
|
return nil
|
|
}
|
|
u.window.SetPos(x, y)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (u *UserInterface) WindowPosition() (int, int) {
|
|
if !u.isRunning() {
|
|
panic("glfw: WindowPosition can't be called before the main loop starts")
|
|
}
|
|
x, y := 0, 0
|
|
_ = u.t.Call(func() error {
|
|
if u.isFullscreen() {
|
|
x, y = u.origPosX, u.origPosY
|
|
return nil
|
|
}
|
|
x, y = u.window.GetPos()
|
|
return nil
|
|
})
|
|
return x, y
|
|
}
|
|
|
|
func (u *UserInterface) SetScreenTransparent(transparent bool) {
|
|
if !u.isRunning() {
|
|
u.setInitScreenTransparent(transparent)
|
|
return
|
|
}
|
|
panic("glfw: SetScreenTransparent can't be called after the main loop starts")
|
|
}
|
|
|
|
func (u *UserInterface) IsScreenTransparent() bool {
|
|
if !u.isRunning() {
|
|
return u.isInitScreenTransparent()
|
|
}
|
|
val := false
|
|
_ = u.t.Call(func() error {
|
|
val = u.window.GetAttrib(glfw.TransparentFramebuffer) == glfw.True
|
|
return nil
|
|
})
|
|
return val
|
|
}
|
|
|
|
func (u *UserInterface) SetWindowSize(width, height int) {
|
|
if !u.isRunning() {
|
|
panic("glfw: SetWindowSize can't be called before the main loop starts")
|
|
}
|
|
w := int(u.toDeviceDependentPixel(float64(width)))
|
|
h := int(u.toDeviceDependentPixel(float64(height)))
|
|
u.setWindowSize(w, h, u.isFullscreen(), u.vsync)
|
|
}
|
|
|
|
func (u *UserInterface) CanHaveWindow() bool {
|
|
return true
|
|
}
|
|
|
|
func (u *UserInterface) Input() driver.Input {
|
|
return &u.input
|
|
}
|