[0.83] fix(fabric): touch-input reliability — primary-button labeling, guaranteed touch END/CANCEL, phantom active-touch self-heal - #16333
Conversation
…uch, guaranteed touch END/CANCEL on missed releases and capture loss, phantom active-touch self-heal
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
…ve-touch entry onPointerPressed already detects a stale m_activeTouches entry for an OS-reused pointer id and erases it so the new press can proceed. But the leaked entry's touchStart may already have reached JS - erasing silently leaves the JS gesture responder convinced that finger is still down, a wedge no later native cleanup can undo once the entry is gone. Dispatch TouchEventType::Cancel for the stale entry before erasing it, matching the other three cleanup sites in this PR.
|
Pushed one more cleanup site: when Reason: the leaked entry's Validated in the same production 0.83.2 new-arch app as the rest of the PR (from-source framework build): an injected-touch soak — real |
There was a problem hiding this comment.
Pull request overview
This PR hardens Fabric/Composition input handling on Windows to make touch input behave reliably like mouse input, specifically by aligning pointer-event button semantics for touch/pen and ensuring active touches can’t be stranded across missed releases/capture loss.
Changes:
- Ensure non-mouse contacts dispatch with W3C primary-button semantics (
button=0⇒buttons=1) so touch presses are recognized by pointer-event–driven press logic. - Prevent leaked/stranded
m_activeTouchesby dispatchingEndeven when hit-test misses, dispatchingCancelon capture loss, and adding “self-heal” cleanup for stale/leftover touches. - Add a change file for a patch release entry.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp | Adjusts touch/pointer dispatch and cleanup behavior to prevent ignored taps and app-wide press “wedges” caused by leaked active touches. |
| change/react-native-windows-fix-fabric-touch-input-reliability.json | Records the patch-level change for release notes/versioning. |
Suppressed comments (1)
vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp:1402
- When tag == -1, getTargetPointerArgs() calls RootComponentView().hitTest(...), and ViewComponentView::hitTest returns -1 without writing localPt. That leaves ptLocal uninitialized, but it is still passed to UpdateActiveTouch() (screen/offset points), causing undefined coordinates on touch End for slide-off/missed-hit releases.
UpdateActiveTouch(activeTouch->second, ptScaled, ptLocal);
DispatchTouchEvent(TouchEventType::End, pointerId, pointerPoint, keyModifiers);
| try { | ||
| auto inner = winrt::get_self<winrt::Microsoft::ReactNative::Composition::Input::implementation::PointerPoint>( | ||
| pointerPoint) | ||
| ->Inner(); | ||
| if (inner && inner.Properties().IsPrimary() && !m_activeTouches.empty()) { | ||
| std::vector<PointerId> leftoverIds; | ||
| leftoverIds.reserve(m_activeTouches.size()); | ||
| for (auto const &entry : m_activeTouches) { | ||
| if (entry.second.touch.identifier != pointerId) { | ||
| leftoverIds.push_back(entry.first); | ||
| } | ||
| } | ||
| for (auto leftoverId : leftoverIds) { | ||
| DispatchTouchEvent(TouchEventType::Cancel, leftoverId, pointerPoint, keyModifiers); | ||
| m_activeTouches.erase(leftoverId); | ||
| } | ||
| } | ||
| } catch (...) { | ||
| } |
| if (!m_activeTouches.empty()) { | ||
| std::vector<PointerId> activeIds; | ||
| activeIds.reserve(m_activeTouches.size()); | ||
| for (auto const &entry : m_activeTouches) { | ||
| activeIds.push_back(entry.first); | ||
| } | ||
| for (auto activeId : activeIds) { | ||
| DispatchTouchEvent(TouchEventType::Cancel, activeId, pointerPoint, keyModifiers); | ||
| m_activeTouches.erase(activeId); | ||
| } |
Fixes #16332.
Problem
On real touch hardware, Fabric/Composition touch input fails in ways mouse input does not (verified by differential testing: synthetic
InjectTouchInputtouch vsmouse_eventon identical controls):Root causes (all in
CompositionEventHandler.cpp)button=-1for touch:onPointerPressedmapsActiveTouch.buttonfromPointerUpdateKind(mouse-only concept); touch falls to the default-1, and the derived W3Cbuttonsbitmask becomes0. The dispatchedpointerdownsays "no button pressed", so pointer-event–driven press logic ignores finger contacts. W3C requires a touch/pen contact to be the primary button.onPointerReleasedreturned beforem_activeTouches.erase(...)when the up hit-tested to no component (tag == -1);onPointerCaptureLostreleased captures but never dispatched touchCancelnor erased entries;onPointerPressed's stale-id guardreturned, silently discarding new presses on a reused pointer id. A leaked entry is included in every subsequentDispatchTouchEvent(the loop iterates all ofm_activeTouches), so the responder treats each new tap as corrupt multi-touch with a phantom finger down — presses animate but never commit.Fix
onPointerPressed: non-mouse contacts that fell through tobutton = -1getbutton = 0(⇒buttons = 1while down) — W3C primary-button semantics for touch/pen.onPointerReleased: dispatch the touchEndand erase the entry even whentag == -1; only the target-specificOnPointerReleasedrequires a valid tag.onPointerCaptureLost: dispatch touchCancelfor all active touches and clear them (proper responder reset), in addition to the existing capture release.onPointerPressed: when a new primary touch contact arrives (IsPrimary()— a genuine second finger is non-primary and unaffected), cancel any leftover active touches; plus a 30s staleness backstop, and the stale-id guard now drops the leaked entry and continues instead of discarding the press.Validation
Compiled into Microsoft.ReactNative from source (
UseExperimentalNuget=false) in a production RNW 0.83.2 new-arch app (Facilitron FIT), x64 Release, on a physical Surface-class touch tablet:Pressable/TouchableOpacitynow fire (previously mouse-only) ✅Caveats for reviewers
0.83-stableto match our production app; happy to re-cut ontomain.Inner().Properties().IsPrimary()via the implementation type; if there's a preferred projection-level accessor, glad to switch.Microsoft Reviewers: Open in CodeFlow