Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions Assets/Tests/InputSystem/Plugins/UITests.InputModuleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
#if UNITY_INPUT_SYSTEM_SENDPOINTERHOVERTOPARENT
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.UI;
using UnityEngine.TestTools;
using UnityEngine.UI;

internal partial class UITests
{
#pragma warning disable CS0649
public class InputModuleTests : CoreTestsFixture
{
private InputSystemUIInputModule m_InputModule;
private Mouse m_Mouse;
private Image m_Image;
private Image m_NestedImage;

private bool sendPointerHoverToParent
{
set => m_InputModule.sendPointerHoverToParent = value;
}

private Vector2 mousePosition
{
set { Set(m_Mouse.position, value); }
}

public override void Setup()
{
base.Setup();

var canvas = new GameObject("Canvas").AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.gameObject.AddComponent<GraphicRaycaster>();

m_Image = new GameObject("Image").AddComponent<Image>();
m_Image.gameObject.transform.SetParent(canvas.transform);
RectTransform imageRectTransform = m_Image.GetComponent<RectTransform>();
imageRectTransform.sizeDelta = new Vector2(400f, 400f);
imageRectTransform.localPosition = Vector3.zero;

m_NestedImage = new GameObject("NestedImage").AddComponent<Image>();
m_NestedImage.gameObject.transform.SetParent(m_Image.transform);
RectTransform nestedImageRectTransform = m_NestedImage.GetComponent<RectTransform>();
nestedImageRectTransform.sizeDelta = new Vector2(200f, 200f);
nestedImageRectTransform.localPosition = Vector3.zero;

GameObject go = new GameObject("Event System");
var eventSystem = go.AddComponent<EventSystem>();
eventSystem.pixelDragThreshold = 1;

m_InputModule = go.AddComponent<InputSystemUIInputModule>();
Cursor.lockState = CursorLockMode.None;

m_Mouse = InputSystem.AddDevice<Mouse>();
var actions = ScriptableObject.CreateInstance<InputActionAsset>();
var uiActions = actions.AddActionMap("UI");
var pointAction = uiActions.AddAction("point", type: InputActionType.PassThrough);
pointAction.AddBinding("<Mouse>/position");
pointAction.Enable();
m_InputModule.point = InputActionReference.Create(pointAction);
}

[UnityTest]
public IEnumerator PointerEnterChildShouldNotFullyExit_NotSendPointerEventToParent()
{
sendPointerHoverToParent = false;
PointerExitCallbackCheck callbackCheck = m_Image.gameObject.AddComponent<PointerExitCallbackCheck>();
m_NestedImage.gameObject.AddComponent<PointerExitCallbackCheck>();
var screenMiddle = new Vector2(Screen.width / 2, Screen.height / 2);

mousePosition = screenMiddle - new Vector2(150, 150);
yield return null;
mousePosition = screenMiddle;
yield return null;
Assert.IsTrue(callbackCheck.pointerData.fullyExited == false);
}

[UnityTest]
public IEnumerator PointerEnterChildShouldNotExit_SendPointerEventToParent()
{
sendPointerHoverToParent = true;
PointerExitCallbackCheck callbackCheck = m_Image.gameObject.AddComponent<PointerExitCallbackCheck>();
m_NestedImage.gameObject.AddComponent<PointerExitCallbackCheck>();
var screenMiddle = new Vector2(Screen.width / 2, Screen.height / 2);

mousePosition = screenMiddle - new Vector2(150, 150);
yield return null;
mousePosition = screenMiddle;
yield return null;
Assert.IsTrue(callbackCheck.pointerData == null);
}

[UnityTest]
public IEnumerator PointerEnterChildShouldNotReenter()
{
PointerEnterCallbackCheck callbackCheck =
m_NestedImage.gameObject.AddComponent<PointerEnterCallbackCheck>();
var screenMiddle = new Vector2(Screen.width / 2, Screen.height / 2);

mousePosition = screenMiddle - new Vector2(150, 150);
yield return null;
mousePosition = screenMiddle;
yield return null;
Assert.IsTrue(callbackCheck.pointerData.reentered == false);
}

[UnityTest]
public IEnumerator PointerExitChildShouldReenter_NotSendPointerEventToParent()
{
sendPointerHoverToParent = false;
PointerEnterCallbackCheck callbackCheck =
m_Image.gameObject.AddComponent<PointerEnterCallbackCheck>();
var screenMiddle = new Vector2(Screen.width / 2, Screen.height / 2);

mousePosition = screenMiddle - new Vector2(150, 150);
yield return null;
mousePosition = screenMiddle;
yield return null;
mousePosition = screenMiddle - new Vector2(150, 150);
yield return null;
Assert.IsTrue(callbackCheck.pointerData.reentered == true);
}

[UnityTest]
public IEnumerator PointerExitChildShouldNotSendEnter_SendPointerEventToParent()
{
sendPointerHoverToParent = true;
m_NestedImage.gameObject.AddComponent<PointerEnterCallbackCheck>();
var screenMiddle = new Vector2(Screen.width / 2, Screen.height / 2);

mousePosition = screenMiddle;
yield return null;
PointerEnterCallbackCheck callbackCheck =
m_Image.gameObject.AddComponent<PointerEnterCallbackCheck>();
mousePosition = screenMiddle - new Vector2(150, 150);
yield return null;
Assert.IsTrue(callbackCheck.pointerData == null);
}

[UnityTest]
public IEnumerator PointerExitChildShouldFullyExit()
{
PointerExitCallbackCheck callbackCheck =
m_NestedImage.gameObject.AddComponent<PointerExitCallbackCheck>();
var screenMiddle = new Vector2(Screen.width / 2, Screen.height / 2);

mousePosition = screenMiddle - new Vector2(150, 150);
yield return null;
mousePosition = screenMiddle;
yield return null;
mousePosition = screenMiddle - new Vector2(150, 150);
yield return null;
Assert.IsTrue(callbackCheck.pointerData.fullyExited == true);
}

public class PointerExitCallbackCheck : MonoBehaviour, IPointerExitHandler
{
public PointerEventData pointerData { get; private set; }

public void OnPointerExit(PointerEventData eventData)
{
pointerData = eventData;
}
}

public class PointerEnterCallbackCheck : MonoBehaviour, IPointerEnterHandler
{
public PointerEventData pointerData { get; private set; }

public void OnPointerEnter(PointerEventData eventData)
{
pointerData = eventData;
}
}
}
}
#endif

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Assets/Tests/InputSystem/Plugins/UITests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
#pragma warning disable CS0649
////TODO: app focus handling

