From b87367aa9ac3ccdbfde16c5bb56a9aae5eab009e Mon Sep 17 00:00:00 2001 From: Stefan Verleysen Date: Thu, 19 Mar 2026 14:43:11 -0400 Subject: [PATCH 1/2] fix: Win10 IoT touch fixes on top of Nick's v6 Three fixes for touch-activates-screen on Windows 10 IoT Enterprise LTSC: 1. Edge bounce clamp: entry coordinates clamped 1px inside destination screen after avoidJumpZone (pre-existing bug, 10+ GitHub issues) 2. Cursor center filter: LL hook ignores touch events within 1px of cursor center to prevent warp-echo false activation 3. LogicalMax sign extension: handle HID descriptors where 0xFFFF sign-extends to -1 in HIDP_VALUE_CAPS.LogicalMax Also stopped eating touch event on primary so click passes through. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/platform/MSWindowsDesks.cpp | 16 ++++++++++++++-- src/lib/platform/MSWindowsHook.cpp | 24 ++++++++++++++++++++---- src/lib/platform/MSWindowsHook.h | 2 ++ src/lib/platform/MSWindowsScreen.cpp | 2 ++ src/lib/server/Server.cpp | 11 +++++++++++ 5 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index 73d2c4695..ab2ef6798 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -883,12 +883,21 @@ MSWindowsDesks::HidTouchDevice MSWindowsDesks::initHidTouchDevice(HANDLE hDevice USAGE usage = vc.IsRange ? vc.Range.UsageMin : vc.NotRange.Usage; auto &ci = collections[vc.LinkCollection]; + + // Windows sign-extends LogicalMax into a LONG using BitSize bits. + // A 16-bit descriptor with max 0xFFFF becomes -1 as a signed LONG. + // Reconstruct the unsigned value from the bit width. + LONG logMax = vc.LogicalMax; + if (logMax < 0 && vc.BitSize > 0 && vc.BitSize < 32) { + logMax = static_cast((1UL << vc.BitSize) - 1); + } + if (usage == HID_USAGE_GENERIC_X) { ci.hasX = true; - ci.maxX = vc.LogicalMax > 0 ? vc.LogicalMax : vc.PhysicalMax; + ci.maxX = logMax > 0 ? logMax : vc.PhysicalMax; } else if (usage == HID_USAGE_GENERIC_Y) { ci.hasY = true; - ci.maxY = vc.LogicalMax > 0 ? vc.LogicalMax : vc.PhysicalMax; + ci.maxY = logMax > 0 ? logMax : vc.PhysicalMax; } } @@ -954,6 +963,9 @@ bool MSWindowsDesks::parseHidTouch(const RAWINPUT *raw, const HidTouchDevice &de // Touch digitizer maps to the primary monitor, not the virtual desktop SInt32 pw = GetSystemMetrics(SM_CXSCREEN); SInt32 ph = GetSystemMetrics(SM_CYSCREEN); + if (dev.logicalMaxX == 0 || dev.logicalMaxY == 0) { + return false; + } outX = static_cast(rawX * pw / dev.logicalMaxX); outY = static_cast(rawY * ph / dev.logicalMaxY); LOG((CLOG_DEBUG1 "touch HID: raw=%lu,%lu logMax=%lu,%lu primary=%dx%d -> %d,%d", diff --git a/src/lib/platform/MSWindowsHook.cpp b/src/lib/platform/MSWindowsHook.cpp index da2a23ea3..4e03063f1 100644 --- a/src/lib/platform/MSWindowsHook.cpp +++ b/src/lib/platform/MSWindowsHook.cpp @@ -46,6 +46,8 @@ static bool g_fakeServerInput = false; static BOOL g_isPrimary = TRUE; static BOOL g_isOnScreen = TRUE; static bool g_touchActivateScreen = false; +static SInt32 g_xCenter = 0; +static SInt32 g_yCenter = 0; MSWindowsHook::MSWindowsHook() { @@ -166,6 +168,12 @@ void MSWindowsHook::setIsOnScreen(bool onScreen) g_isOnScreen = onScreen ? TRUE : FALSE; } +void MSWindowsHook::setCursorCenter(SInt32 x, SInt32 y) +{ + g_xCenter = x; + g_yCenter = y; +} + static void keyboardGetState(BYTE keys[256], DWORD vkCode, bool kf_up) { // we have to use GetAsyncKeyState() rather than GetKeyState() because @@ -615,12 +623,20 @@ static LRESULT CALLBACK mouseLLHook(int code, WPARAM wParam, LPARAM lParam) if (isTouchEvent && (wParam == WM_LBUTTONDOWN || wParam == WM_MOUSEMOVE)) { SInt32 x = static_cast(info->pt.x); SInt32 y = static_cast(info->pt.y); - LOG((CLOG_DEBUG "hook: touch at %d,%d posting DESKFLOW_MSG_TOUCH", x, y)); - PostThreadMessage(g_threadID, DESKFLOW_MSG_TOUCH, x, y); - if (g_isPrimary) { - LOG((CLOG_DEBUG "hook: eating touch event (relay mode)")); + + // Filter out warp-echo: the server warps the cursor to center after + // a switch, which generates a synthetic mouse event at that position. + // Without this filter, the echo triggers an immediate switch-back. + SInt32 dxc = x - g_xCenter; + SInt32 dyc = y - g_yCenter; + if (dxc >= -1 && dxc <= 1 && dyc >= -1 && dyc <= 1) { + LOG((CLOG_DEBUG2 "hook: ignoring touch at cursor center %d,%d", x, y)); return 1; } + + LOG((CLOG_DEBUG "hook: touch at %d,%d posting DESKFLOW_MSG_TOUCH", x, y)); + PostThreadMessage(g_threadID, DESKFLOW_MSG_TOUCH, x, y); + // let the touch event pass through to the app so the click registers } } diff --git a/src/lib/platform/MSWindowsHook.h b/src/lib/platform/MSWindowsHook.h index ff1bc5c51..7e19bb6bd 100644 --- a/src/lib/platform/MSWindowsHook.h +++ b/src/lib/platform/MSWindowsHook.h @@ -55,4 +55,6 @@ class MSWindowsHook void setIsPrimary(bool primary); void setIsOnScreen(bool onScreen); + + void setCursorCenter(SInt32 x, SInt32 y); }; diff --git a/src/lib/platform/MSWindowsScreen.cpp b/src/lib/platform/MSWindowsScreen.cpp index 84fd41ea7..42db7d0c8 100644 --- a/src/lib/platform/MSWindowsScreen.cpp +++ b/src/lib/platform/MSWindowsScreen.cpp @@ -1640,6 +1640,8 @@ void MSWindowsScreen::updateScreenShape() // tell the desks m_desks->setShape(m_x, m_y, m_w, m_h, m_xCenter, m_yCenter, m_multimon); + + m_hook.setCursorCenter(m_xCenter, m_yCenter); } void MSWindowsScreen::handleFixes(const Event &, void *) diff --git a/src/lib/server/Server.cpp b/src/lib/server/Server.cpp index a7d51189f..03faa7453 100644 --- a/src/lib/server/Server.cpp +++ b/src/lib/server/Server.cpp @@ -726,6 +726,17 @@ BaseClientProxy *Server::mapToNeighbor(BaseClientProxy *src, EDirection srcSide, // move inwards because that side can't provoke a jump. avoidJumpZone(dst, srcSide, x, y); + // Clamp entry coordinates 1px inside the destination screen. + // avoidJumpZone only adjusts the crossing axis; the parallel axis can + // land exactly on the edge, which the cursor-warp logic treats as still + // being on the source side, bouncing the cursor back immediately. + SInt32 cx, cy, cw, ch; + dst->getShape(cx, cy, cw, ch); + if (x < cx + 1) x = cx + 1; + if (x > cx + cw - 2) x = cx + cw - 2; + if (y < cy + 1) y = cy + 1; + if (y > cy + ch - 2) y = cy + ch - 2; + return dst; } From 88469804b5c150cdd8517e076aaa09f431563a1c Mon Sep 17 00:00:00 2001 From: Stefan Verleysen Date: Tue, 24 Mar 2026 06:55:16 -0400 Subject: [PATCH 2/2] fix: replay touch click on Win10 after screen switch Win10 doesn't deliver WM_POINTERDOWN to the desk window, so the LL hook path never set up the pending click for deskEnter to replay. --- src/lib/platform/MSWindowsDesks.cpp | 9 +++++++++ src/lib/platform/MSWindowsDesks.h | 2 ++ src/lib/platform/MSWindowsScreen.cpp | 1 + 3 files changed, 12 insertions(+) diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index ab2ef6798..f62948767 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -332,6 +332,15 @@ void MSWindowsDesks::fakeTouchClick(SInt32 x, SInt32 y) const sendMessage(DESKFLOW_MSG_FAKE_TOUCH, static_cast(x), static_cast(y)); } +void MSWindowsDesks::setPendingTouchClick(SInt32 x, SInt32 y) +{ + m_pendingTouchX = x; + m_pendingTouchY = y; + m_pendingTouchUp = true; + m_touchLifted = true; + LOG((CLOG_DEBUG "touch: LL hook path, pending click at %d,%d for deskEnter replay", x, y)); +} + void MSWindowsDesks::fakeMouseMove(SInt32 x, SInt32 y) const { sendMessage(DESKFLOW_MSG_FAKE_MOVE, static_cast(x), static_cast(y)); diff --git a/src/lib/platform/MSWindowsDesks.h b/src/lib/platform/MSWindowsDesks.h index b042c0f21..fc55cbe45 100644 --- a/src/lib/platform/MSWindowsDesks.h +++ b/src/lib/platform/MSWindowsDesks.h @@ -172,6 +172,8 @@ class MSWindowsDesks */ void fakeMouseButton(ButtonID id, bool press); + void setPendingTouchClick(SInt32 x, SInt32 y); + //! Fake mouse move /*! Synthesize a mouse move to the absolute coordinates \c x,y. diff --git a/src/lib/platform/MSWindowsScreen.cpp b/src/lib/platform/MSWindowsScreen.cpp index 42db7d0c8..aaf15280c 100644 --- a/src/lib/platform/MSWindowsScreen.cpp +++ b/src/lib/platform/MSWindowsScreen.cpp @@ -996,6 +996,7 @@ bool MSWindowsScreen::onPreDispatch(HWND hwnd, UINT message, WPARAM wParam, LPAR MotionInfo::alloc(x, y)); } else { LOG((CLOG_INFO "hook: touch requesting grab input at %d,%d", x, y)); + m_desks->setPendingTouchClick(x, y); sendEvent(m_events->forIScreen().grabInput(), MotionInfo::alloc(x, y)); }