Skip to content

FIX: [Port] [UUM-155740] [Input System] Fixed UI Toolkit receiving an extra PointerDownEvent for touch drags - #2502

Draft
benoitalain wants to merge 8 commits into
developfrom
fix/uum-159876-touch-double-pointerdown
Draft

benoitalain wants to merge 8 commits into
developfrom
fix/uum-159876-touch-double-pointerdown

Conversation

@benoitalain

@benoitalain benoitalain commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Port of the Editor fix for UUM-155740. On iOS and Android, a single-finger drag on a UI Toolkit runtime UI dispatched PointerDownEvent twice: Down, Move, Down, Move, Up.

UI/Click is a pass-through action bound to <Touchscreen>/touch*/press, and a touch's press control reads its state from the touch phase. When the phase changes from Began to Moved, the press value stays 1 but the action is performed again, and InputSystemProvider dispatched a second ButtonPressed.

The provider now only dispatches a button event when the pressed state of an input changes:

  • Presses are tracked per binding and control, so each finger is tracked on its own. A composite part is tracked as its composite, since parts report the whole composite's value.
  • A release that happens while the click action is disabled never reaches the callback. Update drops tracked presses that the action no longer evaluates as pressed, or whose device was removed, so they don't swallow the next press.

The PR also includes the fix for UUM-160160, a regression of the first change found in the Editor before it shipped: a UI Toolkit Button pressed with the mouse stayed pressed when the application lost focus while the button was held (Alt+Tab with the mouse button down). Press records now survive focus loss, and when Update drops a press it dispatches the matching ButtonReleased, queued behind any pending callback events so a release never reaches UI before its press. This PR stays in draft until that fix lands in the Editor.

Testing status & QA

Added InputForUITests for touch drags, two fingers, two touchscreens, releases while the UI map is disabled, and click bindings with processors and composites. The UUM-160160 commits add tests for focus loss while a pointer is held, a UI map swap while held, and the order of a release dropped while the UI map was disabled. Ran locally in 6000.0.51f1 batchmode, PlayMode, InputForUITests:

  • With the fix: 71 passed.
  • Without the fix: 6 failed, including TouchDrag_DispatchesOneButtonPressedAndOneButtonReleased and TwoFingerDrag_DispatchesButtonEventsForEachFinger.

I haven't run the full suites locally.

Overall Product Risks

  • Complexity: Low
  • Halo Effect: Medium, every pointer button event from the Input System to UI Toolkit goes through this check, including mouse and pen.

Comments to reviewers

  • The code is the same as the Editor fixes, with two differences. It uses TryGetValue and Remove instead of Dictionary.Remove(key, out value), which the .NET Framework API compatibility level lacks. And Unity.InputSystem.ForUI now allows unsafe code. The provider reads the callback's binding state, and in the Editor the package is built with unsafe code allowed, so the fix didn't need it there.
  • A touch's press control notifies listeners on any phase change even when the press value doesn't change. That could be worth a look on its own, but this PR doesn't change it.

Written by Claude Code (AI agent) on behalf of @benoitalain.

Checklist

Before review:

  • Changelog entry added.
    • Explains the change in Changed, Fixed, Added sections.
    • For API change contains an example snippet and/or migration example.
    • JIRA ticket linked, example (case %%). If it is a private issue, just add the case ID without a link.
    • Jira port for the next release set as "Resolved".
  • Tests added/changed, if applicable.
    • Functional tests Area_CanDoX, Area_CanDoX_EvenIfYIsTheCase, Area_WhenIDoX_AndYHappens_ThisIsTheResult.
    • Performance tests.
    • Integration tests.
  • Docs for new/changed API's.
    • Xmldoc cross references are set correctly.
    • Added explanation how the API works.
    • Usage code examples added.
    • The manual is updated, if needed.

During merge:

  • Commit message for squash-merge is prefixed with one of the list:
    • NEW: ___.
    • FIX: ___.
    • DOCS: ___.
    • CHANGE: ___.
    • RELEASE: 1.1.0-preview.3.

… extra PointerDownEvent for touch drags

A touch's press control reads its state from the touch phase, so the pass-through UI/Click action is
performed again when the phase changes from Began to Moved. InputSystemProvider dispatched a
ButtonPressed on every callback, so a single-finger drag produced two PointerDownEvents.

