feat: Implement touch-to-switch (without timing hacks)#162
feat: Implement touch-to-switch (without timing hacks)#162
Conversation
There was a problem hiding this comment.
Pull request overview
Implements Windows touch-to-switch so that touching an inactive screen requests/forces focus to that screen without relying on fragile timing-based edge switching.
Changes:
- Add new touch-triggered events (
touchActivatedPrimary,grabInput) and wire them through server/client event queues. - Extend protocol to v1.9 with a new
CGRB(“grab input”) message and a newClientProxy1_9. - Add Windows-specific hook detection for touch-originated clicks and desk-thread touch injection (touch or mouse fallback).
Reviewed changes
Copilot reviewed 29 out of 29 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/lib/server/Server.h | Adds handlers + cooldown state for touch-to-switch edge-switch suppression. |
| src/lib/server/Server.cpp | Wires new events, handles touch activation + client grab requests, and blocks edge-switch during cooldown. |
| src/lib/server/PrimaryClient.h | Adds APIs for window activation and touch click injection. |
| src/lib/server/PrimaryClient.cpp | Pass-through implementations to the underlying screen. |
| src/lib/server/Config.cpp | Adds touchActivateScreen config option parsing/serialization. |
| src/lib/server/ClientProxyUnknown.cpp | Instantiates ClientProxy1_9 for protocol minor 9. |
| src/lib/server/ClientProxy1_9.h | Declares v1.9 proxy with grab-input parsing. |
| src/lib/server/ClientProxy1_9.cpp | Parses CGRB and emits ClientProxy.grabInput events. |
| src/lib/platform/dfwhook.h | Adds touch hook message + touch signature constants. |
| src/lib/platform/MSWindowsTouchInjector.h | Declares desk-thread touch/mouse click injector. |
| src/lib/platform/MSWindowsTouchInjector.cpp | Implements activation waiting + touch injection + mouse fallback. |
| src/lib/platform/MSWindowsScreen.h | Adds touch-to-switch overrides + debounce/pending-touch state. |
| src/lib/platform/MSWindowsScreen.cpp | Configures hook, emits touch/grab events, and implements pending-touch activation consumption. |
| src/lib/platform/MSWindowsHook.h | Adds setters to control touch activation + primary/on-screen state. |
| src/lib/platform/MSWindowsHook.cpp | Detects touch-originated clicks (signature) while off-screen and posts touch messages. |
| src/lib/platform/MSWindowsDesks.h | Adds fakeTouchClick entry point to desk thread. |
| src/lib/platform/MSWindowsDesks.cpp | Routes fake-touch messages to the touch injector and adds mouse-move suppression tweak. |
| src/lib/deskflow/protocol_types.h | Bumps protocol minor to 1.9 and declares kMsgCGrabInput. |
| src/lib/deskflow/protocol_types.cpp | Defines kMsgCGrabInput wire format. |
| src/lib/deskflow/option_types.h | Adds kOptionTouchActivateScreen. |
| src/lib/deskflow/Screen.h | Exposes touch/window-activation APIs on Screen. |
| src/lib/deskflow/Screen.cpp | Forwards new APIs to platform screen. |
| src/lib/deskflow/IPlatformScreen.h | Adds default no-op virtuals for touch/window activation + pending-touch storage. |
| src/lib/client/ServerProxy.h | Adds grabInput() client->server API. |
| src/lib/client/ServerProxy.cpp | Sends CGRB protocol message. |
| src/lib/client/Client.h | Adds grab-input event handler declaration. |
| src/lib/client/Client.cpp | Hooks grab-input events, sends CGRB, and performs deferred activation/click on enter. |
| src/lib/base/EventTypes.h | Adds new event types for grab-input and touch-activated-primary. |
| src/lib/base/EventTypes.cpp | Registers the new event types. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| bool MSWindowsTouchInjector::waitForForeground(HWND target, DWORD timeoutMs) | ||
| { | ||
| // set up thread-local callback state | ||
| s_foregroundTarget = target; | ||
| s_foregroundArrived = false; | ||
|
|
There was a problem hiding this comment.
waitForForeground() uses static members (s_foregroundTarget, s_foregroundArrived) as cross-callback state but they’re neither thread_local nor synchronized. SetWinEventHook(..., WINEVENT_OUTOFCONTEXT, ...) may invoke foregroundEventProc on a different thread, so this is a data race and the wait loop may not observe updates. Use proper synchronization (e.g., an event handle or std::atomic), or make the state truly thread-local if that’s the intent.
| #define DESKFLOW_MSG_DEBUG WM_APP + 0x0019 // data, data | ||
| #define DESKFLOW_MSG_TOUCH WM_APP + 0x001A // x; y (touch-originated) | ||
| #define DESKFLOW_MSG_INPUT_FIRST DESKFLOW_MSG_KEY | ||
| #define DESKFLOW_MSG_INPUT_LAST DESKFLOW_MSG_PRE_WARP |
There was a problem hiding this comment.
DESKFLOW_MSG_TOUCH is now a hook-originated input message posted to the same thread queue, but DESKFLOW_MSG_INPUT_LAST still stops at DESKFLOW_MSG_PRE_WARP. MSWindowsScreen::warpCursor() flushes messages in [DESKFLOW_MSG_INPUT_FIRST, DESKFLOW_MSG_INPUT_LAST], so touch messages can remain queued across warps/screen switches and potentially fire later in the wrong state. Consider extending DESKFLOW_MSG_INPUT_LAST to include DESKFLOW_MSG_TOUCH (or flushing up to DESKFLOW_HOOK_LAST_MSG).
| #define DESKFLOW_MSG_INPUT_LAST DESKFLOW_MSG_PRE_WARP | |
| #define DESKFLOW_MSG_INPUT_LAST DESKFLOW_MSG_TOUCH |
| double elapsedTouchCooldown = m_touchSwitchCooldown.getTime(); | ||
| if (elapsedTouchCooldown > 0.0 && elapsedTouchCooldown < kTouchSwitchCooldownTime) { | ||
| LOG((CLOG_DEBUG1 "edge switch blocked by touch cooldown (%.2fs remaining)", | ||
| kTouchSwitchCooldownTime - elapsedTouchCooldown)); | ||
| return false; | ||
| } | ||
|
|
There was a problem hiding this comment.
m_touchSwitchCooldown is a running Stopwatch from server construction time, so during the first ~0.5s after startup getTime() will be < kTouchSwitchCooldownTime and edge switching will be blocked even though no touch switch occurred. Consider gating this check with an explicit “cooldown active” flag, or initialize the stopwatch in a stopped state and only start/reset it when a touch/grab switch happens.
| double elapsedTouchCooldown = m_touchSwitchCooldown.getTime(); | |
| if (elapsedTouchCooldown > 0.0 && elapsedTouchCooldown < kTouchSwitchCooldownTime) { | |
| LOG((CLOG_DEBUG1 "edge switch blocked by touch cooldown (%.2fs remaining)", | |
| kTouchSwitchCooldownTime - elapsedTouchCooldown)); | |
| return false; | |
| } | |
| static double s_lastTouchCooldownTime = -1.0; | |
| static bool s_touchCooldownActive = false; | |
| double elapsedTouchCooldown = m_touchSwitchCooldown.getTime(); | |
| // Detect when the touch cooldown stopwatch has been reset (e.g. on a touch/grab switch). | |
| // Before the first reset after startup, the stopwatch simply runs from construction time, | |
| // so we do not want to treat that as an active cooldown. | |
| if (s_lastTouchCooldownTime >= 0.0 && elapsedTouchCooldown < s_lastTouchCooldownTime) { | |
| // Stopwatch time decreased, so assume it was reset to start a new cooldown window. | |
| s_touchCooldownActive = true; | |
| } | |
| s_lastTouchCooldownTime = elapsedTouchCooldown; | |
| if (s_touchCooldownActive) { | |
| if (elapsedTouchCooldown < kTouchSwitchCooldownTime) { | |
| LOG((CLOG_DEBUG1 "edge switch blocked by touch cooldown (%.2fs remaining)", | |
| kTouchSwitchCooldownTime - elapsedTouchCooldown)); | |
| return false; | |
| } | |
| else { | |
| // Cooldown window has expired; allow edge switches again until the next reset. | |
| s_touchCooldownActive = false; | |
| } | |
| } |
| m_primaryClient->fakeTouchClick(x, y); | ||
|
|
||
| m_touchSwitchCooldown.reset(); | ||
| LOG((CLOG_DEBUG1 "touch switch cooldown started")); | ||
| } |
There was a problem hiding this comment.
If you change m_touchSwitchCooldown to be stopped/disabled until the first touch switch (to avoid blocking edge switches at startup), make sure the cooldown is explicitly started when a touch/grab switch happens (e.g., start+reset) — right now the handlers only call reset(), which won’t start a stopped stopwatch.
| // debounce rapid touch events | ||
| double elapsed = m_touchDebounceTimer.getTime(); | ||
| if (elapsed > 0.0 && elapsed < kTouchDebounceTime) { | ||
| LOG((CLOG_DEBUG1 "touch debounced at %d,%d (%.0fms)", x, y, elapsed * 1000)); | ||
| return true; | ||
| } |
There was a problem hiding this comment.
m_touchDebounceTimer is a default-constructed (running) Stopwatch, so the first touch within ~0.3s of MSWindowsScreen construction will be treated as “debounced” and ignored. Consider initializing the timer in a stopped/disabled state (or using a separate boolean) so the first touch is never dropped.
| // debounce rapid touch events | |
| double elapsed = m_touchDebounceTimer.getTime(); | |
| if (elapsed > 0.0 && elapsed < kTouchDebounceTime) { | |
| LOG((CLOG_DEBUG1 "touch debounced at %d,%d (%.0fms)", x, y, elapsed * 1000)); | |
| return true; | |
| } | |
| // per-instance debounce state: ensure the very first touch for a screen is never dropped | |
| static MSWindowsScreen* s_lastTouchScreen = NULL; | |
| static bool s_touchDebounceArmed = false; | |
| if (s_lastTouchScreen != this) { | |
| // new screen instance; do not debounce the first touch for it | |
| s_lastTouchScreen = this; | |
| s_touchDebounceArmed = false; | |
| } | |
| // debounce rapid touch events (only after debounce has been armed) | |
| double elapsed = m_touchDebounceTimer.getTime(); | |
| if (s_touchDebounceArmed && elapsed > 0.0 && elapsed < kTouchDebounceTime) { | |
| LOG((CLOG_DEBUG1 "touch debounced at %d,%d (%.0fms)", x, y, elapsed * 1000)); | |
| return true; | |
| } | |
| s_touchDebounceArmed = true; |
…ging for touch events
…handling in onPreDispatch
https://symless.atlassian.net/browse/S1-2091