Skip to content

Fix taps not producing clicks on noisy digitizers (#13) - #28

Draft
sapkra wants to merge 2 commits into
shueber:mainfrom
sapkra:fix/tap-slop-click-detection
Draft

Fix taps not producing clicks on noisy digitizers (#13)#28
sapkra wants to merge 2 commits into
shueber:mainfrom
sapkra:fix/tap-slop-click-detection

Conversation

@sapkra

@sapkra sapkra commented Aug 4, 2026

Copy link
Copy Markdown

Fixes #13.

The bug

A touch only produces a click on lift-off if cursorTouchQualifiedForTap is still set when
the finger leaves the glass. That flag was cleared the moment two consecutive HID reports
differed by more than 0.1 mm:

CGFloat digitizerRelDistance = sqrt(pow(touch.location.x - touch.previousLocation.x, 2) + ...);
CGFloat screenSize = [self touchscreenForLocationID:locationID].nativePhysicalSize.width;
BOOL isStationary = (digitizerRelDistance * screenSize) < 0.1;

0.1 mm is below the reporting resolution of many digitizers, and well below the shift of the
reported contact centroid as a finger flattens onto the glass and lifts off again. On panels
that cross that line, every tap was read as the start of a drag: the cursor moved to the
touch point and no click was ever generated. Hold-and-drag could never arm either, since it
also required the tap flag.

This matches the reports in the issue — including that "On Finger Drag → Point and Click"
works around it, because that mode clicks on NSTouchPhaseEnded regardless of the flag (and
costs you one-finger scrolling in exchange).

Whether the threshold trips is a property of the panel, which is why the bug is
screen-specific rather than universal.

The fix

Measure the tap and hold decisions against an anchor point rather than against the
previous report:

  • A touch stays a tap until the finger leaves a slop radius around where it landed,
    configurable as Tap Zone (default 2.5 mm).
  • The hold clock runs while the finger stays within 1 mm of its anchor and restarts when it
    wanders off, so a slow deliberate drag still never turns into a hold. It is now evaluated
    on every report instead of only stationary ones, which a flickering phase used to swallow.
  • Scroll and drag no longer start until the tap slop is exceeded, so digitizer noise can't
    leak a few pixels of stray scroll into a tap.

The per-report threshold stays, but only to classify the touch phase, where a small value
keeps fine slow scrolling responsive. Nothing consequential hangs off it anymore.

Also fixed

Three things in the same code paths, each with its own commit message detail:

  • Distances were anisotropic. Relative coordinates were scaled by the panel width on
    both axes, so every vertical mm threshold was off by the aspect ratio.
    -[TUCScreen millimetreDistanceBetweenRelativePoint:and:] scales each axis by its own
    physical extent.
  • Zero EDID physical size. Panels reporting no physical size turned every mm threshold
    into 0 or infinity — which would have made the tap slop infinite and disabled scrolling
    outright. -[TUCScreen effectivePhysicalSize] falls back to an assumed ~100 dpi.
  • "$errorResistance" key typo meant Error Resistance never persisted.
  • Dropped a dead branch that re-posted identifiedMultitouchGesture after
    stopCurrentGesture had already cleared it. stopMagnifying already posts the
    terminating magnify, so nothing is lost.

Second commit: quick taps far apart registering as a double click

Found while tracing the tap path. The distance guard in the click-sequence counter compared
the two signed axis deltas and required both to exceed the tolerance:

else if ((aLocation.x - self.locationOfLastClick.x) > self.doubleClickTolerance
         && (aLocation.y - self.locationOfLastClick.y) > self.doubleClickTolerance) {

So a new sequence only started when the second tap landed down and to the right of the
first. In every other direction the guard could not fire and the tap inherited click state 2
— meaning any two quick taps anywhere on a large screen became a double click.

Now compared as a radius, which is what the "Double Click Zone" setting already describes.

The counter is shared with the start of a drag, which deliberately inherits an elevated click
state so a drag right after a tap selects by word. That still works — the drag begins inside
the tap slop, well within the zone — while a drag starting far from the last tap now
correctly begins a plain drag instead of a word selection.

Since this is the first release where the tolerance actually binds, the range is adjusted to
match: the slider no longer offers 0 mm (unsatisfiable; a stored 0 from when the setting was
inert is lifted to 1 on load), and the ceiling goes 8 → 16 mm. Until now the guard was
effectively unbounded, so nothing has ever tested how far apart a deliberate double tap lands
on a wall-sized panel — the headroom keeps those devices working. The default stays 8.

Compatibility

This is a universal driver, so the intent throughout is that no working configuration
changes behavior:

Gesture Behavior
Plain tap Exactly one click (was: none, on affected panels)
Slow tap held >100 ms One click, no drag
Hold then drag Mouse-down once past slop, mouse-up on lift, no trailing click
Scroll Starts after 2.5 mm instead of immediately; no jump when the threshold is crossed
Move Cursor mode Unchanged — TouchDown already places the cursor before the dead zone
Point and Click mode Unchanged, still exactly one click
Pinch / secondary click Untouched — both sit before the new gate

Anyone whose screen worked before had a digitizer quiet enough that the old 0.1 mm check was
never tripping, so the new slop radius doesn't change what they see.

Testing

Builds clean with no new warnings. I don't have a touchscreen to verify against yet (that's why its a draft right now, will order one soon), so the
behavior above is traced through the code rather than measured — worth a check on real
hardware before merging, particularly the double-click zone, which is the easiest to feel by
hand. The debug overlay will show whether taps now stay inside the slop radius.

Two notes on things I deliberately left out, in case you'd rather they were in scope

  • mouseDown and mouseUp firing together on release (AlexvanGiersbergen's comment on the issue). Real, but separate — performClickAt: posts down+up back to back by design, and it isn't what blocks the click here. Press-and-hold UI would need a different event model.
  • The cursorClickCount == 4 wraparound and the fact that dragCursorTo: advances the counter without updating the anchors. Both look intentional; I left them alone.

sapkra and others added 2 commits August 4, 2026 02:09
A touch only produced a click on lift-off if `cursorTouchQualifiedForTap`
was still set, and that flag was cleared the moment two consecutive HID
reports differed by more than 0.1 mm. That is below the resolution of many
digitizers and well below the shift of the reported contact centroid as a
finger flattens onto the glass, so on affected panels every tap was read as
the start of a drag: the cursor moved to the touch point and no click was
ever generated. Hold-and-drag could never arm for the same reason, since it
also required the tap flag.

Measure both decisions against an anchor instead of the previous report:

- A touch stays a tap until the finger leaves a slop radius around where it
  landed, configurable as "Tap Zone" (default 2.5 mm).
- The hold clock runs while the finger stays within 1 mm of its anchor and
  restarts when it wanders off, so a slow deliberate drag still never turns
  into a hold. It is evaluated on every report rather than only on
  stationary ones, which a flickering phase used to swallow.
- Scroll and drag no longer start until the tap slop has been exceeded, so
  digitizer noise can no longer leak a few pixels of scroll into a tap.

The per-report threshold is kept, but only to classify the touch phase,
where a small value keeps fine slow scrolling responsive.

Also in the same paths:

- Distances were computed from relative coordinates scaled by the panel
  width on both axes, which made every vertical threshold wrong on
  non-square panels. `-[TUCScreen millimetreDistanceBetweenRelativePoint:and:]`
  now scales each axis by its own physical extent.
- Panels reporting a zero EDID physical size turned every mm threshold into
  0 or infinity. `-[TUCScreen effectivePhysicalSize]` falls back to an
  assumed density.
- Dropped a dead branch that re-posted `identifiedMultitouchGesture` after
  `stopCurrentGesture` had already cleared it.
- Fixed the "$errorResistance" key typo that stopped Error Resistance from
  ever persisting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The distance guard in the click-sequence counter compared the two signed
axis deltas against the tolerance and required *both* to exceed it. Two
quick taps therefore only started a new sequence when the second landed
down and to the right of the first; in every other direction the guard
could not fire, and the tap inherited click state 2. On a large touchscreen,
where consecutive taps are naturally far apart, that turned any pair of
quick taps anywhere on the glass into a double click.

Compare the distance as a radius instead, which is what the "Double Click
Zone" setting already describes.

The counter is shared with the start of a drag, which deliberately inherits
an elevated click state so that a drag right after a tap selects by word.
That still works — the drag begins inside the tap slop, well within the
zone — while a drag starting far from the last tap now correctly begins a
plain drag instead of a word selection.

This is the first release in which the tolerance actually binds, so the
value range is adjusted to match:

- The slider no longer offers 0 mm, which would mean two taps can never be
  close enough to double click, and a stored 0 from when the setting was
  inert is lifted to 1 on load.
- Its ceiling goes from 8 to 16 mm. Until now the guard was effectively
  unbounded, so nothing tested how far apart a deliberate double tap lands
  on a wall-sized panel; the headroom keeps those devices working.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sapkra sapkra changed the title fix: tap slop click detection Fix taps not producing clicks on noisy digitizers (#13) Aug 4, 2026
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.

Touch up not 'clicking' items

1 participant