Skip to content

Commit 23e316a

Browse files
FIX: Pen touch input triggers UI/Click action two times (ISXB-1456) (#2201)
1 parent a447999 commit 23e316a

File tree

3 files changed

+125
-32
lines changed

3 files changed

+125
-32
lines changed

Assets/Tests/InputSystem/Plugins/UITests.cs

Lines changed: 114 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1460,33 +1460,132 @@ public IEnumerator UI_CanDriveUIFromMultiplePointers(UIPointerBehavior pointerBe
14601460
scene.leftChildReceiver.events.Clear();
14611461
scene.rightChildReceiver.events.Clear();
14621462

1463-
// Test if creating Pointer events from different devices at the same time results in only one event
1464-
BeginTouch(0, firstPosition, screen: touch1, queueEventOnly: true);
1463+
// End previous touches that started so that we can do a cleanup from the last test.
1464+
EndTouch(1, secondPosition, screen: touch1);
1465+
yield return null;
1466+
EndTouch(1, firstPosition, screen: touch2);
1467+
yield return null;
1468+
// Set a mouse position without any clicks to "emulate" a real movement before a button press.
1469+
Set(mouse1.position, secondPosition + new Vector2(-10, 0));
1470+
yield return null;
1471+
1472+
scene.leftChildReceiver.events.Clear();
1473+
scene.rightChildReceiver.events.Clear();
1474+
1475+
// Test a press and release from both a Mouse and Touchscreen at the same time
1476+
// This is to simulate some platforms that always send Mouse/Pen and Touches (e.g. Android).
1477+
// Also, this mostly assets the expected behavior for the options SingleMouseOrPenButMultiTouchAndTrack.
1478+
var touchId = 2;
1479+
BeginTouch(touchId, secondPosition, screen: touch1, queueEventOnly: true);
1480+
Set(mouse1.position, secondPosition, queueEventOnly: true);
14651481
Press(mouse1.leftButton);
1482+
14661483
yield return null;
1467-
EndTouch(0, firstPosition, screen: touch1, queueEventOnly: true);
1484+
1485+
EndTouch(touchId, secondPosition, screen: touch1, queueEventOnly: true);
14681486
Release(mouse1.leftButton);
14691487
yield return null;
14701488

1489+
Func<UICallbackReceiver.Event, bool> eventDeviceCondition = null;
1490+
var expectedCount = 0;
14711491
switch (pointerBehavior)
14721492
{
1493+
case UIPointerBehavior.SingleMouseOrPenButMultiTouchAndTrack:
1494+
// Expects only mouse events for PointerClick, PointerDown, and PointerUp
1495+
eventDeviceCondition = (e) => e.pointerData.device == mouse1;
1496+
expectedCount = 1;
1497+
// Make sure that the touch does not generate a UI events.
1498+
Assert.That(scene.rightChildReceiver.events, Has.None.Matches((UICallbackReceiver.Event e) =>
1499+
e.pointerData != null && e.pointerData.device == touch1));
1500+
break;
1501+
14731502
case UIPointerBehavior.SingleUnifiedPointer:
1474-
//// Getting "Drop" event even if using only one type of input device for Press/Release.
1475-
//// E.g. the following test would also produce only a Drop event:
1476-
//// Press(mouse1.leftButton);
1477-
//// yield return null;
1478-
//// Release(mouse1.leftButton);
1479-
//// yield return null;
1503+
// Expects only single UI events with touch source since they are the first events in the queue
1504+
eventDeviceCondition = (e) => e.pointerData.device == touch1;
1505+
expectedCount = 1;
14801506
break;
1481-
case UIPointerBehavior.SingleMouseOrPenButMultiTouchAndTrack:
1507+
14821508
case UIPointerBehavior.AllPointersAsIs:
1483-
// Single pointer click on the left object
1484-
Assert.That(scene.leftChildReceiver.events,
1485-
Has.Exactly(1).With.Property("type").EqualTo(EventType.PointerClick).And
1486-
.Matches((UICallbackReceiver.Event e) => e.pointerData.device == mouse1).And
1487-
.Matches((UICallbackReceiver.Event e) => e.pointerData.position == firstPosition));
1509+
// Expects both pointer devices to generate PointerClick, PointerDown, and PointerUp events
1510+
eventDeviceCondition = (e) => e.pointerData.device == mouse1 || e.pointerData.device == touch1;
1511+
expectedCount = 2;
14881512
break;
1513+
1514+
default:
1515+
throw new ArgumentOutOfRangeException(nameof(pointerBehavior), pointerBehavior, null);
14891516
}
1517+
1518+
Assert.That(scene.rightChildReceiver.events,
1519+
Has.Exactly(expectedCount).With.Property("type").EqualTo(EventType.PointerClick).And
1520+
.Matches((UICallbackReceiver.Event e) => eventDeviceCondition(e)).And
1521+
.Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition));
1522+
Assert.That(scene.rightChildReceiver.events,
1523+
Has.Exactly(expectedCount).With.Property("type").EqualTo(EventType.PointerDown).And
1524+
.Matches((UICallbackReceiver.Event e) => eventDeviceCondition(e)).And
1525+
.Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition));
1526+
Assert.That(scene.rightChildReceiver.events,
1527+
Has.Exactly(expectedCount).With.Property("type").EqualTo(EventType.PointerUp).And
1528+
.Matches((UICallbackReceiver.Event e) => eventDeviceCondition(e)).And
1529+
.Matches((UICallbackReceiver.Event e) => e.pointerData.position == secondPosition));
1530+
}
1531+
1532+
[UnityTest]
1533+
[Category("UI")]
1534+
[Description("Tests that disabling the UI module during a Button click event works correctly with touch pointers." +
1535+
"ISXB-687")]
1536+
public IEnumerator UI_DisablingEventSystemOnClickEventWorksWithTouchPointers()
1537+
{
1538+
var touch = InputSystem.AddDevice<Touchscreen>();
1539+
var scene = CreateTestUI();
1540+
1541+
var actions = ScriptableObject.CreateInstance<InputActionAsset>();
1542+
var uiActions = actions.AddActionMap("UI");
1543+
var pointAction = uiActions.AddAction("point", type: InputActionType.PassThrough);
1544+
var clickAction = uiActions.AddAction("click", type: InputActionType.PassThrough);
1545+
1546+
pointAction.AddBinding("<Touchscreen>/touch*/position");
1547+
clickAction.AddBinding("<Touchscreen>/touch*/press");
1548+
1549+
pointAction.Enable();
1550+
clickAction.Enable();
1551+
1552+
scene.uiModule.point = InputActionReference.Create(pointAction);
1553+
scene.uiModule.pointerBehavior = UIPointerBehavior.SingleMouseOrPenButMultiTouchAndTrack;
1554+
scene.uiModule.leftClick = InputActionReference.Create(clickAction);
1555+
1556+
// Turn left object into a button.
1557+
var button = scene.leftGameObject.AddComponent<MyButton>();
1558+
var clicked = false;
1559+
1560+
// Add a listener to the button to disable the UI module when clicked.
1561+
// This calls InputSystemUIInputModule.OnDisable() which will reset the pointer data during
1562+
// InputSystemUIInputModule.Process() and ProcessPointer(). It will allow us to test that removing
1563+
// a pointer once the UI module is disabled (all pointers are removed) works correctly.
1564+
button.onClick.AddListener(() =>
1565+
{
1566+
clicked = true;
1567+
scene.uiModule.enabled = false; // Disable the UI module to test pointer reset.
1568+
});
1569+
1570+
yield return null;
1571+
1572+
var firstPosition = scene.From640x480ToScreen(100, 100);
1573+
1574+
// This will allocate a pointer for the touch and set the first touch position and press
1575+
BeginTouch(1, firstPosition, screen: touch);
1576+
yield return null;
1577+
1578+
Assert.That(clicked, Is.False, "Button was clicked when it should not have been yet.");
1579+
Assert.That(scene.uiModule.m_PointerStates.length, Is.EqualTo(1),
1580+
"A pointer states was not allocated for the touch pointer.");
1581+
1582+
// Release the touch to make sure we have a Click event that calls the button listener.
1583+
EndTouch(1, firstPosition, screen: touch);
1584+
yield return null;
1585+
1586+
Assert.That(clicked, Is.True, "Button was not clicked when it should have been.");
1587+
Assert.That(scene.uiModule.m_PointerStates.length, Is.EqualTo(0),
1588+
"Pointer states were not cleared when the UI module was disabled after a click event.");
14901589
}
14911590

