From 2c2c4bc428f340c5de68634cad96d4672b1164d9 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Wed, 29 Dec 2021 22:08:59 +0900 Subject: [PATCH] ebiten: add WindowResizingModeType and its constants and functions This allows a new state to disallow resizing the window but allow making the window fullscreen on macOS by a user. This change adds the new type WindowResizingModeType. There are these constants of this type: * WindowResizingModeDisabled * WindowResizingModeOnlyFullscreenEnabled * WindowResizingModeEnabled Closes #1819 --- examples/windowsize/main.go | 29 ++++++--- internal/ui/ui.go | 8 +++ internal/ui/ui_glfw.go | 38 ++++++------ internal/ui/ui_glfw_darwin.go | 108 ++++++++++++++++++++++++++++++++- internal/ui/ui_glfw_unix.go | 3 + internal/ui/ui_glfw_windows.go | 3 + internal/ui/window_glfw.go | 25 +++++--- internal/ui/window_null.go | 6 +- window.go | 59 +++++++++++++++--- 9 files changed, 225 insertions(+), 54 deletions(-) diff --git a/examples/windowsize/main.go b/examples/windowsize/main.go index 981ea79c2..214d849cb 100644 --- a/examples/windowsize/main.go +++ b/examples/windowsize/main.go @@ -131,7 +131,7 @@ func (g *game) Update() error { positionX, positionY := ebiten.WindowPosition() g.transparent = ebiten.IsScreenTransparent() floating := ebiten.IsWindowFloating() - resizable := ebiten.IsWindowResizable() + resizingMode := ebiten.WindowResizingMode() screenCleared := ebiten.IsScreenClearedEveryFrame() const d = 16 @@ -238,7 +238,16 @@ func (g *game) Update() error { floating = !floating } if inpututil.IsKeyJustPressed(ebiten.KeyR) { - resizable = !resizable + switch resizingMode { + case ebiten.WindowResizingModeDisabled: + resizingMode = ebiten.WindowResizingModeOnlyFullscreenEnabled + case ebiten.WindowResizingModeOnlyFullscreenEnabled: + resizingMode = ebiten.WindowResizingModeEnabled + case ebiten.WindowResizingModeEnabled: + resizingMode = ebiten.WindowResizingModeDisabled + default: + panic("not reached") + } } if inpututil.IsKeyJustPressed(ebiten.KeyW) { screenCleared = !screenCleared @@ -275,7 +284,7 @@ func (g *game) Update() error { } ebiten.SetWindowFloating(floating) ebiten.SetScreenClearedEveryFrame(screenCleared) - if maximize && ebiten.IsWindowResizable() { + if maximize && ebiten.WindowResizingMode() == ebiten.WindowResizingModeEnabled { ebiten.MaximizeWindow() } if minimize { @@ -284,7 +293,7 @@ func (g *game) Update() error { if restore { ebiten.RestoreWindow() } - ebiten.SetWindowResizable(resizable) + ebiten.SetWindowResizingMode(resizingMode) if inpututil.IsKeyJustPressed(ebiten.KeyI) { ebiten.SetWindowIcon([]image.Image{createRandomIconImage()}) @@ -314,7 +323,7 @@ func (g *game) Draw(screen *ebiten.Image) { } var lines []string - if !ebiten.IsWindowMaximized() && ebiten.IsWindowResizable() { + if !ebiten.IsWindowMaximized() && ebiten.WindowResizingMode() == ebiten.WindowResizingModeEnabled { lines = append(lines, "[M] Maximize the window (only for desktops)") } if !ebiten.IsWindowMinimized() { @@ -325,7 +334,7 @@ func (g *game) Draw(screen *ebiten.Image) { } msgM := strings.Join(lines, "\n") - msgR := "[R] Switch the window resizable state (only for desktops)\n" + msgR := "[R] Switch the window resizing mode (only for desktops)\n" fg := "Yes" if !ebiten.IsFocused() { fg = "No" @@ -405,20 +414,20 @@ func main() { ebiten.SetFullscreen(true) } if *flagResizable { - ebiten.SetWindowResizable(true) + ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) } if *flagFloating { ebiten.SetWindowFloating(true) } if *flagMaximize { - ebiten.SetWindowResizable(true) + ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) ebiten.MaximizeWindow() } if !*flagVsync { ebiten.SetFPSMode(ebiten.FPSModeVsyncOffMaximum) } if *flagAutoAdjusting { - ebiten.SetWindowResizable(true) + ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) } ebiten.SetInitFocused(*flagInitFocused) @@ -438,7 +447,7 @@ func main() { } if minw >= 0 || minh >= 0 || maxw >= 0 || maxh >= 0 { ebiten.SetWindowSizeLimits(minw, minh, maxw, maxh) - ebiten.SetWindowResizable(true) + ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) } const title = "Window Size (Ebiten Demo)" diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 8ba21ca0a..a895a819a 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -68,3 +68,11 @@ const ( CursorShapeEWResize CursorShapeNSResize ) + +type WindowResizingMode int + +const ( + WindowResizingModeDisabled WindowResizingMode = iota + WindowResizingModeOnlyFullscreenEnabled + WindowResizingModeEnabled +) diff --git a/internal/ui/ui_glfw.go b/internal/ui/ui_glfw.go index aa8b01408..6527f9289 100644 --- a/internal/ui/ui_glfw.go +++ b/internal/ui/ui_glfw.go @@ -70,6 +70,7 @@ type UserInterface struct { cursorShape CursorShape windowClosingHandled bool windowBeingClosed bool + windowResizingMode WindowResizingMode // setSizeCallbackEnabled must be accessed from the main thread. setSizeCallbackEnabled bool @@ -88,7 +89,6 @@ type UserInterface struct { initFullscreen bool initCursorMode CursorMode initWindowDecorated bool - initWindowResizable bool initWindowPositionXInDIP int initWindowPositionYInDIP int initWindowWidthInDIP int @@ -346,19 +346,6 @@ func (u *UserInterface) setRunnableOnUnfocused(runnableOnUnfocused bool) { 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 @@ -873,7 +860,11 @@ func (u *UserInterface) init() error { // Before creating a window, set it unresizable no matter what u.isInitWindowResizable() is (#1987). // Making the window resizable here doesn't work correctly when switching to enable resizing. - glfw.WindowHint(glfw.Resizable, glfw.False) + resizable := glfw.False + if u.windowResizingMode == WindowResizingModeEnabled { + resizable = glfw.True + } + glfw.WindowHint(glfw.Resizable, resizable) floating := glfw.False if u.isInitWindowFloating() { @@ -908,13 +899,13 @@ func (u *UserInterface) init() error { u.setWindowPositionInDIP(wx, wy, u.initMonitor) u.setWindowSizeInDIP(ww, wh, u.isFullscreen()) - u.setWindowResizable(u.isInitWindowResizable()) - // Maximizing a window requires a proper size and position. Call Maximize here (#1117). if u.isInitWindowMaximized() { u.window.Maximize() } + u.setWindowResizingModeForOS(u.windowResizingMode) + u.window.Show() if g, ok := Graphics().(interface{ SetWindow(uintptr) }); ok { @@ -1520,8 +1511,12 @@ func (u *UserInterface) setWindowFloating(floating bool) { u.window.SetAttrib(glfw.Floating, v) } -// setWindowResizable must be called from the main thread. -func (u *UserInterface) setWindowResizable(resizable bool) { +// setWindowResizingMode must be called from the main thread. +func (u *UserInterface) setWindowResizingMode(mode WindowResizingMode) { + if u.windowResizingMode == mode { + return + } + if u.setSizeCallbackEnabled { u.setSizeCallbackEnabled = false defer func() { @@ -1529,11 +1524,14 @@ func (u *UserInterface) setWindowResizable(resizable bool) { }() } + u.windowResizingMode = mode + v := glfw.False - if resizable { + if mode == WindowResizingModeEnabled { v = glfw.True } u.window.SetAttrib(glfw.Resizable, v) + u.setWindowResizingModeForOS(mode) } // setWindowPositionInDIP sets the window position. diff --git a/internal/ui/ui_glfw_darwin.go b/internal/ui/ui_glfw_darwin.go index c950c8ef5..562baf84a 100644 --- a/internal/ui/ui_glfw_darwin.go +++ b/internal/ui/ui_glfw_darwin.go @@ -22,6 +22,91 @@ package ui // // #import // +// @interface EbitenWindowDelegate : NSObject +// @end +// +// @implementation EbitenWindowDelegate { +// id origDelegate_; +// bool origResizable_; +// } +// +// - (instancetype)initWithOrigDelegate:(id)origDelegate { +// self = [super init]; +// if (self != nil) { +// origDelegate_ = origDelegate; +// } +// return self; +// } +// +// // The method set of origDelegate_ must sync with GLFWWindowDelegate's implementation. +// // See cocoa_window.m in GLFW. +// - (BOOL)windowShouldClose:(id)sender { +// return [origDelegate_ windowShouldClose:sender]; +// } +// - (void)windowDidResize:(NSNotification *)notification { +// [origDelegate_ windowDidResize:notification]; +// } +// - (void)windowDidMove:(NSNotification *)notification { +// [origDelegate_ windowDidMove:notification]; +// } +// - (void)windowDidMiniaturize:(NSNotification *)notification { +// [origDelegate_ windowDidMiniaturize:notification]; +// } +// - (void)windowDidDeminiaturize:(NSNotification *)notification { +// [origDelegate_ windowDidDeminiaturize:notification]; +// } +// - (void)windowDidBecomeKey:(NSNotification *)notification { +// [origDelegate_ windowDidBecomeKey:notification]; +// } +// - (void)windowDidResignKey:(NSNotification *)notification { +// [origDelegate_ windowDidResignKey:notification]; +// } +// - (void)windowDidChangeOcclusionState:(NSNotification* )notification { +// [origDelegate_ windowDidChangeOcclusionState:notification]; +// } +// +// - (void)pushResizableState:(NSWindow*)window { +// origResizable_ = window.styleMask & NSWindowStyleMaskResizable; +// if (!origResizable_) { +// window.styleMask |= NSWindowStyleMaskResizable; +// } +// } +// +// - (void)popResizableState:(NSWindow*)window { +// if (!origResizable_) { +// window.styleMask &= ~NSWindowStyleMaskResizable; +// } +// origResizable_ = false; +// } +// +// - (void)windowWillEnterFullScreen:(NSNotification *)notification { +// NSWindow* window = (NSWindow*)[notification object]; +// [self pushResizableState:window]; +// } +// +// - (void)windowDidEnterFullScreen:(NSNotification *)notification { +// NSWindow* window = (NSWindow*)[notification object]; +// [self popResizableState:window]; +// } +// +// - (void)windowWillExitFullScreen:(NSNotification *)notification { +// NSWindow* window = (NSWindow*)[notification object]; +// [self pushResizableState:window]; +// } +// +// - (void)windowDidExitFullScreen:(NSNotification *)notification { +// NSWindow* window = (NSWindow*)[notification object]; +// [self popResizableState:window]; +// } +// +// @end +// +// static void initializeWindow(uintptr_t windowPtr) { +// NSWindow* window = (NSWindow*)windowPtr; +// // This delegate is never released. This assumes that the window lives until the process lives. +// window.delegate = [[EbitenWindowDelegate alloc] initWithOrigDelegate:window.delegate]; +// } +// // static void currentMonitorPos(uintptr_t windowPtr, int* x, int* y) { // @autoreleasepool { // NSScreen* screen = [NSScreen mainScreen]; @@ -55,6 +140,9 @@ package ui // if (((window.styleMask & NSWindowStyleMaskFullScreen) != 0) == fullscreen) { // return; // } +// +// // Even though EbitenWindowDelegate is used, this hack is still required. +// // toggleFullscreen doesn't work when the window is not resizable. // bool origResizable = window.styleMask & NSWindowStyleMaskResizable; // if (!origResizable) { // window.styleMask |= NSWindowStyleMaskResizable; @@ -126,6 +214,15 @@ package ui // *x = (int)(location.x); // *y = (int)(location.y); // } +// +// static void setAllowFullscreen(uintptr_t windowPtr, bool allowFullscreen) { +// NSWindow* window = (NSWindow*)windowPtr; +// if (allowFullscreen) { +// window.collectionBehavior |= NSWindowCollectionBehaviorFullScreenPrimary; +// } else { +// window.collectionBehavior &= ~NSWindowCollectionBehaviorFullScreenPrimary; +// } +// } import "C" import ( @@ -225,5 +322,14 @@ func (u *UserInterface) adjustViewSize() { C.adjustViewSize(C.uintptr_t(u.window.GetCocoaWindow())) } -func initializeWindowAfterCreation(w *glfw.Window) { +func (u *UserInterface) setWindowResizingModeForOS(mode WindowResizingMode) { + allowFullscreen := mode == WindowResizingModeOnlyFullscreenEnabled || + mode == WindowResizingModeEnabled + C.setAllowFullscreen(C.uintptr_t(u.window.GetCocoaWindow()), C.bool(allowFullscreen)) +} + +func initializeWindowAfterCreation(w *glfw.Window) { + // TODO: Register NSWindowWillEnterFullScreenNotification and so on. + // Enable resizing temporary before making the window fullscreen. + C.initializeWindow(C.uintptr_t(w.GetCocoaWindow())) } diff --git a/internal/ui/ui_glfw_unix.go b/internal/ui/ui_glfw_unix.go index 5d59adc72..0eee60fe4 100644 --- a/internal/ui/ui_glfw_unix.go +++ b/internal/ui/ui_glfw_unix.go @@ -189,6 +189,9 @@ func (u *UserInterface) setNativeFullscreen(fullscreen bool) { func (u *UserInterface) adjustViewSize() { } +func (u *UserInterface) setWindowResizingModeForOS(mode WindowResizingMode) { +} + func initializeWindowAfterCreation(w *glfw.Window) { // Show the window once before getting the position of the window. // On Linux/Unix, the window position is not reliable before showing. diff --git a/internal/ui/ui_glfw_windows.go b/internal/ui/ui_glfw_windows.go index d4b118415..40a5d910a 100644 --- a/internal/ui/ui_glfw_windows.go +++ b/internal/ui/ui_glfw_windows.go @@ -203,5 +203,8 @@ func (u *UserInterface) setNativeFullscreen(fullscreen bool) { func (u *UserInterface) adjustViewSize() { } +func (u *UserInterface) setWindowResizingModeForOS(mode WindowResizingMode) { +} + func initializeWindowAfterCreation(w *glfw.Window) { } diff --git a/internal/ui/window_glfw.go b/internal/ui/window_glfw.go index 60cb2b7f6..cfbc95f73 100644 --- a/internal/ui/window_glfw.go +++ b/internal/ui/window_glfw.go @@ -53,27 +53,32 @@ func (w *Window) SetDecorated(decorated bool) { }) } -func (w *Window) IsResizable() bool { +func (w *Window) ResizingMode() WindowResizingMode { if !w.ui.isRunning() { - return w.ui.isInitWindowResizable() + w.ui.m.Lock() + mode := w.ui.windowResizingMode + w.ui.m.Unlock() + return mode } - v := false + var mode WindowResizingMode w.ui.t.Call(func() { - v = w.ui.window.GetAttrib(glfw.Resizable) == glfw.True + mode = w.ui.windowResizingMode }) - return v + return mode } -func (w *Window) SetResizable(resizable bool) { +func (w *Window) SetResizingMode(mode WindowResizingMode) { if !w.ui.isRunning() { - w.ui.setInitWindowResizable(resizable) + w.ui.m.Lock() + w.ui.windowResizingMode = mode + w.ui.m.Unlock() return } w.ui.t.Call(func() { if w.ui.isNativeFullscreen() { return } - w.ui.setWindowResizable(resizable) + w.ui.setWindowResizingMode(mode) }) } @@ -105,7 +110,7 @@ func (w *Window) IsMaximized() bool { if !w.ui.isRunning() { return w.ui.isInitWindowMaximized() } - if !w.IsResizable() { + if w.ResizingMode() != WindowResizingModeEnabled { return false } var v bool @@ -119,7 +124,7 @@ func (w *Window) Maximize() { // Do not allow maximizing the window when the window is not resizable. // On Windows, it is possible to restore the window from being maximized by mouse-dragging, // and this can be an unexpected behavior. - if !w.IsResizable() { + if w.ResizingMode() != WindowResizingModeEnabled { panic("ui: a window to maximize must be resizable") } if !w.ui.isRunning() { diff --git a/internal/ui/window_null.go b/internal/ui/window_null.go index 861f67c56..ac428f089 100644 --- a/internal/ui/window_null.go +++ b/internal/ui/window_null.go @@ -30,11 +30,11 @@ func (*Window) IsDecorated() bool { func (*Window) SetDecorated(decorated bool) { } -func (*Window) IsResizable() bool { - return false +func (*Window) ResizingMode() WindowResizingMode { + return WindowResizingModeDisabled } -func (*Window) SetResizable(resizable bool) { +func (*Window) SetResizingMode(mode WindowResizingMode) { } func (*Window) Position() (int, int) { diff --git a/window.go b/window.go index 5fd40aad8..cd6f19124 100644 --- a/window.go +++ b/window.go @@ -21,6 +21,27 @@ import ( "github.com/hajimehoshi/ebiten/v2/internal/ui" ) +// WindowResizingModeType represents a mode in which a user resizes the window. +// +// Regardless of the resizing mode, an Ebiten application can still change the window size or make +// the window fullscreen by calling Ebiten functions. +type WindowResizingModeType = ui.WindowResizingMode + +// WindowResizingModeTypes +const ( + // WindowResizingModeDisabled indicates the mode to disallow resizing the window by a user. + WindowResizingModeDisabled WindowResizingModeType = WindowResizingModeType(ui.WindowResizingModeDisabled) + + // WindowResizingModeDisabled indicates the mode to disallow resizing the window, + // but allow to make the window fullscreen by a user. + // This works only on macOS so far. + // On the other platforms, this is the same as WindowResizingModeDisabled. + WindowResizingModeOnlyFullscreenEnabled WindowResizingModeType = WindowResizingModeType(ui.WindowResizingModeOnlyFullscreenEnabled) + + // WindowResizingModeEnabled indicates the mode to allow resizing the window by a user. + WindowResizingModeEnabled WindowResizingModeType = WindowResizingModeType(ui.WindowResizingModeEnabled) +) + // IsWindowDecorated reports whether the window is decorated. // // IsWindowDecorated is concurrent-safe. @@ -42,24 +63,42 @@ func SetWindowDecorated(decorated bool) { ui.Get().Window().SetDecorated(decorated) } +// WindowResizingMode returns the current mode in which a user resizes the window. +// +// The default mode is WindowResizingModeDisabled. +// +// WindowResizingMode is concurrent-safe. +func WindowResizingMode() WindowResizingModeType { + return WindowResizingModeType(ui.Get().Window().ResizingMode()) +} + +// SetWindowResizingMode sets the mode in which a user resizes the window. +// +// SetWindowResizingMode does nothing on macOS when the window is fullscreened. +// +// SetWindowResizingMode is concurrent-safe. +func SetWindowResizingMode(mode WindowResizingModeType) { + ui.Get().Window().SetResizingMode(ui.WindowResizingMode(mode)) +} + // IsWindowResizable reports whether the window is resizable by the user's dragging on desktops. // On the other environments, IsWindowResizable always returns false. // -// IsWindowResizable is concurrent-safe. +// Deprecated: as of v2.2. Use WindowResizingMode instead. func IsWindowResizable() bool { - return ui.Get().Window().IsResizable() + return ui.Get().Window().ResizingMode() == ui.WindowResizingModeEnabled } // SetWindowResizable sets whether the window is resizable by the user's dragging on desktops. // On the other environments, SetWindowResizable does nothing. // -// The window is not resizable by default. -// -// SetWindowResizable does nothing on macOS when the window is fullscreened. -// -// SetWindowResizable is concurrent-safe. +// Deprecated: as of v2.2, Use SetWindowResizingMode instead. func SetWindowResizable(resizable bool) { - ui.Get().Window().SetResizable(resizable) + mode := ui.WindowResizingModeDisabled + if resizable { + mode = ui.WindowResizingModeEnabled + } + ui.Get().Window().SetResizingMode(mode) } // SetWindowTitle sets the title of the window. @@ -200,7 +239,7 @@ func SetWindowFloating(float bool) { // MaximizeWindow maximizes the window. // -// MaximizeWindow panics when the window is not resizable. +// MaximizeWindow panics when the window is not resizable (WindowResizingModeEnabled). // // MaximizeWindow does nothing on browsers or mobiles. // @@ -211,7 +250,7 @@ func MaximizeWindow() { // IsWindowMaximized reports whether the window is maximized or not. // -// IsWindowMaximized returns false when the window is not resizable. +// IsWindowMaximized returns false when the window is not resizable (WindowResizingModeEnabled). // // IsWindowMaximized always returns false on browsers and mobiles. //