ebiten/internal/uidriver/glfw/ui.go
2019-12-14 12:30:03 +09:00

1168 lines
28 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"
"math"
"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
screenWidthInDP int
screenHeightInDP int
scale float64
fullscreenScale float64
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
}
func (u *UserInterface) SetScreenSize(width, height int) {
if !u.isRunning() {
panic("glfw: SetScreenSize can't be called before the main loop starts")
}
u.setScreenSize(width, height, u.scale, u.isFullscreen(), u.vsync)
}
func (u *UserInterface) SetScreenScale(scale float64) {
if !u.isRunning() {
panic("glfw: SetScreenScale can't be called before the main loop starts")
}
u.setScreenSize(u.screenWidthInDP, u.screenHeightInDP, scale, u.isFullscreen(), u.vsync)
}
func (u *UserInterface) ScreenScale() float64 {
if !u.isRunning() {
return 0
}
s := 0.0
_ = u.t.Call(func() error {
s = u.scale
return nil
})
return s
}
// 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
}
u.setScreenSize(u.screenWidthInDP, u.screenHeightInDP, u.scale, 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 setScreenSize so far, but
// it should be OK since any goroutines can't reach here when
// the game already starts and setScreenSize can be called.
u.m.Lock()
u.vsync = enabled
u.m.Unlock()
return
}
u.setScreenSize(u.screenWidthInDP, u.screenHeightInDP, u.scale, 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) ScreenPadding() (x0, y0, x1, y1 float64) {
if !u.isRunning() {
return 0, 0, 0, 0
}
if !u.IsFullscreen() {
w, _ := u.window.GetSize()
wf := u.toDeviceIndependentPixel(float64(w)) / u.getScale()
if u.screenWidthInDP == int(wf) {
return 0, 0, 0, 0
}
// The window width can be bigger than the game screen width (#444).
ox := 0.0
_ = u.t.Call(func() error {
ox = (wf*u.actualScreenScale() - float64(u.screenWidthInDP)*u.actualScreenScale()) / 2
return nil
})
return ox, 0, ox, 0
}
d := 0.0
sx := 0.0
sy := 0.0
mx := 0.0
my := 0.0
_ = u.t.Call(func() error {
sx = float64(u.screenWidthInDP) * u.actualScreenScale()
sy = float64(u.screenHeightInDP) * u.actualScreenScale()
v := u.currentMonitor().GetVideoMode()
d = u.deviceScaleFactor()
mx = u.toDeviceIndependentPixel(float64(v.Width)) * d
my = u.toDeviceIndependentPixel(float64(v.Height)) * d
return nil
})
ox := (mx - sx) / 2
oy := (my - sy) / 2
return ox, oy, (mx - sx) - ox, (my - sy) - oy
}
func (u *UserInterface) adjustPosition(x, y int) (int, int) {
if !u.isRunning() {
return x, y
}
ox, oy, _, _ := u.ScreenPadding()
s := 0.0
_ = u.t.Call(func() error {
s = u.actualScreenScale()
return nil
})
return x - int(ox/s), y - int(oy/s)
}
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
}
w := int(u.toDeviceIndependentPixel(float64(width)) / u.scale)
h := int(u.toDeviceIndependentPixel(float64(height)) / u.scale)
u.reqWidth = w
u.reqHeight = h
})
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
)
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()
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.setScreenSize(width, height, scale, 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)
}
// getScale must be called from the main thread.
func (u *UserInterface) getScale() float64 {
if !u.isFullscreen() {
return u.scale
}
if u.fullscreenScale == 0 {
v := u.currentMonitor().GetVideoMode()
sw := u.toDeviceIndependentPixel(float64(v.Width)) / float64(u.screenWidthInDP)
sh := u.toDeviceIndependentPixel(float64(v.Height)) / float64(u.screenHeightInDP)
s := sw
if s > sh {
s = sh
}
u.fullscreenScale = s
}
return u.fullscreenScale
}
// actualScreenScale must be called from the main thread.
func (u *UserInterface) actualScreenScale() float64 {
return u.getScale() * u.deviceScaleFactor()
}
func (u *UserInterface) updateSize(context driver.UIContext) {
u.setScreenSize(u.screenWidthInDP, u.screenHeightInDP, u.scale, 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 {
actualScale := 0.0
_ = u.t.Call(func() error {
actualScale = u.actualScreenScale()
return nil
})
context.SetSize(u.screenWidthInDP, u.screenHeightInDP, actualScale)
}
}
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() {
u.setScreenSize(u.screenWidthInDP, u.screenHeightInDP, u.scale, 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)
_ = 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.setScreenSize(w, h, u.scale, 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) setScreenSize(width, height int, scale float64, fullscreen bool, vsync bool) {
windowRecreated := false
_ = u.t.Call(func() error {
if u.screenWidthInDP == width && u.screenHeightInDP == height && u.scale == scale && u.isFullscreen() == fullscreen && u.vsync == vsync && u.lastDeviceScaleFactor == u.deviceScaleFactor() {
return nil
}
if width < 1 {
width = 1
}
if height < 1 {
height = 1
}
u.screenWidthInDP = width
u.screenHeightInDP = height
u.scale = scale
u.fullscreenScale = 0
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.
// 252 is an arbitrary number and I guess this is small enough.
minWindowWidth := 252
if u.window.GetAttrib(glfw.Decorated) == glfw.False {
minWindowWidth = 1
}
windowWidthInDP := width
s := scale * u.deviceScaleFactor()
if int(float64(width)*s) < minWindowWidth {
windowWidthInDP = int(math.Ceil(float64(minWindowWidth) / s))
}
oldW, oldH := u.window.GetSize()
newW := int(u.toDeviceDependentPixel(float64(windowWidthInDP) * u.getScale()))
newH := int(u.toDeviceDependentPixel(float64(u.screenHeightInDP) * u.getScale()))
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:
}
}
}
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
}
// Window title might be lost on macOS after coming back from fullscreen.
u.window.SetTitle(u.title)
}
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) Input() driver.Input {
return &u.input
}