diff --git a/examples/windowclosing/main.go b/examples/windowclosing/main.go new file mode 100644 index 000000000..8bc43733c --- /dev/null +++ b/examples/windowclosing/main.go @@ -0,0 +1,67 @@ +// Copyright 2021 The Ebiten Authors +// +// 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 example + +package main + +import ( + "errors" + "log" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "github.com/hajimehoshi/ebiten/v2/inpututil" +) + +var regularTermination = errors.New("regular termination") + +type Game struct { + windowClosingHandled bool +} + +func (g *Game) Update() error { + if ebiten.IsWindowBeingClosed() { + g.windowClosingHandled = true + } + if g.windowClosingHandled { + if inpututil.IsKeyJustPressed(ebiten.KeyY) { + return regularTermination + } + if inpututil.IsKeyJustPressed(ebiten.KeyN) { + g.windowClosingHandled = false + } + } + return nil +} + +func (g *Game) Draw(screen *ebiten.Image) { + if !g.windowClosingHandled { + ebitenutil.DebugPrint(screen, "Try to close this window.") + return + } + ebitenutil.DebugPrint(screen, "Do you really want to close this window? [y/n]") +} + +func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { + return outsideWidth, outsideHeight +} + +func main() { + ebiten.SetWindowClosingHandled(true) + ebiten.SetWindowTitle("Window Closing (Ebiten Demo)") + if err := ebiten.RunGame(&Game{}); err != nil && err != regularTermination { + log.Fatal(err) + } +} diff --git a/internal/driver/ui.go b/internal/driver/ui.go index 5756d2be0..3f6a205eb 100644 --- a/internal/driver/ui.go +++ b/internal/driver/ui.go @@ -93,4 +93,8 @@ type Window interface { SetIcon(iconImages []image.Image) SetTitle(title string) Restore() + + IsBeingClosed() bool + SetClosingHandled(handled bool) + IsClosingHandled() bool } diff --git a/internal/glfw/callback_notwindows.go b/internal/glfw/callback_notwindows.go index 7757e0ce7..b8d91dffd 100644 --- a/internal/glfw/callback_notwindows.go +++ b/internal/glfw/callback_notwindows.go @@ -23,6 +23,7 @@ import ( var ( charModsCallbacks = map[CharModsCallback]glfw.CharModsCallback{} + closeCallbacks = map[CloseCallback]glfw.CloseCallback{} framebufferSizeCallbacks = map[FramebufferSizeCallback]glfw.FramebufferSizeCallback{} scrollCallbacks = map[ScrollCallback]glfw.ScrollCallback{} sizeCallbacks = map[SizeCallback]glfw.SizeCallback{} @@ -40,6 +41,18 @@ func ToCharModsCallback(cb func(window *Window, char rune, mods ModifierKey)) Ch return id } +func ToCloseCallback(cb func(window *Window)) CloseCallback { + if cb == nil { + return 0 + } + id := CloseCallback(len(closeCallbacks) + 1) + var gcb glfw.CloseCallback = func(window *glfw.Window) { + cb(theWindows.get(window)) + } + closeCallbacks[id] = gcb + return id +} + func ToFramebufferSizeCallback(cb func(window *Window, width int, height int)) FramebufferSizeCallback { if cb == nil { return 0 diff --git a/internal/glfw/callback_windows.go b/internal/glfw/callback_windows.go index 30679ed44..396545a55 100644 --- a/internal/glfw/callback_windows.go +++ b/internal/glfw/callback_windows.go @@ -28,6 +28,16 @@ func ToCharModsCallback(cb func(window *Window, char rune, mods ModifierKey)) Ch })) } +func ToCloseCallback(cb func(window *Window)) CloseCallback { + if cb == nil { + return 0 + } + return CloseCallback(windows.NewCallbackCDecl(func(window uintptr) uintptr { + cb(theGLFWWindows.get(window)) + return 0 + })) +} + func ToFramebufferSizeCallback(cb func(window *Window, width int, height int)) FramebufferSizeCallback { if cb == nil { return 0 diff --git a/internal/glfw/glfw_notwindows.go b/internal/glfw/glfw_notwindows.go index 98cb18a8c..19d0d3a9a 100644 --- a/internal/glfw/glfw_notwindows.go +++ b/internal/glfw/glfw_notwindows.go @@ -174,6 +174,11 @@ func (w *Window) SetCursor(cursor *Cursor) { w.w.SetCursor(c) } +func (w *Window) SetCloseCallback(cbfun CloseCallback) (previous CloseCallback) { + w.w.SetCloseCallback(closeCallbacks[cbfun]) + return ToCloseCallback(nil) // TODO +} + func (w *Window) SetFramebufferSizeCallback(cbfun FramebufferSizeCallback) (previous FramebufferSizeCallback) { w.w.SetFramebufferSizeCallback(framebufferSizeCallbacks[cbfun]) return ToFramebufferSizeCallback(nil) // TODO @@ -184,6 +189,10 @@ func (w *Window) SetScrollCallback(cbfun ScrollCallback) (previous ScrollCallbac return ToScrollCallback(nil) // TODO } +func (w *Window) SetShouldClose(value bool) { + w.w.SetShouldClose(value) +} + func (w *Window) SetSizeCallback(cbfun SizeCallback) (previous SizeCallback) { w.w.SetSizeCallback(sizeCallbacks[cbfun]) prev := w.prevSizeCallback diff --git a/internal/glfw/glfw_windows.go b/internal/glfw/glfw_windows.go index 379e18b9b..b3d941f85 100644 --- a/internal/glfw/glfw_windows.go +++ b/internal/glfw/glfw_windows.go @@ -201,6 +201,12 @@ func (w *Window) SetCharModsCallback(cbfun CharModsCallback) (previous CharModsC return ToCharModsCallback(nil) // TODO } +func (w *Window) SetCloseCallback(cbfun CloseCallback) (previous CloseCallback) { + glfwDLL.call("glfwSetWindowCloseCallback", w.w, uintptr(cbfun)) + panicError() + return ToCloseCallback(nil) // TODO +} + func (w *Window) SetCursor(cursor *Cursor) { var c uintptr if cursor != nil { @@ -221,6 +227,15 @@ func (w *Window) SetScrollCallback(cbfun ScrollCallback) (previous ScrollCallbac return ToScrollCallback(nil) // TODO } +func (w *Window) SetShouldClose(value bool) { + var v uintptr = False + if value { + v = True + } + glfwDLL.call("glfwSetWindowShouldClose", w.w, v) + panicError() +} + func (w *Window) SetSizeCallback(cbfun SizeCallback) (previous SizeCallback) { glfwDLL.call("glfwSetWindowSizeCallback", w.w, uintptr(cbfun)) panicError() diff --git a/internal/glfw/type.go b/internal/glfw/type.go index c7c767dba..3a978eec4 100644 --- a/internal/glfw/type.go +++ b/internal/glfw/type.go @@ -19,6 +19,7 @@ package glfw type ( CharModsCallback uintptr + CloseCallback uintptr FramebufferSizeCallback uintptr ScrollCallback uintptr SizeCallback uintptr diff --git a/internal/uidriver/glfw/ui.go b/internal/uidriver/glfw/ui.go index 72dc70a6b..b56845ea9 100644 --- a/internal/uidriver/glfw/ui.go +++ b/internal/uidriver/glfw/ui.go @@ -64,14 +64,16 @@ type UserInterface struct { maxWindowWidthInDP int maxWindowHeightInDP int - running uint32 - toChangeSize bool - origPosX int - origPosY int - runnableOnUnfocused bool - vsync bool - iconImages []image.Image - cursorShape driver.CursorShape + running uint32 + toChangeSize bool + origPosX int + origPosY int + runnableOnUnfocused bool + vsync bool + iconImages []image.Image + cursorShape driver.CursorShape + windowClosingHandled bool + windowBeingClosed bool // setSizeCallbackEnabled must be accessed from the main thread. setSizeCallbackEnabled bool @@ -108,6 +110,7 @@ type UserInterface struct { iwindow window sizeCallback glfw.SizeCallback + closeCallback glfw.CloseCallback framebufferSizeCallback glfw.FramebufferSizeCallback framebufferSizeCallbackCh chan struct{} @@ -476,6 +479,26 @@ func (u *UserInterface) setInitWindowMaximized(maximized bool) { u.m.Unlock() } +func (u *UserInterface) isWindowClosingHandled() bool { + u.m.Lock() + v := u.windowClosingHandled + u.m.Unlock() + return v +} + +func (u *UserInterface) setWindowClosingHandled(handled bool) { + u.m.Lock() + u.windowClosingHandled = handled + u.m.Unlock() +} + +func (u *UserInterface) isWindowBeingClosed() bool { + u.m.Lock() + v := u.windowBeingClosed + u.m.Unlock() + return v +} + func (u *UserInterface) isInitFocused() bool { u.m.Lock() v := u.initFocused @@ -724,6 +747,7 @@ func (u *UserInterface) createWindow() error { // TODO: Set icons u.registerWindowSetSizeCallback() + u.registerWindowCloseCallback() return nil } @@ -776,6 +800,23 @@ func (u *UserInterface) registerWindowSetSizeCallback() { u.window.SetSizeCallback(u.sizeCallback) } +// registerWindowCloseCallback must be called from the main thread. +func (u *UserInterface) registerWindowCloseCallback() { + if u.closeCallback == 0 { + u.closeCallback = glfw.ToCloseCallback(func(_ *glfw.Window) { + u.m.Lock() + u.windowBeingClosed = true + u.m.Unlock() + + if !u.isWindowClosingHandled() { + return + } + u.window.SetShouldClose(false) + }) + } + u.window.SetCloseCallback(u.closeCallback) +} + func (u *UserInterface) init() error { if u.Graphics().IsGL() { glfw.WindowHint(glfw.ClientAPI, glfw.OpenGLAPI) @@ -1347,6 +1388,10 @@ func (u *UserInterface) ResetForFrame() { u.context.Layout(w, h) } u.input.resetForFrame() + + u.m.Lock() + u.windowBeingClosed = false + u.m.Unlock() } func (u *UserInterface) MonitorPosition() (int, int) { diff --git a/internal/uidriver/glfw/window.go b/internal/uidriver/glfw/window.go index bfa825fcf..8820567ae 100644 --- a/internal/uidriver/glfw/window.go +++ b/internal/uidriver/glfw/window.go @@ -264,3 +264,15 @@ func (w *window) SetTitle(title string) { return nil }) } + +func (w *window) IsBeingClosed() bool { + return w.ui.isWindowBeingClosed() +} + +func (w *window) SetClosingHandled(handled bool) { + w.ui.setWindowClosingHandled(handled) +} + +func (w *window) IsClosingHandled() bool { + return w.ui.isWindowClosingHandled() +} diff --git a/window.go b/window.go index 0e87cc62f..ad922317e 100644 --- a/window.go +++ b/window.go @@ -315,3 +315,46 @@ func RestoreWindow() { w.Restore() } } + +// IsWindowBeingClosed returns true when the user is trying to close the window on desktops. +// As the window is closed immediately by default, +// you might want to call SetWindowClosingHandled(true) to prevent the window is automatically closed. +// +// IsWindowBeingClosed always returns false on other platforms. +// +// IsWindowBeingClosed is concurrent-safe. +func IsWindowBeingClosed() bool { + if w := uiDriver().Window(); w != nil { + return w.IsBeingClosed() + } + return false +} + +// SetWindowClosingHandled sets whether the window closing is handled or not on desktops. The default state is false. +// +// If the window closing is handled, the window is not closed immediately and +// the game can know whether the window is begin closed or not by IsWindowBeingClosed. +// In this case, the window is not closed automatically. +// To end the game, you have to return an error value at the Game's Update function. +// +// SetWindowClosingHandled works only on desktops. +// SetWindowClosingHandled does nothing on other platforms. +// +// SetWindowClosingHandled is concurrent-safe. +func SetWindowClosingHandled(handled bool) { + if w := uiDriver().Window(); w != nil { + w.SetClosingHandled(handled) + } +} + +// IsWindowClosingHandled reports whether the window closing is handled or not on desktops by SetWindowClosingHandled. +// +// IsWindowClosingHandled always returns false on other platforms. +// +// IsWindowClosingHandled is concurrent-safe. +func IsWindowClosingHandled() bool { + if w := uiDriver().Window(); w != nil { + return w.IsClosingHandled() + } + return false +}