14921591
[UnityTest]

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ however, it has to be formatted properly to pass verification tests.
1010

1111
## [Unreleased] - yyyy-mm-dd
1212

13+
### Fixed
14+
- Fixed an issue where using Pen devices on Android tablets would result in double clicks for UI interactions. [ISXB-1456](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1456)
15+
1316

1417

1518
## [1.14.1] - 2025-07-10

Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2070,15 +2070,12 @@ private bool SendPointerExitEventsAndRemovePointer(int index)
20702070

20712071
private bool RemovePointerAtIndex(int index)
20722072
{
2073-
Debug.Assert(m_PointerStates[index].eventData.pointerEnter == null, "Pointer should have exited all objects before being removed");
2074-
2075-
// We don't want to release touch pointers on the same frame they are released (unpressed). They get cleaned up one frame later in Process()
2076-
ref var state = ref GetPointerStateForIndex(index);
2077-
if (state.pointerType == UIPointerType.Touch && (state.leftButton.isPressed || state.leftButton.wasReleasedThisFrame))
2078-
{
2079-
// The pointer was not removed
2073+
// Pointers might have been reset before (e.g. when calling OnDisable) which would make m_PointerStates
2074+
// empty (ISXB-687).
2075+
if (m_PointerStates.length == 0)
20802076
return false;
2081-
}
2077+
2078+
Debug.Assert(m_PointerStates[index].eventData.pointerEnter == null, "Pointer should have exited all objects before being removed");
20822079

20832080
// Retain event data so that we can reuse the event the next time we allocate a PointerModel record.
20842081
var eventData = m_PointerStates[index].eventData;
@@ -2350,13 +2347,7 @@ private void FilterPointerStatesByType()
23502347
// We have input on a mouse or pen. Kill all touch and tracked pointers we may have.
23512348
for (var i = 0; i < m_PointerStates.length; ++i)
23522349
{
2353-
ref var state = ref GetPointerStateForIndex(i);
2354-
// Touch pointers need to get forced to no longer be pressed otherwise they will not get released in subsequent frames.
2355-
if (m_PointerStates[i].pointerType == UIPointerType.Touch)
2356-
{
2357-
state.leftButton.isPressed = false;
2358-
}
2359-
if (m_PointerStates[i].pointerType != UIPointerType.MouseOrPen && m_PointerStates[i].pointerType != UIPointerType.Touch || (m_PointerStates[i].pointerType == UIPointerType.Touch && !state.leftButton.isPressed && !state.leftButton.wasReleasedThisFrame))
2350+
if (m_PointerStates[i].pointerType != UIPointerType.MouseOrPen)
23602351
{
23612352
if (SendPointerExitEventsAndRemovePointer(i))
23622353
--i;
@@ -2452,8 +2443,8 @@ public override void Process()
24522443
// stays true for the touch in the frame of release (see UI_TouchPointersAreKeptForOneFrameAfterRelease).
24532444
if (state.pointerType == UIPointerType.Touch && !state.leftButton.isPressed && !state.leftButton.wasReleasedThisFrame)
24542445
{
2455-
RemovePointerAtIndex(i);
2456-
--i;
2446+
if (RemovePointerAtIndex(i))
2447+
--i;
24572448
continue;
24582449
}
24592450

0 commit comments

Comments
 (0)