From 428c079e52ba4a1010b4f395d303f575b34f1e8a Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sat, 18 Sep 2021 02:21:24 +0900 Subject: [PATCH] internal/uidriver/glfw: Make a 'native' fullscreen on macOS This is a breaking change. On macOS, `SetFullscreen` creates a new independent space and maximize the size of the window there. Updates #1506 Closes #1579 --- internal/glfw/glfw_notwindows.go | 4 ++ internal/uidriver/glfw/ui.go | 103 +++++++++++++++++---------- internal/uidriver/glfw/ui_darwin.go | 25 +++++++ internal/uidriver/glfw/ui_unix.go | 9 +++ internal/uidriver/glfw/ui_windows.go | 9 +++ internal/uidriver/glfw/window.go | 10 ++- 6 files changed, 121 insertions(+), 39 deletions(-) diff --git a/internal/glfw/glfw_notwindows.go b/internal/glfw/glfw_notwindows.go index e3a3d5774..29c0fe6c9 100644 --- a/internal/glfw/glfw_notwindows.go +++ b/internal/glfw/glfw_notwindows.go @@ -351,6 +351,10 @@ func WaitEvents() { glfw.WaitEvents() } +func WaitEventsTimeout(timeout float64) { + glfw.WaitEventsTimeout(timeout) +} + func WindowHint(target Hint, hint int) { glfw.WindowHint(glfw.Hint(target), hint) } diff --git a/internal/uidriver/glfw/ui.go b/internal/uidriver/glfw/ui.go index b4d6f622f..8b9a0ad92 100644 --- a/internal/uidriver/glfw/ui.go +++ b/internal/uidriver/glfw/ui.go @@ -545,7 +545,7 @@ 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 + return u.window.GetMonitor() != nil || u.isNativeFullscreen() } func (u *UserInterface) IsFullscreen() bool { @@ -576,10 +576,6 @@ func (u *UserInterface) SetFullscreen(fullscreen bool) { } _ = u.t.Call(func() error { - if u.isNativeFullscreen() { - return nil - } - w, h := u.windowWidth, u.windowHeight u.setWindowSize(w, h, fullscreen) return nil @@ -783,7 +779,7 @@ func (u *UserInterface) registerWindowSetSizeCallback() { if u.window.GetAttrib(glfw.Resizable) == glfw.False { return } - if u.isFullscreen() { + if u.isFullscreen() && !u.isNativeFullscreen() { return } @@ -849,8 +845,8 @@ func (u *UserInterface) init() error { glfw.WindowHint(glfw.ClientAPI, glfw.NoAPI) } - // Enable auto-iconifying on Windows and macOS until some fullscreen issues are solved (#1506). - if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { + // Enable auto-iconifying on Windows until some fullscreen issues are solved (#1506). + if runtime.GOOS == "windows" { glfw.WindowHint(glfw.AutoIconify, glfw.True) } else { glfw.WindowHint(glfw.AutoIconify, glfw.False) @@ -1205,7 +1201,7 @@ func (u *UserInterface) adjustWindowSizeBasedOnSizeLimitsInDP(width, height int) func (u *UserInterface) setWindowSize(width, height int, fullscreen bool) { width, height = u.adjustWindowSizeBasedOnSizeLimits(width, height) - u.Graphics().SetFullscreen(fullscreen || u.isNativeFullscreen()) + u.Graphics().SetFullscreen(fullscreen) if u.windowWidth == width && u.windowHeight == height && u.isFullscreen() == fullscreen && u.lastDeviceScaleFactor == u.deviceScaleFactor() { return @@ -1235,21 +1231,42 @@ func (u *UserInterface) setWindowSize(width, height int, fullscreen bool) { }() } + windowRecreated := u.setWindowSizeImpl(width, height, fullscreen) + + // As width might be updated, update windowWidth/Height here. + u.windowWidth = width + u.windowHeight = height + + u.toChangeSize = true + + if windowRecreated { + if g, ok := u.Graphics().(interface{ SetWindow(uintptr) }); ok { + g.SetWindow(u.nativeWindow()) + } + } +} + +func (u *UserInterface) setWindowSizeImpl(width, height int, fullscreen bool) bool { var windowRecreated bool if fullscreen { - if u.origPosX == invalidPos || u.origPosY == invalidPos { - u.origPosX, u.origPosY = u.window.GetPos() + if x, y := u.origPos(); x == invalidPos || y == invalidPos { + u.setOrigPos(u.window.GetPos()) } - m := currentMonitor(u.window) - 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() + if u.isNativeFullscreenAvailable() { + u.setNativeFullscreen(fullscreen) + } else { + m := currentMonitor(u.window) + 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 { // On Windows, giving a too small width doesn't call a callback (#165). @@ -1263,7 +1280,9 @@ func (u *UserInterface) setWindowSize(width, height int, fullscreen bool) { width = minWindowWidth } - if u.window.GetMonitor() != nil { + if u.isNativeFullscreenAvailable() && u.isNativeFullscreen() { + u.setNativeFullscreen(false) + } else if !u.isNativeFullscreenAvailable() && 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. @@ -1289,9 +1308,7 @@ func (u *UserInterface) setWindowSize(width, height int, fullscreen bool) { } } - if u.origPosX != invalidPos && u.origPosY != invalidPos { - x := u.origPosX - y := u.origPosY + if x, y := u.origPos(); x != invalidPos && y != invalidPos { 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. @@ -1299,8 +1316,7 @@ func (u *UserInterface) setWindowSize(width, height int, fullscreen bool) { u.window.SetPos(x+1, y) u.window.SetPos(x, y) } - u.origPosX = invalidPos - u.origPosY = invalidPos + u.setOrigPos(invalidPos, invalidPos) } // Set the window size after the position. The order matters. @@ -1350,17 +1366,7 @@ func (u *UserInterface) setWindowSize(width, height int, fullscreen bool) { u.window.SetTitle(u.title) } - // As width might be updated, update windowWidth/Height here. - u.windowWidth = width - u.windowHeight = height - - u.toChangeSize = true - - if windowRecreated { - if g, ok := u.Graphics().(interface{ SetWindow(uintptr) }); ok { - g.SetWindow(u.nativeWindow()) - } - } + return windowRecreated } // updateVsync must be called on the main thread. @@ -1625,7 +1631,7 @@ func (u *UserInterface) setWindowPosition(x, y int) { xf := u.toGLFWPixel(float64(x)) yf := u.toGLFWPixel(float64(y)) if x, y := u.adjustWindowPosition(mx+int(xf), my+int(yf)); u.isFullscreen() { - u.origPosX, u.origPosY = x, y + u.setOrigPos(x, y) } else { u.window.SetPos(x, y) } @@ -1654,3 +1660,26 @@ func (u *UserInterface) setWindowTitle(title string) { u.window.SetTitle(title) } + +func (u *UserInterface) origPos() (int, int) { + // On macOS, the window can be fullscreened without calling an Ebiten function. + // Then, an original position might not be available by u.window.GetPos(). + // Do not rely on the window position. + if u.isNativeFullscreenAvailable() { + return invalidPos, invalidPos + } + return u.origPosX, u.origPosY +} + +func (u *UserInterface) setOrigPos(x, y int) { + // TODO: The original position should be updated at a 'PosCallback'. + + // On macOS, the window can be fullscreened without calling an Ebiten function. + // Then, an original position might not be available by u.window.GetPos(). + // Do not rely on the window position. + if u.isNativeFullscreenAvailable() { + return + } + u.origPosX = x + u.origPosY = y +} diff --git a/internal/uidriver/glfw/ui_darwin.go b/internal/uidriver/glfw/ui_darwin.go index e5f7fd1b4..509dda517 100644 --- a/internal/uidriver/glfw/ui_darwin.go +++ b/internal/uidriver/glfw/ui_darwin.go @@ -50,6 +50,21 @@ package glfw // return (window.styleMask & NSWindowStyleMaskFullScreen) != 0; // } // +// static void setNativeFullscreen(uintptr_t windowPtr, bool fullscreen) { +// NSWindow* window = (NSWindow*)windowPtr; +// if (((window.styleMask & NSWindowStyleMaskFullScreen) != 0) == fullscreen) { +// return; +// } +// bool origResizable = window.styleMask & NSWindowStyleMaskResizable; +// if (!origResizable) { +// window.styleMask |= NSWindowStyleMaskResizable; +// } +// [window toggleFullScreen:nil]; +// if (!origResizable) { +// window.styleMask &= ~NSWindowStyleMaskResizable; +// } +// } +// // static void setNativeCursor(int cursorID) { // id cursor = [[NSCursor class] performSelector:@selector(arrowCursor)]; // switch (cursorID) { @@ -139,3 +154,13 @@ func (u *UserInterface) isNativeFullscreen() bool { func (u *UserInterface) setNativeCursor(shape driver.CursorShape) { C.setNativeCursor(C.int(shape)) } + +func (u *UserInterface) isNativeFullscreenAvailable() bool { + return true +} + +func (u *UserInterface) setNativeFullscreen(fullscreen bool) { + // Toggling fullscreen might ignore events like keyUp. Ensure that events are fired. + glfw.WaitEventsTimeout(1) + C.setNativeFullscreen(C.uintptr_t(u.window.GetCocoaWindow()), C.bool(fullscreen)) +} diff --git a/internal/uidriver/glfw/ui_unix.go b/internal/uidriver/glfw/ui_unix.go index e9b816758..69d473dce 100644 --- a/internal/uidriver/glfw/ui_unix.go +++ b/internal/uidriver/glfw/ui_unix.go @@ -20,6 +20,7 @@ package glfw import ( "math" + "runtime" "github.com/hajimehoshi/ebiten/v2/internal/driver" "github.com/hajimehoshi/ebiten/v2/internal/glfw" @@ -146,3 +147,11 @@ func (u *UserInterface) setNativeCursor(shape driver.CursorShape) { // TODO: Use native API in the future (#1571) u.window.SetCursor(glfwSystemCursors[shape]) } + +func (u *UserInterface) isNativeFullscreenAvailable() bool { + return false +} + +func (u *UserInterface) setNativeFullscreen(fullscreen bool) { + panic(fmt.Sprintf("glfw: setNativeFullscreen is not implemented in this environment: %s", runtime.GOOS)) +} diff --git a/internal/uidriver/glfw/ui_windows.go b/internal/uidriver/glfw/ui_windows.go index 68d2685dd..3ac45b7e3 100644 --- a/internal/uidriver/glfw/ui_windows.go +++ b/internal/uidriver/glfw/ui_windows.go @@ -16,6 +16,7 @@ package glfw import ( "fmt" + "runtime" "unsafe" "golang.org/x/sys/windows" @@ -194,3 +195,11 @@ func (u *UserInterface) setNativeCursor(shape driver.CursorShape) { // TODO: Use native API in the future (#1571) u.window.SetCursor(glfwSystemCursors[shape]) } + +func (u *UserInterface) isNativeFullscreenAvailable() bool { + return false +} + +func (u *UserInterface) setNativeFullscreen(fullscreen bool) { + panic(fmt.Sprintf("glfw: setNativeFullscreen is not implemented in this environment: %s", runtime.GOOS)) +} diff --git a/internal/uidriver/glfw/window.go b/internal/uidriver/glfw/window.go index 8820567ae..3b094afdd 100644 --- a/internal/uidriver/glfw/window.go +++ b/internal/uidriver/glfw/window.go @@ -176,8 +176,8 @@ func (w *window) Position() (int, int) { x, y := 0, 0 _ = w.ui.t.Call(func() error { var wx, wy int - if w.ui.isFullscreen() { - wx, wy = w.ui.origPosX, w.ui.origPosY + if w.ui.isFullscreen() && !w.ui.isNativeFullscreenAvailable() { + wx, wy = w.ui.origPos() } else { wx, wy = w.ui.window.GetPos() } @@ -223,6 +223,12 @@ func (w *window) SetSize(width, height int) { return } _ = w.ui.t.Call(func() error { + // When a window is a native fullscreen, forcing to resize the window might leave unexpected image lags. + // Forbid this. + if w.ui.isNativeFullscreen() { + return nil + } + ww := int(w.ui.toGLFWPixel(float64(width))) wh := int(w.ui.toGLFWPixel(float64(height))) w.ui.setWindowSize(ww, wh, w.ui.isFullscreen())