The provider now tracks pressed inputs per binding and only dispatches a button event when the
pressed state changes. Presses whose release was missed while the click action was disabled are
dropped in Update.

Unity.InputSystem.ForUI now allows unsafe code, to read the binding state of the callback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@u-pr u-pr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Harness Review

Needs changes

The change filters repeated pressed callbacks and adds focused regression coverage, but the retained press state does not fully account for action lifecycle changes.

Reviewed commit e452b5b

🤖 Helpful? 👍/👎

@codecov-github-com

codecov-github-com Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 99.76798% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
.../Runtime/Plugins/InputForUI/InputSystemProvider.cs 98.61% 1 Missing ⚠️
@@             Coverage Diff             @@
##           develop    #2502      +/-   ##
===========================================
+ Coverage    78.96%   79.26%   +0.29%     
===========================================
  Files          767      769       +2     
  Lines       140949   141665     +716     
===========================================
+ Hits        111305   112293     +988     
+ Misses       29644    29372     -272     
Flag Coverage Δ
inputsystem_MacOS_6000.0 5.30% <0.00%> (+<0.01%) ⬆️
inputsystem_MacOS_6000.0_project 77.97% <99.76%> (+0.46%) ⬆️
inputsystem_MacOS_6000.3 5.30% <0.00%> (+<0.01%) ⬆️
inputsystem_MacOS_6000.6 5.29% <0.00%> (+<0.01%) ⬆️
inputsystem_MacOS_6000.6_project 78.03% <99.53%> (+0.46%) ⬆️
inputsystem_Ubuntu_6000.0 5.31% <0.00%> (+<0.01%) ⬆️
inputsystem_Ubuntu_6000.0_project 77.88% <99.76%> (+0.46%) ⬆️
inputsystem_Ubuntu_6000.3 5.31% <0.00%> (+<0.01%) ⬆️
inputsystem_Ubuntu_6000.3_project 77.88% <99.53%> (+0.46%) ⬆️
inputsystem_Ubuntu_6000.6 5.30% <0.00%> (+<0.01%) ⬆️
inputsystem_Ubuntu_6000.6_project 77.93% <99.53%> (+0.46%) ⬆️
inputsystem_Windows_6000.0 5.30% <0.00%> (+<0.01%) ⬆️
inputsystem_Windows_6000.0_project 78.09% <99.76%> (+0.46%) ⬆️
inputsystem_Windows_6000.3 5.30% <0.00%> (+<0.01%) ⬆️
inputsystem_Windows_6000.3_project 78.09% <99.53%> (+0.45%) ⬆️
inputsystem_Windows_6000.6 5.29% <0.00%> (+<0.01%) ⬆️
inputsystem_Windows_6000.6_project 78.15% <99.53%> (+0.45%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...ssets/Tests/InputSystem/Plugins/InputForUITests.cs 98.51% <100.00%> (+1.33%) ⬆️
...tsystem/InputSystem/Runtime/Actions/InputAction.cs 93.15% <100.00%> (+0.82%) ⬆️
...em/InputSystem/Runtime/Actions/InputActionState.cs 92.56% <ø> (+0.39%) ⬆️
.../Runtime/Plugins/InputForUI/InputSystemProvider.cs 90.26% <98.61%> (+1.68%) ⬆️

... and 11 files with indirect coverage changes

ℹ️ Need help interpreting these results?

benoitalain and others added 4 commits November 6, 2026 10:11
A UI Toolkit Button pressed with the mouse stayed pressed when the
application lost focus while the button was held. OnFocusChanged(false)
cleared m_PressedInputs, so the release callback that follows the Input
System's device reset found no record and its ButtonReleased was
suppressed; the Button never received a PointerUp.

The pressed records are now kept across focus loss, and
ForgetReleasedInputs dispatches the ButtonReleased of each press it
forgets instead of dropping it silently. This restores the 6.7
behaviour: the release reaches UI when focus returns, at the pointer's
last known position. The event construction is shared between
OnClickPerformed and the new release path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Disabling and re-enabling the UI action map while a pointer is held must
not dispatch a ButtonReleased: IsStillPressed reads the control's device
state, which a map swap does not change. The release is dispatched only
once the pointer is physically released.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A press reaches OnClickPerformed and is buffered until the provider update.
If the UI map is disabled and the pointer released before that update, the
release only comes from ForgetReleasedInputs, which dispatched it immediately,
ahead of the buffered press. UI Toolkit then ignored the unmatched release and
the press captured the pointer after it was physically released.

The fallback release now goes through the same queue as callback events, so it
follows the press and goes through the same sorting and touch/pen priority
filtering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dictionary.Remove(key, out value) is missing from the .NET Framework API
compatibility level that package users can select.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@benoitalain
benoitalain marked this pull request as draft November 6, 2026 15:12
@benoitalain

Copy link
Copy Markdown
Collaborator Author

/review

Written by Claude Code (AI agent) on behalf of @benoitalain.

@u-pr u-pr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Harness Review

Needs changes

The change adds per-binding press tracking and queued fallback releases, but the release lifecycle has a remaining pointer-filtering regression.

Reviewed commit de0be61

🤖 Helpful? 👍/👎

Touch and pen events make the provider drop mouse events until a click
callback schedules a reset of the seen-event flags. A release dispatched by
ForgetReleasedInputs or a focus loss skipped that step, so after a touch drag
ended while the UI map was disabled, mouse events stayed filtered until the
next touch click.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@benoitalain

Copy link
Copy Markdown
Collaborator Author

/review

Written by Claude Code (AI agent) on behalf of @benoitalain.

@u-pr u-pr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Harness Review

Needs changes

The change adds transition-based press filtering and fallback release handling, with broad regression coverage, but composite press tracking has a remaining lifecycle defect.

Reviewed commit 01a24a1

🤖 Helpful? 👍/👎

A fallback release used the pointer's last position from a Point callback.
When the pointer moved while the UI map was disabled, the release landed on
the press position and could click the element under the press. The release
now reads the pointer's current position from its device.

A press through a composite was released when the device of its triggering
control was removed, even while the composite stayed pressed through another
device. Composite presses are now checked through any control of their part,
since reading a part evaluates the whole composite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@benoitalain

Copy link
Copy Markdown
Collaborator Author

/review

Written by Claude Code (AI agent) on behalf of @benoitalain.

@u-pr u-pr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Harness Review

Ship it

The change deduplicates pointer button events and synthesizes releases missed by callbacks. I examined press identity, action-state lifetime, pointer positioning, focus handling, and the added regression coverage, with no new findings beyond those previously reported.

Reviewed commit c41fc10

⚠️ Earlier blocking findings with unresolved threads: 4. The rating above does not cover them; resolve each thread after addressing it.

🤖 Helpful? 👍/👎

@benoitalain
benoitalain marked this pull request as ready for review November 9, 2026 15:43
# Conflicts:
#	Packages/com.unity.inputsystem/CHANGELOG.md

@u-pr u-pr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Harness Review

Needs changes

The per-binding press tracking and expanded regression tests address touch-drag duplication, but the synthetic release path needs one further correction.

Reviewed commit c41fc10

🤖 Helpful? 👍/👎


DispatchFromCallback(Event.From(new PointerEvent
// The pointer can move without Point callbacks, such as while the UI map is disabled
if (pressedInput.control.device is Pointer pointer && pointer.added)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1

A modifier composite’s fallback release uses the old mouse position when the mouse moves while the UI map is disabled. Press the mouse first, then Ctrl with ButtonWithOneModifier(modifiersOrder=2), and PressedInput.control records the keyboard. If the mouse then moves and releases with the map disabled, DispatchButtonReleased skips its position refresh because the recorded device is a keyboard. The emitted ButtonReleased therefore carries the press position instead of the release position. Resolve the composite’s pointer control for fallback positioning. Add a disabled-map regression test with the modifier pressed last.

🤖 Helpful? 👍/👎

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Declining. The fallback release exists to reset the pressed state on the UI that saw the press, not to deliver an accurate click. This case needs a click bound through a modifier composite, with the modifier pressed after the pointer button, and the UI map disabled while it is held. The release then lands at the press position, the last position the UI saw, so the pressed state still resets correctly. Resolving the composite's pointer control would add another rule to an already long list of edge cases, for a configuration the default UI actions don't use.

The code matches the trunk and 6000.7 versions of this fix. How these fallback releases should be delivered, including their position, is left for a separate change.

Written by Claude Code (AI agent) on behalf of @benoitalain.

@benoitalain
benoitalain marked this pull request as draft November 9, 2026 20:11
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.

1 participant