Skip to content

ebiten: add a way to specify DWM window attributes on Windows #3381

@djweber

Description

@djweber

Operating System

  • Windows
  • macOS
  • Linux
  • FreeBSD
  • OpenBSD
  • Android
  • iOS
  • Nintendo Switch
  • PlayStation 5
  • Xbox
  • Web Browsers

What feature would you like to be added?

A method to expose the game window's handle to the user once the game window has been created, potentially with a documented point in the lifecycle where it is guaranteed to be available.

Why is this needed?

Getting the handle for the game window to set DWM attributes for window styling currently requires brittle mechanisms to query the Win32 APIs. You can enumerate the windows and either look up the game window handle by its title or its PID but requires a good bit of boilerplate that is easy to mess up. Additionally, you must use arbitrary delays to correctly style the game window after it is created. Here is example code to grab a handle:

package windows

import (
	"fmt"
	"lock-on-labs/slip-hop/internal/window/theme/windows/backdrop"
	"os"
	"syscall"
	"time"
	"unsafe"

	"golang.org/x/sys/windows"
)

var (
	user32                       = syscall.NewLazyDLL("user32.dll")
	procEnumWindows              = user32.NewProc("EnumWindows")
	procGetWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId")
	procIsWindowVisible          = user32.NewProc("IsWindowVisible")
)

const dwmwaSystemBackdropType = 38

func findWindowByProcessId(myPid uint32) windows.HWND {
	var target windows.HWND

	cb := syscall.NewCallback(func(hwnd, lParam uintptr) uintptr {
		var pid uint32
		procGetWindowThreadProcessId.Call(hwnd, uintptr(unsafe.Pointer(&pid)))

		if myPid == pid {
			ret, _, _ := procIsWindowVisible.Call(hwnd)

			if ret != 0 {
				target = windows.HWND(hwnd)
				return 0
			}
		}
		return 1
	})

	procEnumWindows.Call(cb, 0)

	return target
}

func ApplyBackdrop(backdrop backdrop.Backdrop) {
	pid := uint32(os.Getpid())

	var window windows.HWND

	// time out if we don't fetch the window within 5 seconds
	deadline := time.Now().Add(5 * time.Second)

	for window == 0 {
		if time.Now().After(deadline) {
			return
		}
		window = findWindowByProcessId(pid)
		time.Sleep(10 * time.Millisecond)
	}

	darkMode := uint32(1) // dark mode
	if err := windows.DwmSetWindowAttribute(window,
		windows.DWMWA_USE_IMMERSIVE_DARK_MODE,
		unsafe.Pointer(&darkMode),
		uint32(unsafe.Sizeof(darkMode)),
	); err != nil {
		fmt.Printf("DWMWA_USE_IMMERSIVE_DARK_MODE failed: %v\n", err)
	}

	if err := windows.DwmSetWindowAttribute(window,
		dwmwaSystemBackdropType,
		unsafe.Pointer(&backdrop),
		uint32(unsafe.Sizeof(backdrop)),
	); err != nil {
		fmt.Printf("DWMWA_SYSTEMBACKDROP_TYPE failed: %v\n", err)
	}
}

Invoking it from the root game creation:

func main() {
	ebiten.SetWindowTitle(title)
	ebiten.SetWindowSize(windowWidth, windowHeight)

	if runtime.GOOS == "windows" {
		go theme.ApplyBackdrop(theme.MainWindow)
	}

	g := game.NewGame()

	err := ebiten.RunGame(&g)

	if err != nil {
		log.Fatal(err)
	}
}

Ideally, advanced users could be able to get a pointer to the game window with a lifecycle guarantee without enumerating over windows or by using arbitrary delays.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions