//go:build windows package win32 import ( "fmt" "sync" "syscall" "unsafe" ) // WindowSubclass intercepts the WM_CLOSE message on an external window // (in our case Gio's app window) and redirects it to a Go callback. The // caller decides whether to actually close, hide, or ignore. // // Implementation notes // // - We swap the window procedure via SetWindowLongPtrW(GWLP_WNDPROC), // keeping a reference to the original so non-WM_CLOSE messages pass // through unchanged. // - The Go callback runs on the UI thread of the subclassed window // (because that's who DispatchMessage'd into our hook). Returning // false from the callback drops the message; true forwards it on. // - Only one subclass per HWND is supported. Calling Install a second // time on the same HWND returns an error. type WindowSubclass struct { hwnd uintptr origProc uintptr cb func() bool once sync.Once gone bool } var ( subclassMu sync.Mutex subclasses = map[uintptr]*WindowSubclass{} ) // InstallCloseHook subclasses hwnd and routes WM_CLOSE through onClose. // If onClose returns false the close is swallowed; if true the original // window procedure handles it (the default behaviour, which destroys // the window). func InstallCloseHook(hwnd uintptr, onClose func() bool) (*WindowSubclass, error) { if hwnd == 0 { return nil, fmt.Errorf("win32: InstallCloseHook: nil hwnd") } subclassMu.Lock() defer subclassMu.Unlock() if _, exists := subclasses[hwnd]; exists { return nil, fmt.Errorf("win32: hwnd %#x already subclassed", hwnd) } s := &WindowSubclass{hwnd: hwnd, cb: onClose} proc := syscall.NewCallback(s.wndProc) orig, _, e := procSetWindowLongPtrW.Call(hwnd, gwlpWndProc, proc) if orig == 0 { if errno, ok := e.(syscall.Errno); ok && errno != 0 { return nil, fmt.Errorf("win32: SetWindowLongPtrW: %w", errno) } return nil, fmt.Errorf("win32: SetWindowLongPtrW returned 0") } s.origProc = orig subclasses[hwnd] = s return s, nil } // Remove restores the original window procedure. Safe to call more than // once; subsequent calls are no-ops. func (s *WindowSubclass) Remove() { s.once.Do(func() { s.gone = true subclassMu.Lock() delete(subclasses, s.hwnd) subclassMu.Unlock() procSetWindowLongPtrW.Call(s.hwnd, gwlpWndProc, s.origProc) }) } func (s *WindowSubclass) wndProc(hwnd uintptr, msg uint32, wParam, lParam uintptr) uintptr { if msg == wmClose && s.cb != nil && !s.gone { if !s.cb() { return 0 } } r, _, _ := procCallWindowProcW.Call(s.origProc, hwnd, uintptr(msg), wParam, lParam) return r } // ShowWindow toggles a window's visibility. cmdShow uses the standard // SW_* constants (SW_HIDE = 0, SW_SHOW = 5, SW_RESTORE = 9). func ShowWindow(hwnd uintptr, cmdShow int32) { procShowWindowProc.Call(hwnd, uintptr(cmdShow)) } // SW_* constants exposed so callers do not need to redefine them. const ( SW_HIDE int32 = 0 SW_SHOW int32 = 5 SW_RESTORE int32 = 9 ) // FindWindowByTitle searches top-level windows for one whose title // exactly matches. Returns 0 when nothing matches. func FindWindowByTitle(title string) uintptr { wtitle, err := syscall.UTF16PtrFromString(title) if err != nil { return 0 } r, _, _ := procFindWindowW.Call(0, uintptr(unsafe.Pointer(wtitle))) return r } // PostCloseMessage asks the window to close the same way the user // clicking the X would. Combined with InstallCloseHook this lets the // tray "Exit" item route through the same path as a real close, so the // hook can let it through after setting an "exiting" flag. func PostCloseMessage(hwnd uintptr) { procPostMessageW.Call(hwnd, uintptr(wmClose), 0, 0) } // ----------------------------------------------------------------------------- // raw syscalls var ( procSetWindowLongPtrW = user32.NewProc(setWindowLongPtrName) procCallWindowProcW = user32.NewProc("CallWindowProcW") ) const ( wmClose uint32 = 0x0010 ) // gwlpWndProc is the GWLP_WNDPROC index. Declared as a var (not a const) // because Go forbids constant negative-to-uintptr conversion. var gwlpWndProc = func() uintptr { x := int32(-4) return uintptr(x) }()