Skip to content

Commit 9d485e6

Browse files
authored
FIX: ISXB-586 inputsystem sendpointerhovertoparent (#1975)
* Fixed ISXB-586: sendPointerHoverToParent for InputSystemUIInputModule. * Update changelog * Adjusted trunk version for the actual version the feature landed in. * Fixed missing sendPointerHoverToParent in Unity < 6000.0.15. * Fixed PointerEventData.fullyExited and reentered version checks. * Cleanup spacing for #if UNITY_ version checks.
1 parent 80e6a0e commit 9d485e6

7 files changed

Lines changed: 275 additions & 23 deletions

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#if UNITY_INPUT_SYSTEM_SENDPOINTERHOVERTOPARENT
2+
using System.Collections;
3+
using NUnit.Framework;
4+
using UnityEngine;
5+
using UnityEngine.EventSystems;
6+
using UnityEngine.InputSystem;
7+
using UnityEngine.InputSystem.UI;
8+
using UnityEngine.TestTools;
9+
using UnityEngine.UI;
10+
11+
internal partial class UITests
12+
{
13+
#pragma warning disable CS0649
14+
public class InputModuleTests : CoreTestsFixture
15+
{
16+
private InputSystemUIInputModule m_InputModule;
17+
private Mouse m_Mouse;
18+
private Image m_Image;
19+
private Image m_NestedImage;
20+
21+
private bool sendPointerHoverToParent
22+
{
23+
set => m_InputModule.sendPointerHoverToParent = value;
24+
}
25+
26+
private Vector2 mousePosition
27+
{
28+
set { Set(m_Mouse.position, value); }
29+
}
30+
31+
public override void Setup()
32+
{
33+
base.Setup();
34+
35+
var canvas = new GameObject("Canvas").AddComponent<Canvas>();
36+
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
37+
canvas.gameObject.AddComponent<GraphicRaycaster>();
38+
39+
m_Image = new GameObject("Image").AddComponent<Image>();
40+
m_Image.gameObject.transform.SetParent(canvas.transform);
41+
RectTransform imageRectTransform = m_Image.GetComponent<RectTransform>();
42+
imageRectTransform.sizeDelta = new Vector2(400f, 400f);
43+
imageRectTransform.localPosition = Vector3.zero;
44+
45+
m_NestedImage = new GameObject("NestedImage").AddComponent<Image>();
46+
m_NestedImage.gameObject.transform.SetParent(m_Image.transform);
47+
RectTransform nestedImageRectTransform = m_NestedImage.GetComponent<RectTransform>();
48+
nestedImageRectTransform.sizeDelta = new Vector2(200f, 200f);
49+
nestedImageRectTransform.localPosition = Vector3.zero;
50+
51+
GameObject go = new GameObject("Event System");
52+
var eventSystem = go.AddComponent<EventSystem>();
53+
eventSystem.pixelDragThreshold = 1;
54+
55+
m_InputModule = go.AddComponent<InputSystemUIInputModule>();
56+
Cursor.lockState = CursorLockMode.None;
57+
58+
m_Mouse = InputSystem.AddDevice<Mouse>();
59+
var actions = ScriptableObject.CreateInstance<InputActionAsset>();
60+
var uiActions = actions.AddActionMap("UI");
61+
var pointAction = uiActions.AddAction("point", type: InputActionType.PassThrough);
62+
pointAction.AddBinding("<Mouse>/position");
63+
pointAction.Enable();
64+
m_InputModule.point = InputActionReference.Create(pointAction);
65+
}
66+
67+
[UnityTest]
68+
public IEnumerator PointerEnterChildShouldNotFullyExit_NotSendPointerEventToParent()
69+
{
70+
sendPointerHoverToParent = false;
71+
PointerExitCallbackCheck callbackCheck = m_Image.gameObject.AddComponent<PointerExitCallbackCheck>();
72+
m_NestedImage.gameObject.AddComponent<PointerExitCallbackCheck>();
73+
var screenMiddle = new Vector2(Screen.width / 2, Screen.height / 2);
74+
75+
mousePosition = screenMiddle - new Vector2(150, 150);
76+
yield return null;
77+
mousePosition = screenMiddle;
78+
yield return null;
79+
Assert.IsTrue(callbackCheck.pointerData.fullyExited == false);
80+
}
81+
82+
[UnityTest]
83+
public IEnumerator PointerEnterChildShouldNotExit_SendPointerEventToParent()
84+
{
85+
sendPointerHoverToParent = true;
86+
PointerExitCallbackCheck callbackCheck = m_Image.gameObject.AddComponent<PointerExitCallbackCheck>();
87+
m_NestedImage.gameObject.AddComponent<PointerExitCallbackCheck>();
88+
var screenMiddle = new Vector2(Screen.width / 2, Screen.height / 2);
89+
90+
mousePosition = screenMiddle - new Vector2(150, 150);
91+
yield return null;
92+
mousePosition = screenMiddle;
93+
yield return null;
94+
Assert.IsTrue(callbackCheck.pointerData == null);
95+
}
96+
97+
[UnityTest]
98+
public IEnumerator PointerEnterChildShouldNotReenter()
99+
{
100+
PointerEnterCallbackCheck callbackCheck =
101+
m_NestedImage.gameObject.AddComponent<PointerEnterCallbackCheck>();
102+
var screenMiddle = new Vector2(Screen.width / 2, Screen.height / 2);
103+
104+
mousePosition = screenMiddle - new Vector2(150, 150);
105+
yield return null;
106+
mousePosition = screenMiddle;
107+
yield return null;
108+
Assert.IsTrue(callbackCheck.pointerData.reentered == false);
109+
}
110+
111+
[UnityTest]
112+
public IEnumerator PointerExitChildShouldReenter_NotSendPointerEventToParent()
113+
{
114+
sendPointerHoverToParent = false;
115+
PointerEnterCallbackCheck callbackCheck =
116+
m_Image.gameObject.AddComponent<PointerEnterCallbackCheck>();
117+
var screenMiddle = new Vector2(Screen.width / 2, Screen.height / 2);
118+
119+
mousePosition = screenMiddle - new Vector2(150, 150);
120+
yield return null;
121+
mousePosition = screenMiddle;
122+
yield return null;
123+
mousePosition = screenMiddle - new Vector2(150, 150);
124+
yield return null;
125+
Assert.IsTrue(callbackCheck.pointerData.reentered == true);
126+
}
127+
128+
[UnityTest]
129+
public IEnumerator PointerExitChildShouldNotSendEnter_SendPointerEventToParent()
130+
{
131+
sendPointerHoverToParent = true;
132+
m_NestedImage.gameObject.AddComponent<PointerEnterCallbackCheck>();
133+
var screenMiddle = new Vector2(Screen.width / 2, Screen.height / 2);
134+
135+
mousePosition = screenMiddle;
136+
yield return null;
137+
PointerEnterCallbackCheck callbackCheck =
138+
m_Image.gameObject.AddComponent<PointerEnterCallbackCheck>();
139+
mousePosition = screenMiddle - new Vector2(150, 150);
140+
yield return null;
141+
Assert.IsTrue(callbackCheck.pointerData == null);
142+
}
143+
144+
[UnityTest]
145+
public IEnumerator PointerExitChildShouldFullyExit()
146+
{
147+
PointerExitCallbackCheck callbackCheck =
148+
m_NestedImage.gameObject.AddComponent<PointerExitCallbackCheck>();
149+
var screenMiddle = new Vector2(Screen.width / 2, Screen.height / 2);
150+
151+
mousePosition = screenMiddle - new Vector2(150, 150);
152+
yield return null;
153+
mousePosition = screenMiddle;
154+
yield return null;
155+
mousePosition = screenMiddle - new Vector2(150, 150);
156+
yield return null;
157+
Assert.IsTrue(callbackCheck.pointerData.fullyExited == true);
158+
}
159+
160+
public class PointerExitCallbackCheck : MonoBehaviour, IPointerExitHandler
161+
{
162+
public PointerEventData pointerData { get; private set; }
163+
164+
public void OnPointerExit(PointerEventData eventData)
165+
{
166+
pointerData = eventData;
167+
}
168+
}
169+
170+
public class PointerEnterCallbackCheck : MonoBehaviour, IPointerEnterHandler
171+
{
172+
public PointerEventData pointerData { get; private set; }
173+
174+
public void OnPointerEnter(PointerEventData eventData)
175+
{
176+
pointerData = eventData;
177+
}
178+
}
179+
}
180+
}
181+
#endif

Assets/Tests/InputSystem/Plugins/UITests.InputModuleTests.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Assets/Tests/InputSystem/Plugins/UITests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
#pragma warning disable CS0649
3939
////TODO: app focus handling
4040

41-
internal class UITests : CoreTestsFixture
41+
internal partial class UITests : CoreTestsFixture
4242
{
4343
private struct TestObjects
4444
{

Assets/Tests/InputSystem/Unity.InputSystem.Tests.asmdef

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@
5050
"name": "Unity",
5151
"expression": "6000.0.11",
5252
"define": "UNITY_INPUT_SYSTEM_INPUT_MODULE_SCROLL_DELTA"
53+
},
54+
{
55+
"name": "Unity",
56+
"expression": "6000.0.15",
57+
"define": "UNITY_INPUT_SYSTEM_SENDPOINTERHOVERTOPARENT"
5358
}
5459
],
5560
"noEngineReferences": false

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,17 @@ however, it has to be formatted properly to pass verification tests.
1717
- Fixed an update loop in the asset editor that occurs when selecting an Action Map that has no Actions.
1818
- Fixed Package compilation when Unity Analytics module is not enabled on 2022.3. [ISXB-996](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-996)
1919
- Fixed 'OnDrop' event not called when 'IPointerDownHandler' is also listened. [ISXB-1014](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1014)
20-
21-
### Added
22-
- Added Hinge Angle sensor support for foldable devices.
23-
- Added InputDeviceMatcher.WithManufacturerContains(string noRegexMatch) API to improve DualShockSupport.Initialize performance (ISX-1411)
24-
- Added an IME Input sample scene.
20+
- Fixed InputSystemUIInputModule calling pointer events on parent objects even when the "Send Pointer Hover To Parent" is off. [ISXB-586](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-586)
2521

2622
### Changed
2723
- Use `ProfilerMarker` instead of `Profiler.BeginSample` and `Profiler.EndSample` when appropriate to enable recording of profiling data.
2824

2925
### Added
26+
- Added Hinge Angle sensor support for foldable devices.
27+
- Added InputDeviceMatcher.WithManufacturerContains(string noRegexMatch) API to improve DualShockSupport.Initialize performance (ISX-1411)
3028
- Added tests for Input Action Editor UI for managing action maps (List, create, rename, delete) (ISX-2087)
3129
- Added automatic loading of custom extensions of InputProcessor, InputInteraction and InputBindingComposite [ISXB-856]](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-856).
30+
- Added an IME Input sample scene.
3231

