Skip to content

feat: Implement touch-to-switch (without timing hacks)#162

Closed
nbolton wants to merge 9 commits intomasterfrom
feat/touch-activates-screen
Closed

feat: Implement touch-to-switch (without timing hacks)#162
nbolton wants to merge 9 commits intomasterfrom
feat/touch-activates-screen

Conversation

@nbolton
Copy link
Member

@nbolton nbolton commented Mar 3, 2026

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 new ClientProxy1_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.

Comment on lines +95 to +100
bool MSWindowsTouchInjector::waitForForeground(HWND target, DWORD timeoutMs)
{
// set up thread-local callback state
s_foregroundTarget = target;
s_foregroundArrived = false;

Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
#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
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
#define DESKFLOW_MSG_INPUT_LAST DESKFLOW_MSG_PRE_WARP
#define DESKFLOW_MSG_INPUT_LAST DESKFLOW_MSG_TOUCH

Copilot uses AI. Check for mistakes.
Comment on lines +791 to +797
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;
}

Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +1381 to +1385
m_primaryClient->fakeTouchClick(x, y);

m_touchSwitchCooldown.reset();
LOG((CLOG_DEBUG1 "touch switch cooldown started"));
}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +983 to +988
// 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;
}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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;

Copilot uses AI. Check for mistakes.
@nbolton nbolton closed this Mar 4, 2026
@nbolton nbolton deleted the feat/touch-activates-screen branch March 4, 2026 13:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants