diff --git a/internal/glfw/api_windows.go b/internal/glfw/api_windows.go index 6255cd3fa..a9746678b 100644 --- a/internal/glfw/api_windows.go +++ b/internal/glfw/api_windows.go @@ -100,6 +100,7 @@ const ( _MAPVK_VSC_TO_VK = 1 _MONITOR_DEFAULTTONEAREST = 0x00000002 _MOUSE_MOVE_ABSOLUTE = 0x01 + _MOUSE_VIRTUAL_DESKTOP = 0x02 _MSGFLT_ALLOW = 1 _OCR_CROSS = 32515 _OCR_HAND = 32649 @@ -141,11 +142,18 @@ const ( _SIZE_MAXIMIZED = 2 _SIZE_MINIMIZED = 1 _SIZE_RESTORED = 0 + _SM_CXCURSOR = 13 _SM_CXICON = 11 _SM_CXSMICON = 49 _SM_CYCAPTION = 4 + _SM_CYCURSOR = 14 _SM_CYICON = 12 + _SM_CXSCREEN = 0 + _SM_CYSCREEN = 1 _SM_CYSMICON = 50 + _SM_CXVIRTUALSCREEN = 78 + _SM_CYVIRTUALSCREEN = 79 + _SM_REMOTESESSION = 0x1000 _SPI_GETFOREGROUNDLOCKTIMEOUT = 0x2000 _SPI_GETMOUSETRAILS = 94 _SPI_SETFOREGROUNDLOCKTIMEOUT = 0x2001 @@ -755,9 +763,11 @@ var ( procChangeWindowMessageFilterEx = user32.NewProc("ChangeWindowMessageFilterEx") procClientToScreen = user32.NewProc("ClientToScreen") procClipCursor = user32.NewProc("ClipCursor") + procCreateCursor = user32.NewProc("CreateCursor") procCreateIconIndirect = user32.NewProc("CreateIconIndirect") procCreateWindowExW = user32.NewProc("CreateWindowExW") procDefWindowProcW = user32.NewProc("DefWindowProcW") + procDestroyCursor = user32.NewProc("DestroyCursor") procDestroyIcon = user32.NewProc("DestroyIcon") procDestroyWindow = user32.NewProc("DestroyWindow") procDispatchMessageW = user32.NewProc("DispatchMessageW") @@ -915,6 +925,26 @@ func _ClipCursor(lpRect *_RECT) error { return nil } +func _CreateCursor(hInst _HINSTANCE, xHotSpot int32, yHotSpot int32, nWidth int32, nHeight int32, pvANDPlane, pvXORPlane []byte) (_HCURSOR, error) { + var andPlane *byte + if len(pvANDPlane) > 0 { + andPlane = &pvANDPlane[0] + } + var xorPlane *byte + if len(pvXORPlane) > 0 { + xorPlane = &pvXORPlane[0] + } + + r, _, e := procCreateCursor.Call(uintptr(hInst), uintptr(xHotSpot), uintptr(yHotSpot), uintptr(nWidth), uintptr(nHeight), uintptr(unsafe.Pointer(andPlane)), uintptr(unsafe.Pointer(xorPlane))) + runtime.KeepAlive(pvANDPlane) + runtime.KeepAlive(pvXORPlane) + + if _HCURSOR(r) == 0 && !errors.Is(e, windows.ERROR_SUCCESS) { + return 0, fmt.Errorf("glfw: CreateCursor failed: %w", e) + } + return _HCURSOR(r), nil +} + func _CreateBitmap(nWidth int32, nHeight int32, nPlanes uint32, nBitCount uint32, lpBits unsafe.Pointer) (_HBITMAP, error) { r, _, e := procCreateBitmap.Call(uintptr(nWidth), uintptr(nHeight), uintptr(nPlanes), uintptr(nBitCount), uintptr(lpBits)) if _HBITMAP(r) == 0 { @@ -986,6 +1016,14 @@ func _DefWindowProcW(hWnd windows.HWND, uMsg uint32, wParam _WPARAM, lParam _LPA return _LRESULT(r) } +func _DestroyCursor(hCursor _HCURSOR) error { + r, _, e := procDestroyCursor.Call(uintptr(hCursor)) + if int32(r) == 0 && !errors.Is(e, windows.ERROR_SUCCESS) { + return fmt.Errorf("glfw: DestroyCursor failed: %w", e) + } + return nil +} + func _DestroyIcon(hIcon _HICON) error { r, _, e := procDestroyIcon.Call(uintptr(hIcon)) if int32(r) == 0 && !errors.Is(e, windows.ERROR_SUCCESS) { diff --git a/internal/glfw/win32_init_windows.go b/internal/glfw/win32_init_windows.go index 4e7f5eaa7..b33027786 100644 --- a/internal/glfw/win32_init_windows.go +++ b/internal/glfw/win32_init_windows.go @@ -239,6 +239,55 @@ func createHelperWindow() error { return nil } +func createBlankCursor() error { + // HACK: Create a transparent cursor as using the NULL cursor breaks + // using SetCursorPos when connected over RDP + cursorWidth, err := _GetSystemMetrics(_SM_CXCURSOR) + if err != nil { + return err + } + cursorHeight, err := _GetSystemMetrics(_SM_CYCURSOR) + if err != nil { + return err + } + andMask := make([]byte, cursorWidth*cursorHeight/8) + for i := range andMask { + andMask[i] = 0xff + } + xorMask := make([]byte, cursorWidth*cursorHeight/8) + + // Cursor creation might fail, but that's fine as we get NULL in that case, + // which serves as an acceptable fallback blank cursor (other than on RDP) + c, _ := _CreateCursor(0, 0, 0, cursorWidth, cursorHeight, andMask, xorMask) + _glfw.platformWindow.blankCursor = c + + return nil +} + +func initRemoteSession() error { + if microsoftgdk.IsXbox() { + return nil + } + + // Check if the current progress was started with Remote Desktop. + r, err := _GetSystemMetrics(_SM_REMOTESESSION) + if err != nil { + return err + } + _glfw.platformWindow.isRemoteSession = r > 0 + + // With Remote desktop, we need to create a blank cursor because of the cursor is Set to nil + // if cannot be moved to center in capture mode. If not Remote Desktop platformWindow.blankCursor stays nil + // and will perform has before (normal). + if _glfw.platformWindow.isRemoteSession { + if err := createBlankCursor(); err != nil { + return err + } + } + + return nil +} + func platformInit() error { // Changing the foreground lock timeout was removed from the original code. // See https://github.com/glfw/glfw/commit/58b48a3a00d9c2a5ca10cc23069a71d8773cc7a4 @@ -293,6 +342,10 @@ func platformInit() error { return err } } else { + // Some hacks are needed to support Remote Desktop... + if err := initRemoteSession(); err != nil { + return err + } if err := pollMonitorsWin32(); err != nil { return err } @@ -301,6 +354,12 @@ func platformInit() error { } func platformTerminate() error { + if _glfw.platformWindow.blankCursor != 0 { + if err := _DestroyCursor(_glfw.platformWindow.blankCursor); err != nil { + return err + } + } + if _glfw.platformWindow.deviceNotificationHandle != 0 { if err := _UnregisterDeviceNotification(_glfw.platformWindow.deviceNotificationHandle); err != nil { return err diff --git a/internal/glfw/win32_platform_windows.go b/internal/glfw/win32_platform_windows.go index 7c1e50e1f..4199c5dcd 100644 --- a/internal/glfw/win32_platform_windows.go +++ b/internal/glfw/win32_platform_windows.go @@ -66,14 +66,18 @@ type platformLibraryWindowState struct { scancodes [KeyLast + 1]int keynames [KeyLast + 1]string - // Where to place the cursor when re-enabled + // restoreCursorPosX and restoreCursorPosY indicates where to place the cursor when re-enabled restoreCursorPosX float64 restoreCursorPosY float64 - // The window whose disabled cursor mode is active + // disabledCursorWindow is the window whose disabled cursor mode is active disabledCursorWindow *Window - // The window the cursor is captured in + // capturedCursorWindow is the window the cursor is captured in capturedCursorWindow *Window rawInput []byte mouseTrailSize uint32 + // isRemoteSession indicates if the process was started behind Remote Destop + isRemoteSession bool + // blankCursor is an invisible cursor, needed for special cases (see WM_INPUT handler) + blankCursor _HCURSOR } diff --git a/internal/glfw/win32_window_windows.go b/internal/glfw/win32_window_windows.go index 3bba41122..6fcb33356 100644 --- a/internal/glfw/win32_window_windows.go +++ b/internal/glfw/win32_window_windows.go @@ -167,7 +167,10 @@ func (w *Window) updateCursorImage() error { _SetCursor(cursor) } } else { - _SetCursor(0) + // Connected via Remote Desktop, nil cursor will present SetCursorPos the move the cursor. + // using a blank cursor fix that. + // When not via Remote Desktop, platformWindow.blankCursor should be nil. + _SetCursor(_glfw.platformWindow.blankCursor) } return nil } @@ -907,8 +910,46 @@ func windowProc(hWnd windows.HWND, uMsg uint32, wParam _WPARAM, lParam _LPARAM) var dx, dy int data := (*_RAWINPUT)(unsafe.Pointer(&_glfw.platformWindow.rawInput[0])) if data.mouse.usFlags&_MOUSE_MOVE_ABSOLUTE != 0 { - dx = int(data.mouse.lLastX) - window.platform.lastCursorPosX - dy = int(data.mouse.lLastY) - window.platform.lastCursorPosY + if _glfw.platformWindow.isRemoteSession { + // Remote Desktop Mode + // As per https://github.com/Microsoft/DirectXTK/commit/ef56b63f3739381e451f7a5a5bd2c9779d2a7555 + // MOUSE_MOVE_ABSOLUTE is a range from 0 through 65535, based on the screen size. + // Apparently, absolute mode only occurs over RDP though. + var smx int32 = _SM_CXSCREEN + var smy int32 = _SM_CYSCREEN + if data.mouse.usFlags&_MOUSE_VIRTUAL_DESKTOP != 0 { + smx = _SM_CXVIRTUALSCREEN + smy = _SM_CYVIRTUALSCREEN + } + + width, err := _GetSystemMetrics(smx) + if err != nil { + _glfw.errors = append(_glfw.errors, err) + return 0 + } + height, err := _GetSystemMetrics(smy) + if err != nil { + _glfw.errors = append(_glfw.errors, err) + return 0 + } + + pos := _POINT{ + x: int32(float64(data.mouse.lLastX) / 65535.0 * float64(width)), + y: int32(float64(data.mouse.lLastY) / 65535.0 * float64(height)), + } + if err := _ScreenToClient(window.platform.handle, &pos); err != nil { + _glfw.errors = append(_glfw.errors, err) + return 0 + } + + dx = int(pos.x) - window.platform.lastCursorPosX + dy = int(pos.y) - window.platform.lastCursorPosY + } else { + // Normal mode + // We should have the right absolute coords in data.mouse + dx = int(data.mouse.lLastX) - window.platform.lastCursorPosX + dy = int(data.mouse.lLastY) - window.platform.lastCursorPosY + } } else { dx = int(data.mouse.lLastX) dy = int(data.mouse.lLastY) @@ -2159,6 +2200,7 @@ func platformPollEvents() error { // NOTE: Re-center the cursor only if it has moved since the last call, // to avoid breaking glfwWaitEvents with WM_MOUSEMOVE + // The re-center is required in order to prevent the mouse cursor stopping at the edges of the screen. if window.platform.lastCursorPosX != width/2 || window.platform.lastCursorPosY != height/2 { if err := window.platformSetCursorPos(float64(width/2), float64(height/2)); err != nil { return err