diff --git a/examples/mascot/main.go b/examples/mascot/main.go index 415e799b9..5bcd99369 100644 --- a/examples/mascot/main.go +++ b/examples/mascot/main.go @@ -145,6 +145,7 @@ func main() { op := &ebiten.RunGameOptions{} op.ScreenTransparent = true + op.SkipTaskbar = true if err := ebiten.RunGameWithOptions(&mascot{}, op); err != nil { log.Fatal(err) } diff --git a/internal/ui/api_windows.go b/internal/ui/api_windows.go index 3d047ebca..cd6f18826 100644 --- a/internal/ui/api_windows.go +++ b/internal/ui/api_windows.go @@ -17,15 +17,37 @@ package ui import ( + "errors" "fmt" + "runtime" + "syscall" "unsafe" "golang.org/x/sys/windows" ) const ( - _SM_CYCAPTION = 4 + _CLSCTX_INPROC_SERVER = 0x1 + _CLSCTX_LOCAL_SERVER = 0x4 + _CLSCTX_REMOTE_SERVER = 0x10 + _CLSCTX_SERVER = _CLSCTX_INPROC_SERVER | _CLSCTX_LOCAL_SERVER | _CLSCTX_REMOTE_SERVER _MONITOR_DEFAULTTONEAREST = 2 + _SM_CYCAPTION = 4 +) + +var ( + _CLSID_TaskbarList = windows.GUID{ + Data1: 0x56FDF344, + Data2: 0xFD6D, + Data3: 0x11D0, + Data4: [...]byte{0x95, 0x8A, 0x00, 0x60, 0x97, 0xC9, 0xA0, 0x90}, + } + _IID_ITaskbarList = windows.GUID{ + Data1: 0x56FDF342, + Data2: 0xFD6D, + Data3: 0x11D0, + Data4: [...]byte{0x95, 0x8A, 0x00, 0x60, 0x97, 0xC9, 0xA0, 0x90}, + } ) type _RECT struct { @@ -48,14 +70,28 @@ type _POINT struct { } var ( + ole32 = windows.NewLazySystemDLL("ole32.dll") user32 = windows.NewLazySystemDLL("user32.dll") + procCoCreateInstance = ole32.NewProc("CoCreateInstance") + procGetSystemMetrics = user32.NewProc("GetSystemMetrics") procMonitorFromWindow = user32.NewProc("MonitorFromWindow") procGetMonitorInfoW = user32.NewProc("GetMonitorInfoW") procGetCursorPos = user32.NewProc("GetCursorPos") ) +func _CoCreateInstance(rclsid *windows.GUID, pUnkOuter unsafe.Pointer, dwClsContext uint32, riid *windows.GUID) (unsafe.Pointer, error) { + var ptr unsafe.Pointer + r, _, _ := procCoCreateInstance.Call(uintptr(unsafe.Pointer(rclsid)), uintptr(pUnkOuter), uintptr(dwClsContext), uintptr(unsafe.Pointer(riid)), uintptr(unsafe.Pointer(&ptr))) + runtime.KeepAlive(rclsid) + runtime.KeepAlive(riid) + if uint32(r) != uint32(windows.S_OK) { + return nil, fmt.Errorf("ui: CoCreateInstance failed: error code: HRESULT(%d)", uint32(r)) + } + return ptr, nil +} + func _GetSystemMetrics(nIndex int) (int32, error) { r, _, _ := procGetSystemMetrics.Call(uintptr(nIndex)) if int32(r) == 0 { @@ -77,7 +113,7 @@ func _GetMonitorInfoW(hMonitor uintptr) (_MONITORINFO, error) { r, _, e := procGetMonitorInfoW.Call(hMonitor, uintptr(unsafe.Pointer(&mi))) if int32(r) == 0 { - if e != nil && e != windows.ERROR_SUCCESS { + if e != nil && !errors.Is(e, windows.ERROR_SUCCESS) { return _MONITORINFO{}, fmt.Errorf("ui: GetMonitorInfoW failed: error code: %w", e) } return _MONITORINFO{}, fmt.Errorf("ui: GetMonitorInfoW failed: returned 0") @@ -89,10 +125,38 @@ func _GetCursorPos() (int32, int32, error) { var pt _POINT r, _, e := procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt))) if int32(r) == 0 { - if e != nil && e != windows.ERROR_SUCCESS { + if e != nil && !errors.Is(e, windows.ERROR_SUCCESS) { return 0, 0, fmt.Errorf("ui: GetCursorPos failed: error code: %w", e) } return 0, 0, fmt.Errorf("ui: GetCursorPos failed: returned 0") } return pt.x, pt.y, nil } + +type _ITaskbarList struct { + vtbl *_ITaskbarList_Vtbl +} + +type _ITaskbarList_Vtbl struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr + + HrInit uintptr + AddTab uintptr + DeleteTab uintptr + ActivateTab uintptr + SetActiveAlt uintptr +} + +func (i *_ITaskbarList) DeleteTab(hwnd windows.HWND) error { + r, _, _ := syscall.Syscall(i.vtbl.DeleteTab, 2, uintptr(unsafe.Pointer(i)), uintptr(hwnd), 0) + if uint32(r) != uint32(windows.S_OK) { + return fmt.Errorf("ui: ITaskbarList::DeleteTab failed: HRESULT(%d)", uint32(r)) + } + return nil +} + +func (i *_ITaskbarList) Release() { + syscall.Syscall(i.vtbl.Release, 1, uintptr(unsafe.Pointer(i)), 0, 0) +} diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 753b72f7e..9b28a1662 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -100,4 +100,5 @@ type RunOptions struct { GraphicsLibrary GraphicsLibrary InitUnfocused bool ScreenTransparent bool + SkipTaskbar bool } diff --git a/internal/ui/ui_glfw.go b/internal/ui/ui_glfw.go index ab656c193..6b66ee94c 100644 --- a/internal/ui/ui_glfw.go +++ b/internal/ui/ui_glfw.go @@ -940,6 +940,11 @@ func (u *userInterfaceImpl) init(options *RunOptions) error { u.setWindowResizingModeForOS(u.windowResizingMode) + if options.SkipTaskbar { + // Ignore the error. + _ = u.skipTaskbar() + } + u.window.Show() if g, ok := u.graphicsDriver.(interface{ SetWindow(uintptr) }); ok { diff --git a/internal/ui/ui_glfw_darwin.go b/internal/ui/ui_glfw_darwin.go index b124646a9..cec1df68a 100644 --- a/internal/ui/ui_glfw_darwin.go +++ b/internal/ui/ui_glfw_darwin.go @@ -385,3 +385,7 @@ func initializeWindowAfterCreation(w *glfw.Window) { delegate := objc.ID(class_EbitengineWindowDelegate).Send(objc.RegisterName("alloc")).Send(objc.RegisterName("initWithOrigDelegate:"), nswindow.Send(sel_delegate)) nswindow.Send(objc.RegisterName("setDelegate:"), delegate) } + +func (u *userInterfaceImpl) skipTaskbar() error { + return nil +} diff --git a/internal/ui/ui_glfw_unix.go b/internal/ui/ui_glfw_unix.go index 2bc00984b..0060c958d 100644 --- a/internal/ui/ui_glfw_unix.go +++ b/internal/ui/ui_glfw_unix.go @@ -229,3 +229,7 @@ func initializeWindowAfterCreation(w *glfw.Window) { // Apparently the window state is inconsistent just after the window is created, but we are not sure. // For more details, see the discussion in #1829. } + +func (u *userInterfaceImpl) skipTaskbar() error { + return nil +} diff --git a/internal/ui/ui_glfw_windows.go b/internal/ui/ui_glfw_windows.go index 79ae17ab5..dd164957e 100644 --- a/internal/ui/ui_glfw_windows.go +++ b/internal/ui/ui_glfw_windows.go @@ -185,3 +185,24 @@ func (u *userInterfaceImpl) setWindowResizingModeForOS(mode WindowResizingMode) func initializeWindowAfterCreation(w *glfw.Window) { } + +func (u *userInterfaceImpl) skipTaskbar() error { + if err := windows.CoInitializeEx(0, windows.COINIT_MULTITHREADED); err != nil { + return err + } + defer windows.CoUninitialize() + + ptr, err := _CoCreateInstance(&_CLSID_TaskbarList, nil, _CLSCTX_SERVER, &_IID_ITaskbarList) + if err != nil { + return err + } + + t := (*_ITaskbarList)(ptr) + defer t.Release() + + if err := t.DeleteTab(windows.HWND(u.window.GetWin32Window())); err != nil { + return err + } + + return nil +} diff --git a/run.go b/run.go index 5a63bc3fd..ee08c0ef1 100644 --- a/run.go +++ b/run.go @@ -236,17 +236,23 @@ type RunGameOptions struct { // The default (zero) value is GraphicsLibraryAuto, which lets Ebitengine choose the graphics library. GraphicsLibrary GraphicsLibrary - // InitUnfocused represents whether the window is unfocused or not on launching. + // InitUnfocused indicates whether the window is unfocused or not on launching. // InitUnfocused is valid on desktops and browsers. // // The default (zero) value is false, which means that the window is focused. InitUnfocused bool - // ScreenTransparent represents whether the window is transparent or not. + // ScreenTransparent indicates whether the window is transparent or not. // ScreenTransparent is valid on desktops and browsers. // // The default (zero) value is false, which means that the window is not transparent. ScreenTransparent bool + + // SkipTaskbar indicates whether an application icon is shown on a taskbar or not. + // SkipTaskbar is valid only on Windows. + // + // The default (zero) value is false, which means that an icon is shown on a taskbar. + SkipTaskbar bool } // RunGameWithOptions starts the main loop and runs the game with the specified options. @@ -656,5 +662,6 @@ func toUIRunOptions(options *RunGameOptions) *ui.RunOptions { GraphicsLibrary: ui.GraphicsLibrary(options.GraphicsLibrary), InitUnfocused: options.InitUnfocused, ScreenTransparent: options.ScreenTransparent, + SkipTaskbar: options.SkipTaskbar, } }