diff --git a/Assets/Tests/InputSystem/Plugins/UITests.InputModuleTests.cs b/Assets/Tests/InputSystem/Plugins/UITests.InputModuleTests.cs new file mode 100644 index 0000000000..7b32dd4ace --- /dev/null +++ b/Assets/Tests/InputSystem/Plugins/UITests.InputModuleTests.cs @@ -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.renderMode = RenderMode.ScreenSpaceOverlay; + canvas.gameObject.AddComponent(); + + m_Image = new GameObject("Image").AddComponent(); + m_Image.gameObject.transform.SetParent(canvas.transform); + RectTransform imageRectTransform = m_Image.GetComponent(); + imageRectTransform.sizeDelta = new Vector2(400f, 400f); + imageRectTransform.localPosition = Vector3.zero; + + m_NestedImage = new GameObject("NestedImage").AddComponent(); + m_NestedImage.gameObject.transform.SetParent(m_Image.transform); + RectTransform nestedImageRectTransform = m_NestedImage.GetComponent(); + nestedImageRectTransform.sizeDelta = new Vector2(200f, 200f); + nestedImageRectTransform.localPosition = Vector3.zero; + + GameObject go = new GameObject("Event System"); + var eventSystem = go.AddComponent(); + eventSystem.pixelDragThreshold = 1; + + m_InputModule = go.AddComponent(); + Cursor.lockState = CursorLockMode.None; + + m_Mouse = InputSystem.AddDevice(); + var actions = ScriptableObject.CreateInstance(); + var uiActions = actions.AddActionMap("UI"); + var pointAction = uiActions.AddAction("point", type: InputActionType.PassThrough); + pointAction.AddBinding("/position"); + pointAction.Enable(); + m_InputModule.point = InputActionReference.Create(pointAction); + } + + [UnityTest] + public IEnumerator PointerEnterChildShouldNotFullyExit_NotSendPointerEventToParent() + { + sendPointerHoverToParent = false; + PointerExitCallbackCheck callbackCheck = m_Image.gameObject.AddComponent(); + m_NestedImage.gameObject.AddComponent(); + 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(); + m_NestedImage.gameObject.AddComponent(); + 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(); + 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(); + 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(); + var screenMiddle = new Vector2(Screen.width / 2, Screen.height / 2); + + mousePosition = screenMiddle; + yield return null; + PointerEnterCallbackCheck callbackCheck = + m_Image.gameObject.AddComponent(); + mousePosition = screenMiddle - new Vector2(150, 150); + yield return null; + Assert.IsTrue(callbackCheck.pointerData == null); + } + + [UnityTest] + public IEnumerator PointerExitChildShouldFullyExit() + { + PointerExitCallbackCheck callbackCheck = + m_NestedImage.gameObject.AddComponent(); + 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 diff --git a/Assets/Tests/InputSystem/Plugins/UITests.InputModuleTests.cs.meta b/Assets/Tests/InputSystem/Plugins/UITests.InputModuleTests.cs.meta new file mode 100644 index 0000000000..d13f91145a --- /dev/null +++ b/Assets/Tests/InputSystem/Plugins/UITests.InputModuleTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1085b92f54494b459c0dba3d893a96ec +timeCreated: 1722453279 \ No newline at end of file diff --git a/Assets/Tests/InputSystem/Plugins/UITests.cs b/Assets/Tests/InputSystem/Plugins/UITests.cs index eda76b62cc..ef2f16f3ce 100644 --- a/Assets/Tests/InputSystem/Plugins/UITests.cs +++ b/Assets/Tests/InputSystem/Plugins/UITests.cs @@ -38,7 +38,7 @@ #pragma warning disable CS0649 ////TODO: app focus handling -internal class UITests : CoreTestsFixture +internal partial class UITests : CoreTestsFixture { private struct TestObjects { diff --git a/Assets/Tests/InputSystem/Unity.InputSystem.Tests.asmdef b/Assets/Tests/InputSystem/Unity.InputSystem.Tests.asmdef index 5a2dcaee07..260e6e8b89 100644 --- a/Assets/Tests/InputSystem/Unity.InputSystem.Tests.asmdef +++ b/Assets/Tests/InputSystem/Unity.InputSystem.Tests.asmdef @@ -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 diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index ea645adeca..c7f001bcc7 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -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 diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs index efd09d6a01..c3a95d34e7 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs @@ -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(); @@ -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 @@ -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())?.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() != null) + break; + + if (sendPointerHoverToParent) + current = current.parent; + + // if we reach the common root break out! + if (current == commonRoot) + break; + + if (!sendPointerHoverToParent) + current = current.parent; } } } @@ -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. @@ -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(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. @@ -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); @@ -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 + /// /// Controls the origin point of raycasts when the cursor is locked. /// diff --git a/Packages/com.unity.inputsystem/InputSystem/Unity.InputSystem.asmdef b/Packages/com.unity.inputsystem/InputSystem/Unity.InputSystem.asmdef index d35fc93cc1..6bc8cc3e6a 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Unity.InputSystem.asmdef +++ b/Packages/com.unity.inputsystem/InputSystem/Unity.InputSystem.asmdef @@ -92,6 +92,11 @@ "expression": "6000.0.11", "define": "UNITY_INPUT_SYSTEM_INPUT_MODULE_SCROLL_DELTA" }, + { + "name": "Unity", + "expression": "6000.0.15", + "define": "UNITY_INPUT_SYSTEM_SENDPOINTERHOVERTOPARENT" + }, { "name": "com.unity.modules.unityanalytics", "expression": "1",