Skip to content

fix: bind a drag when its contact begins, not on the tap edge - #55

Open
winst0niuss wants to merge 2 commits into
Free-Ink:mainfrom
winst0niuss:fix/slider-drag-grab
Open

fix: bind a drag when its contact begins, not on the tap edge#55
winst0niuss wants to merge 2 commits into
Free-Ink:mainfrom
winst0niuss:fix/slider-drag-grab

Conversation

@winst0niuss

@winst0niuss winst0niuss commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

The problem

On a touch board, dragging a slider only works if the finger pauses before it starts moving. A drag that begins immediately — the natural gesture — does nothing at all: the handle stays put, and nothing happens on release either. Try again, dwell for a moment first, and it drags normally.

Reported against CrossPoint's frontlight panel (brightness and warmth sliders), but nothing there is specific to that screen — it is the shared routing.

Root cause

InteractionTable::routeAgainst() binds a drag to its element only on the press edge:

if (input.touchPressed) {
  active_ = findTouch(slot, input.touchX, input.touchY, InputTouch);
}
if (input.touchHeld && active_ >= 0 && ...) {   // needs active_
  dragged.dragPermille = dragPermilleFor(held.rect, input.touchX);
}

If active_ is never set, every held frame is dropped silently.

Consumers derive touchPressed from a contact that has already read as a tap. In CrossPoint that is MappedInputManager::wasScreenTouchDown(): it needs InputManager::isTouchTapCandidate() — false once the contact passes the 28 px TOUCH_TAP_SLOP_PX — and a hold of at least 90 ms. A finger that starts moving as it lands fails both halves, so the press edge never arrives and the drag is never bound. On release, wasTouchTap() fails too (slop exceeded), so the snapshot carries no coordinates and even the tap-to-position path is skipped.

The fix

Bind on the frame the contact begins as well, hit-testing where it landed rather than where it has since travelled to.

The landing point matters: binding against the live position would let a contact that started somewhere else grab a slider just by passing over it, which is a worse bug than the one being fixed. contactHeld_ mirrors the previous routed frame's contact so the opening frame is recognisable, and nothing rebinds until the contact ends.

findDrag() requires the InputDrag bit strictly, unlike findTouch(), which also matches anything accepting InputTouch. Buttons, tiles and list rows are therefore never bound and keep their press-then-release semantics unchanged.

One case this does not cover: a screen appearing while a contact is already down would see its first held frame mid-gesture. It does not arise today — swipes are recognised on release (InputManager::wasSwipe() requires touchReleasedEvent), so a gesture-opened screen never inherits a live contact. Carrying the real down point in InputSnapshot would close it off for good, at the cost of a snapshot field every consumer has to fill.

Exercised in the desktop simulator on an X4 Pro profile, not yet on physical hardware — contact timing there differs, so a check on a real device would be worth having before merge.

@winst0niuss
winst0niuss force-pushed the fix/slider-drag-grab branch from 8f53a4d to 3bcc6d3 Compare August 24, 2026 10:22
@winst0niuss winst0niuss changed the title fix: let a slider take a drag that starts moving straight away fix: bind a drag when its contact begins, not on the tap edge Aug 24, 2026
A drag is only bound to its element on InputSnapshot::touchPressed, so a
slider that never sees that edge ignores every held frame that follows:
the handle does not move at all, and the release carries no coordinates
either, so the gesture does nothing.

Consumers gate that edge on the contact reading as a tap first — in
CrossPoint, MappedInputManager::wasScreenTouchDown() requires the contact
to stay within InputManager's 28 px tap slop for 90 ms. A finger that
starts moving as soon as it lands never satisfies it, so a natural quick
drag on a slider is dropped, while a slower one that dwells first works.
That is the reported "the handle only drags on the second try".

Bind on the frame the contact begins as well, hit-testing where it landed
rather than where it has since travelled to: binding against the live
position would let a contact that started elsewhere grab a slider merely
by passing over it. findDrag() requires the InputDrag bit strictly
(unlike findTouch(), which falls back to InputTouch), so buttons, tiles
and list rows are never bound and keep their press-then-release
semantics.
@winst0niuss
winst0niuss force-pushed the fix/slider-drag-grab branch from 3bcc6d3 to 35b37d5 Compare August 24, 2026 10:24
@itsthisjustin

Copy link
Copy Markdown
Contributor

Yep I noticed this too. Ready to merge on this?

@winst0niuss

Copy link
Copy Markdown
Contributor Author

Yes, it's ready

findDrag duplicated findTouch's loop to skip its InputTouch fallback.
Excluding InputDrag from that fallback the way InputLongPress already
is makes the two identical, so the copy goes.

Binding on the contact edge no longer waits for active_ to be clear:
a fresh contact must not inherit what the previous one bound, which
is what the guard allowed when a contact ended without a release
edge. The block moves above touchPressed so an adapter reporting both
on one frame still keeps its pressed-element highlight.

testDragRouting is the suite's first InputDrag coverage. Verified by
mutation: restoring the fallback, the guard, or hit-testing the live
position each fails it.
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