3332
## [1.10.0] - 2024-07-24
3433

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

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ private void ProcessPointerMovement(ref PointerModel pointer, ExtendedPointerEve
404404

405405
private void ProcessPointerMovement(ExtendedPointerEventData eventData, GameObject currentPointerTarget)
406406
{
407-
#if UNITY_2021_1_OR_NEWER
407+
#if UNITY_2021_1_OR_NEWER
408408
// If the pointer moved, send move events to all UI elements the pointer is
409409
// currently over.
410410
var wasMoved = eventData.IsPointerMoving();
@@ -413,7 +413,7 @@ private void ProcessPointerMovement(ExtendedPointerEventData eventData, GameObje
413413
for (var i = 0; i < eventData.hovered.Count; ++i)
414414
ExecuteEvents.Execute(eventData.hovered[i], eventData, ExecuteEvents.pointerMoveHandler);
415415
}
416-
#endif
416+
#endif
417417

418418
// If we have no target or pointerEnter has been deleted,
419419
// we just send exit events to anything we are tracking
@@ -435,32 +435,78 @@ private void ProcessPointerMovement(ExtendedPointerEventData eventData, GameObje
435435
if (eventData.pointerEnter == currentPointerTarget && currentPointerTarget)
436436
return;
437437

438-
var commonRoot = FindCommonRoot(eventData.pointerEnter, currentPointerTarget)?.transform;
438+
Transform commonRoot = FindCommonRoot(eventData.pointerEnter, currentPointerTarget)?.transform;
439+
Transform pointerParent = ((Component)currentPointerTarget.GetComponentInParent<IPointerExitHandler>())?.transform;
439440

440441
// We walk up the tree until a common root and the last entered and current entered object is found.
441442
// Then send exit and enter events up to, but not including, the common root.
443+
// ** or when !m_SendPointerEnterToParent, stop when meeting a gameobject with an exit event handler
442444
if (eventData.pointerEnter != null)
443445
{
444-
for (var current = eventData.pointerEnter.transform; current != null && current != commonRoot; current = current.parent)
446+
var current = eventData.pointerEnter.transform;
447+
while (current != null)
445448
{
449+
// if we reach the common root break out!
450+
if (sendPointerHoverToParent && current == commonRoot)
451+
break;
452+
453+
// if we reach a PointerExitEvent break out!
454+
if (!sendPointerHoverToParent && current == pointerParent)
455+
break;
456+
457+
#if UNITY_2021_3_OR_NEWER
458+
eventData.fullyExited = current != commonRoot && eventData.pointerEnter != currentPointerTarget;
459+
#endif
446460
ExecuteEvents.Execute(current.gameObject, eventData, ExecuteEvents.pointerExitHandler);
447461
eventData.hovered.Remove(current.gameObject);
462+
463+
if (sendPointerHoverToParent)
464+
current = current.parent;
465+
466+
// if we reach the common root break out!
467+
if (current == commonRoot)
468+
break;
469+
470+
if (!sendPointerHoverToParent)
471+
current = current.parent;
448472
}
449473
}
450474

475+
// now issue the enter call up to but not including the common root
476+
Transform oldPointerEnter = eventData.pointerEnter?.transform;
451477
eventData.pointerEnter = currentPointerTarget;
452478
if (currentPointerTarget != null)
453479
{
454-
for (var current = currentPointerTarget.transform;
455-
current != null && current != commonRoot && !PointerShouldIgnoreTransform(current);
456-
current = current.parent)
480+
Transform current = currentPointerTarget.transform;
481+
while (current != null && !PointerShouldIgnoreTransform(current))
457482
{
483+
#if UNITY_2021_3_OR_NEWER
484+
eventData.reentered = current == commonRoot && current != oldPointerEnter;
485+
// if we are sending the event to parent, they are already in hover mode at that point. No need to bubble up the event.
486+
if (sendPointerHoverToParent && eventData.reentered)
487+
break;
488+
#endif
489+
458490
ExecuteEvents.Execute(current.gameObject, eventData, ExecuteEvents.pointerEnterHandler);
459-
#if UNITY_2021_1_OR_NEWER
491+
#if UNITY_2021_1_OR_NEWER
460492
if (wasMoved)
461493
ExecuteEvents.Execute(current.gameObject, eventData, ExecuteEvents.pointerMoveHandler);
462-
#endif
494+
#endif
463495
eventData.hovered.Add(current.gameObject);
496+
497+
// stop when encountering an object with the pointerEnterHandler
498+
if (!sendPointerHoverToParent && current.GetComponent<IPointerEnterHandler>() != null)
499+
break;
500+
501+
if (sendPointerHoverToParent)
502+
current = current.parent;
503+
504+
// if we reach the common root break out!
505+
if (current == commonRoot)
506+
break;
507+
508+
if (!sendPointerHoverToParent)
509+
current = current.parent;
464510
}
465511
}
466512
}
@@ -516,9 +562,9 @@ private void ProcessPointerButton(ref PointerModel.ButtonState button, PointerEv
516562
// Set pointerPress. This nukes lastPress. Meaning that after OnPointerDown, lastPress will
517563
// become null.
518564
eventData.pointerPress = newPressed;
519-
#if UNITY_2020_1_OR_NEWER // pointerClick doesn't exist before this.
565+
#if UNITY_2020_1_OR_NEWER // pointerClick doesn't exist before this.
520566
eventData.pointerClick = pointerClickHandler;
521-
#endif
567+
#endif
522568
eventData.rawPointerPress = currentOverGo;
523569

524570
// Save the drag handler for drag events during this mouse down.
@@ -539,11 +585,11 @@ private void ProcessPointerButton(ref PointerModel.ButtonState button, PointerEv
539585
// 2) StandaloneInputModule increases click counts even if something is eventually not deemed a
540586
// click and OnPointerClick is thus never invoked.
541587
var pointerClickHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
542-
#if UNITY_2020_1_OR_NEWER
588+
#if UNITY_2020_1_OR_NEWER
543589
var isClick = eventData.pointerClick != null && eventData.pointerClick == pointerClickHandler && eventData.eligibleForClick;
544-
#else
590+
#else
545591
var isClick = eventData.pointerPress != null && eventData.pointerPress == pointerClickHandler && eventData.eligibleForClick;
546-
#endif
592+
#endif
547593
if (isClick)
548594
{
549595
// Count clicks.
@@ -566,11 +612,13 @@ private void ProcessPointerButton(ref PointerModel.ButtonState button, PointerEv
566612

567613
// Invoke OnPointerClick or OnDrop.
568614
if (isClick)
569-
#if UNITY_2020_1_OR_NEWER
615+
{
616+
#if UNITY_2020_1_OR_NEWER
570617
ExecuteEvents.Execute(eventData.pointerClick, eventData, ExecuteEvents.pointerClickHandler);
571-
#else
618+
#else
572619
ExecuteEvents.Execute(eventData.pointerPress, eventData, ExecuteEvents.pointerClickHandler);
573-
#endif
620+
#endif
621+
}
574622
else if (eventData.dragging && eventData.pointerDrag != null)
575623
ExecuteEvents.ExecuteHierarchy(currentOverGo, eventData, ExecuteEvents.dropHandler);
576624

@@ -2422,6 +2470,17 @@ private struct InputActionReferenceState
24222470

24232471
[NonSerialized] private GameObject m_LocalMultiPlayerRoot;
24242472

2473+
#if UNITY_INPUT_SYSTEM_SENDPOINTERHOVERTOPARENT
2474+
// Needed for testing.
2475+
internal new bool sendPointerHoverToParent
2476+
{
2477+
get => base.sendPointerHoverToParent;
2478+
set => base.sendPointerHoverToParent = value;
2479+
}
2480+
#else
2481+
private bool sendPointerHoverToParent => true;
2482+
#endif
2483+
24252484
/// <summary>
24262485
/// Controls the origin point of raycasts when the cursor is locked.
24272486
/// </summary>

0 commit comments

Comments
 (0)