internal class UITests : CoreTestsFixture
internal partial class UITests : CoreTestsFixture
{
private struct TestObjects
{
Expand Down
5 changes: 5 additions & 0 deletions Assets/Tests/InputSystem/Unity.InputSystem.Tests.asmdef
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@
"name": "Unity",
"expression": "6000.0.11",
"define": "UNITY_INPUT_SYSTEM_INPUT_MODULE_SCROLL_DELTA"
},
{
"name": "Unity",
"expression": "6000.0.15",
"define": "UNITY_INPUT_SYSTEM_SENDPOINTERHOVERTOPARENT"
}
],
"noEngineReferences": false
Expand Down
9 changes: 4 additions & 5 deletions Packages/com.unity.inputsystem/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,17 @@ however, it has to be formatted properly to pass verification tests.
- Fixed an update loop in the asset editor that occurs when selecting an Action Map that has no Actions.
- 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)
- Fixed 'OnDrop' event not called when 'IPointerDownHandler' is also listened. [ISXB-1014](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1014)

### Added
- Added Hinge Angle sensor support for foldable devices.
- Added InputDeviceMatcher.WithManufacturerContains(string noRegexMatch) API to improve DualShockSupport.Initialize performance (ISX-1411)
- Added an IME Input sample scene.
- 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)

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

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

## [1.10.0] - 2024-07-24

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ private void ProcessPointerMovement(ref PointerModel pointer, ExtendedPointerEve

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

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

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

// We walk up the tree until a common root and the last entered and current entered object is found.
// Then send exit and enter events up to, but not including, the common root.
// ** or when !m_SendPointerEnterToParent, stop when meeting a gameobject with an exit event handler
if (eventData.pointerEnter != null)
{
for (var current = eventData.pointerEnter.transform; current != null && current != commonRoot; current = current.parent)
var current = eventData.pointerEnter.transform;
while (current != null)
{
// if we reach the common root break out!
if (sendPointerHoverToParent && current == commonRoot)
break;

// if we reach a PointerExitEvent break out!
if (!sendPointerHoverToParent && current == pointerParent)
break;

#if UNITY_2021_3_OR_NEWER
eventData.fullyExited = current != commonRoot && eventData.pointerEnter != currentPointerTarget;
#endif
ExecuteEvents.Execute(current.gameObject, eventData, ExecuteEvents.pointerExitHandler);
eventData.hovered.Remove(current.gameObject);

if (sendPointerHoverToParent)
current = current.parent;

// if we reach the common root break out!
if (current == commonRoot)
break;

if (!sendPointerHoverToParent)
current = current.parent;
}
}

// now issue the enter call up to but not including the common root
Transform oldPointerEnter = eventData.pointerEnter?.transform;
eventData.pointerEnter = currentPointerTarget;
if (currentPointerTarget != null)
{
for (var current = currentPointerTarget.transform;
current != null && current != commonRoot && !PointerShouldIgnoreTransform(current);
current = current.parent)
Transform current = currentPointerTarget.transform;
while (current != null && !PointerShouldIgnoreTransform(current))
{
#if UNITY_2021_3_OR_NEWER
eventData.reentered = current == commonRoot && current != oldPointerEnter;
// if we are sending the event to parent, they are already in hover mode at that point. No need to bubble up the event.
if (sendPointerHoverToParent && eventData.reentered)
break;
#endif

ExecuteEvents.Execute(current.gameObject, eventData, ExecuteEvents.pointerEnterHandler);
#if UNITY_2021_1_OR_NEWER
#if UNITY_2021_1_OR_NEWER
if (wasMoved)
ExecuteEvents.Execute(current.gameObject, eventData, ExecuteEvents.pointerMoveHandler);
#endif
#endif
eventData.hovered.Add(current.gameObject);

// stop when encountering an object with the pointerEnterHandler
if (!sendPointerHoverToParent && current.GetComponent<IPointerEnterHandler>() != null)
break;

if (sendPointerHoverToParent)
current = current.parent;

// if we reach the common root break out!
if (current == commonRoot)
break;

if (!sendPointerHoverToParent)
current = current.parent;
}
}
}
Expand Down Expand Up @@ -516,9 +562,9 @@ private void ProcessPointerButton(ref PointerModel.ButtonState button, PointerEv
// Set pointerPress. This nukes lastPress. Meaning that after OnPointerDown, lastPress will
// become null.
eventData.pointerPress = newPressed;
#if UNITY_2020_1_OR_NEWER // pointerClick doesn't exist before this.
#if UNITY_2020_1_OR_NEWER // pointerClick doesn't exist before this.
eventData.pointerClick = pointerClickHandler;
#endif
#endif
eventData.rawPointerPress = currentOverGo;

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

// Invoke OnPointerClick or OnDrop.
if (isClick)
#if UNITY_2020_1_OR_NEWER
{
#if UNITY_2020_1_OR_NEWER
ExecuteEvents.Execute(eventData.pointerClick, eventData, ExecuteEvents.pointerClickHandler);
#else
#else
ExecuteEvents.Execute(eventData.pointerPress, eventData, ExecuteEvents.pointerClickHandler);
#endif
#endif
}
else if (eventData.dragging && eventData.pointerDrag != null)
ExecuteEvents.ExecuteHierarchy(currentOverGo, eventData, ExecuteEvents.dropHandler);

Expand Down Expand Up @@ -2422,6 +2470,17 @@ private struct InputActionReferenceState

[NonSerialized] private GameObject m_LocalMultiPlayerRoot;

#if UNITY_INPUT_SYSTEM_SENDPOINTERHOVERTOPARENT
// Needed for testing.
internal new bool sendPointerHoverToParent
{
get => base.sendPointerHoverToParent;
set => base.sendPointerHoverToParent = value;
}
#else
private bool sendPointerHoverToParent => true;
#endif

/// <summary>
/// Controls the origin point of raycasts when the cursor is locked.
/// </summary>
Expand Down
Loading