From 240024159e38cc4683f05dcd7c495c970bc58b18 Mon Sep 17 00:00:00 2001 From: s402520 <sven.gerlach@stud-mail.uni-wuerzburg.de> Date: Fri, 26 May 2023 12:09:34 +0200 Subject: [PATCH] Added UI Toolkit Fix scripts (from https://github.com/katas94/WorldSpace_UIToolkit_XR) --- Assets/Plugins/XRUIInputModuleFix.meta | 8 + .../IUIInteractorRegisterer.cs | 34 + .../IUIInteractorRegisterer.cs.meta | 11 + .../XRUIInputModuleFix/JoystickModel.cs | 114 ++++ .../XRUIInputModuleFix/JoystickModel.cs.meta | 11 + .../Plugins/XRUIInputModuleFix/MouseModel.cs | 362 ++++++++++ .../XRUIInputModuleFix/MouseModel.cs.meta | 11 + .../Plugins/XRUIInputModuleFix/TouchModel.cs | 198 ++++++ .../XRUIInputModuleFix/TouchModel.cs.meta | 11 + .../UIInputModuleFix.Events.cs | 94 +++ .../UIInputModuleFix.Events.cs.meta | 11 + .../XRUIInputModuleFix/UIInputModuleFix.cs | 639 ++++++++++++++++++ .../UIInputModuleFix.cs.meta | 11 + .../XRUIInputModuleFix/XRUIInputModuleFix.cs | 325 +++++++++ .../XRUIInputModuleFix.cs.meta | 11 + Assets/Scenes/SampleScene.unity | 88 +++ ProjectSettings/ProjectSettings.asset | 2 - 17 files changed, 1939 insertions(+), 2 deletions(-) create mode 100644 Assets/Plugins/XRUIInputModuleFix.meta create mode 100644 Assets/Plugins/XRUIInputModuleFix/IUIInteractorRegisterer.cs create mode 100644 Assets/Plugins/XRUIInputModuleFix/IUIInteractorRegisterer.cs.meta create mode 100644 Assets/Plugins/XRUIInputModuleFix/JoystickModel.cs create mode 100644 Assets/Plugins/XRUIInputModuleFix/JoystickModel.cs.meta create mode 100644 Assets/Plugins/XRUIInputModuleFix/MouseModel.cs create mode 100644 Assets/Plugins/XRUIInputModuleFix/MouseModel.cs.meta create mode 100644 Assets/Plugins/XRUIInputModuleFix/TouchModel.cs create mode 100644 Assets/Plugins/XRUIInputModuleFix/TouchModel.cs.meta create mode 100644 Assets/Plugins/XRUIInputModuleFix/UIInputModuleFix.Events.cs create mode 100644 Assets/Plugins/XRUIInputModuleFix/UIInputModuleFix.Events.cs.meta create mode 100644 Assets/Plugins/XRUIInputModuleFix/UIInputModuleFix.cs create mode 100644 Assets/Plugins/XRUIInputModuleFix/UIInputModuleFix.cs.meta create mode 100644 Assets/Plugins/XRUIInputModuleFix/XRUIInputModuleFix.cs create mode 100644 Assets/Plugins/XRUIInputModuleFix/XRUIInputModuleFix.cs.meta diff --git a/Assets/Plugins/XRUIInputModuleFix.meta b/Assets/Plugins/XRUIInputModuleFix.meta new file mode 100644 index 0000000..f65cd25 --- /dev/null +++ b/Assets/Plugins/XRUIInputModuleFix.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0258d149f294b034797c229c3eb6a8e3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/XRUIInputModuleFix/IUIInteractorRegisterer.cs b/Assets/Plugins/XRUIInputModuleFix/IUIInteractorRegisterer.cs new file mode 100644 index 0000000..b6725ad --- /dev/null +++ b/Assets/Plugins/XRUIInputModuleFix/IUIInteractorRegisterer.cs @@ -0,0 +1,34 @@ +using UnityEngine.EventSystems; + +namespace UnityEngine.XR.Interaction.Toolkit.UI +{ + [RequireComponent(typeof(IUIInteractor))] + public class IUIInteractorRegisterer : MonoBehaviour + { + void OnEnable () + { + XRUIInputModuleFix module = EventSystem.current?.GetComponent<XRUIInputModuleFix>(); + + if (module != null) + { + IUIInteractor[] interactors = GetComponents<IUIInteractor>(); + + foreach (IUIInteractor interactor in interactors) + module.RegisterInteractor(interactor); + } + } + + void OnDisable () + { + XRUIInputModuleFix module = EventSystem.current?.GetComponent<XRUIInputModuleFix>(); + + if (module != null) + { + IUIInteractor[] interactors = GetComponents<IUIInteractor>(); + + foreach (IUIInteractor interactor in interactors) + module.UnregisterInteractor(interactor); + } + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/XRUIInputModuleFix/IUIInteractorRegisterer.cs.meta b/Assets/Plugins/XRUIInputModuleFix/IUIInteractorRegisterer.cs.meta new file mode 100644 index 0000000..9048d51 --- /dev/null +++ b/Assets/Plugins/XRUIInputModuleFix/IUIInteractorRegisterer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fb62641662b746c4ba1436abd6f193eb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/XRUIInputModuleFix/JoystickModel.cs b/Assets/Plugins/XRUIInputModuleFix/JoystickModel.cs new file mode 100644 index 0000000..6d7bc21 --- /dev/null +++ b/Assets/Plugins/XRUIInputModuleFix/JoystickModel.cs @@ -0,0 +1,114 @@ +using UnityEngine.EventSystems; + +namespace UnityEngine.XR.Interaction.Toolkit.UI +{ + /// <summary> + /// Represents the state of a joystick in the Unity UI (UGUI) system. Keeps track of various book-keeping regarding UI selection, and move and button states. + /// </summary> + struct JoystickModel + { + public struct ImplementationData + { + /// <summary> + /// Bookkeeping values for Unity UI (UGUI) that tracks the number of sequential move commands in the same direction that have been sent. Used to handle proper repeat timing. + /// </summary> + public int consecutiveMoveCount { get; set; } + + /// <summary> + /// Bookkeeping values for Unity UI (UGUI) that tracks the direction of the last move command. Used to handle proper repeat timing. + /// </summary> + public MoveDirection lastMoveDirection { get; set; } + + /// <summary> + /// Bookkeeping values for Unity UI (UGUI) that tracks the last time a move command was sent. Used to handle proper repeat timing. + /// </summary> + public float lastMoveTime { get; set; } + + /// <summary> + /// Resets this object to its default, unused state. + /// </summary> + public void Reset() + { + consecutiveMoveCount = 0; + lastMoveTime = 0.0f; + lastMoveDirection = MoveDirection.None; + } + } + + /// <summary> + /// A 2D Vector that represents a UI Selection movement command. Think moving up and down in options menus or highlighting options. + /// </summary> + public Vector2 move { get; set; } + + /// <summary> + /// Tracks the current state of the submit or 'move forward' button. Setting this also updates the <see cref="submitButtonDelta"/> to track if a press or release occurred in the frame. + /// </summary> + public bool submitButtonDown + { + get => m_SubmitButtonDown; + set + { + if (m_SubmitButtonDown != value) + { + submitButtonDelta = value ? ButtonDeltaState.Pressed : ButtonDeltaState.Released; + m_SubmitButtonDown = value; + } + } + } + + /// <summary> + /// Tracks the changes in <see cref="submitButtonDown"/> between calls to <see cref="OnFrameFinished"/>. + /// </summary> + internal ButtonDeltaState submitButtonDelta { get; private set; } + + /// <summary> + /// Tracks the current state of the submit or 'move backward' button. Setting this also updates the <see cref="cancelButtonDelta"/> to track if a press or release occurred in the frame. + /// </summary> + public bool cancelButtonDown + { + get => m_CancelButtonDown; + set + { + if (m_CancelButtonDown != value) + { + cancelButtonDelta = value ? ButtonDeltaState.Pressed : ButtonDeltaState.Released; + m_CancelButtonDown = value; + } + } + } + + /// <summary> + /// Tracks the changes in <see cref="cancelButtonDown"/> between calls to <see cref="OnFrameFinished"/>. + /// </summary> + internal ButtonDeltaState cancelButtonDelta { get; private set; } + + /// <summary> + /// Internal bookkeeping data used by the Unity UI (UGUI) system. + /// </summary> + internal ImplementationData implementationData { get; set; } + + /// <summary> + /// Resets this object to it's default, unused state. + /// </summary> + public void Reset() + { + move = Vector2.zero; + m_SubmitButtonDown = m_CancelButtonDown = false; + submitButtonDelta = cancelButtonDelta = ButtonDeltaState.NoChange; + + implementationData.Reset(); + } + + /// <summary> + /// Call this at the end of polling for per-frame changes. This resets delta values, such as <see cref="submitButtonDelta"/> and <see cref="cancelButtonDelta"/>. + /// </summary> + public void OnFrameFinished() + { + submitButtonDelta = ButtonDeltaState.NoChange; + cancelButtonDelta = ButtonDeltaState.NoChange; + } + + bool m_SubmitButtonDown; + bool m_CancelButtonDown; + } +} diff --git a/Assets/Plugins/XRUIInputModuleFix/JoystickModel.cs.meta b/Assets/Plugins/XRUIInputModuleFix/JoystickModel.cs.meta new file mode 100644 index 0000000..640e48b --- /dev/null +++ b/Assets/Plugins/XRUIInputModuleFix/JoystickModel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9b69cdbfe87c13b49831433b8ec8c50e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/XRUIInputModuleFix/MouseModel.cs b/Assets/Plugins/XRUIInputModuleFix/MouseModel.cs new file mode 100644 index 0000000..2c2e60c --- /dev/null +++ b/Assets/Plugins/XRUIInputModuleFix/MouseModel.cs @@ -0,0 +1,362 @@ +using System.Collections.Generic; +using UnityEngine.EventSystems; + +namespace UnityEngine.XR.Interaction.Toolkit.UI +{ + /// <summary> + /// Represents the state of a single mouse button within the Unity UI (UGUI) system. Keeps track of various book-keeping regarding clicks, drags, and presses. + /// Can be converted to and from PointerEventData for sending into Unity UI (UGUI). + /// </summary> + public struct MouseButtonModel + { + internal struct ImplementationData + { + /// <summary> + /// Used to cache whether or not the current mouse button is being dragged. + /// </summary> + /// <seealso cref="PointerEventData.dragging"/> + public bool isDragging { get; set; } + + /// <summary> + /// Used to cache the last time this button was pressed. + /// </summary> + /// <seealso cref="PointerEventData.clickTime"/> + public float pressedTime { get; set; } + + /// <summary> + /// The position on the screen that this button was last pressed. + /// In the same scale as <see cref="MouseModel.position"/>, and caches the same value as <see cref="PointerEventData.pressPosition"/>. + /// </summary> + /// <seealso cref="PointerEventData.pressPosition"/> + public Vector2 pressedPosition { get; set; } + + /// <summary> + /// The Raycast data from the time it was pressed. + /// </summary> + /// <seealso cref="PointerEventData.pointerPressRaycast"/> + public RaycastResult pressedRaycast { get; set; } + + /// <summary> + /// The last GameObject pressed on that can handle press or click events. + /// </summary> + /// <seealso cref="PointerEventData.pointerPress"/> + public GameObject pressedGameObject { get; set; } + + /// <summary> + /// The last GameObject pressed on regardless of whether it can handle events or not. + /// </summary> + /// <seealso cref="PointerEventData.rawPointerPress"/> + public GameObject pressedGameObjectRaw { get; set; } + + /// <summary> + /// The GameObject currently being dragged if any. + /// </summary> + /// <seealso cref="PointerEventData.pointerDrag"/> + public GameObject draggedGameObject { get; set; } + + /// <summary> + /// Resets this object to it's default, unused state. + /// </summary> + public void Reset() + { + isDragging = false; + pressedTime = 0f; + pressedPosition = Vector2.zero; + pressedRaycast = new RaycastResult(); + pressedGameObject = pressedGameObjectRaw = draggedGameObject = null; + } + } + + /// <summary> + /// Used to store the current binary state of the button. When set, will also track the changes between calls of <see cref="OnFrameFinished"/> in <see cref="lastFrameDelta"/>. + /// </summary> + public bool isDown + { + get => m_IsDown; + set + { + if (m_IsDown != value) + { + m_IsDown = value; + lastFrameDelta |= value ? ButtonDeltaState.Pressed : ButtonDeltaState.Released; + } + } + } + + /// <summary> + /// A set of flags to identify the changes that have occurred between calls of <see cref="OnFrameFinished"/>. + /// </summary> + internal ButtonDeltaState lastFrameDelta { get; private set; } + + /// <summary> + /// Resets this object to it's default, unused state. + /// </summary> + public void Reset() + { + lastFrameDelta = ButtonDeltaState.NoChange; + m_IsDown = false; + + m_ImplementationData.Reset(); + } + + /// <summary> + /// Call this on each frame in order to reset properties that detect whether or not a certain condition was met this frame. + /// </summary> + public void OnFrameFinished() => lastFrameDelta = ButtonDeltaState.NoChange; + + /// <summary> + /// Fills a <see cref="PointerEventData"/> with this mouse button's internally cached values. + /// </summary> + /// <param name="eventData">These objects are used to send data through the Unity UI (UGUI) system.</param> + public void CopyTo(PointerEventData eventData) + { + eventData.dragging = m_ImplementationData.isDragging; + eventData.clickTime = m_ImplementationData.pressedTime; + eventData.pressPosition = m_ImplementationData.pressedPosition; + eventData.pointerPressRaycast = m_ImplementationData.pressedRaycast; + eventData.pointerPress = m_ImplementationData.pressedGameObject; + eventData.rawPointerPress = m_ImplementationData.pressedGameObjectRaw; + eventData.pointerDrag = m_ImplementationData.draggedGameObject; + } + + /// <summary> + /// Fills this object with the values from a <see cref="PointerEventData"/>. + /// </summary> + /// <param name="eventData">These objects are used to send data through the Unity UI (UGUI) system.</param> + public void CopyFrom(PointerEventData eventData) + { + m_ImplementationData.isDragging = eventData.dragging; + m_ImplementationData.pressedTime = eventData.clickTime; + m_ImplementationData.pressedPosition = eventData.pressPosition; + m_ImplementationData.pressedRaycast = eventData.pointerPressRaycast; + m_ImplementationData.pressedGameObject = eventData.pointerPress; + m_ImplementationData.pressedGameObjectRaw = eventData.rawPointerPress; + m_ImplementationData.draggedGameObject = eventData.pointerDrag; + } + + bool m_IsDown; + ImplementationData m_ImplementationData; + } + + struct MouseModel + { + internal struct InternalData + { + /// <summary> + /// This tracks the current GUI targets being hovered over. + /// </summary> + /// <seealso cref="PointerEventData.hovered"/> + public List<GameObject> hoverTargets { get; set; } + + /// <summary> + /// Tracks the current enter/exit target being hovered over at any given moment. + /// </summary> + /// <seealso cref="PointerEventData.pointerEnter"/> + public GameObject pointerTarget { get; set; } + + public void Reset() + { + pointerTarget = null; + + if (hoverTargets == null) + hoverTargets = new List<GameObject>(); + else + hoverTargets.Clear(); + } + } + + /// <summary> + /// An Id representing a unique pointer. + /// </summary> + public int pointerId { get; } + + /// <summary> + /// A boolean value representing whether any mouse data has changed this frame, meaning that events should be processed. + /// </summary> + /// <remarks> + /// This only checks for changes in mouse state (<see cref="position"/>, <see cref="leftButton"/>, <see cref="rightButton"/>, <see cref="middleButton"/>, or <see cref="scrollPosition"/>). + /// </remarks> + public bool changedThisFrame { get; private set; } + + Vector2 m_Position; + + public Vector2 position + { + get => m_Position; + set + { + if (m_Position != value) + { + deltaPosition = value - m_Position; + m_Position = value; + changedThisFrame = true; + } + } + } + + /// <summary> + /// The pixel-space change in <see cref="position"/> since the last call to <see cref="OnFrameFinished"/>. + /// </summary> + public Vector2 deltaPosition { get; private set; } + + Vector2 m_ScrollDelta; + + /// <summary> + /// The amount of scroll since the last call to <see cref="OnFrameFinished"/>. + /// </summary> + public Vector2 scrollDelta + { + get => m_ScrollDelta; + set + { + if (m_ScrollDelta != value) + { + m_ScrollDelta = value; + changedThisFrame = true; + } + } + } + + MouseButtonModel m_LeftButton; + + /// <summary> + /// Cached data and button state representing a left mouse button on a mouse. + /// Used by Unity UI (UGUI) to keep track of persistent click, press, and drag states. + /// </summary> + public MouseButtonModel leftButton + { + get => m_LeftButton; + set + { + changedThisFrame |= (value.lastFrameDelta != ButtonDeltaState.NoChange); + m_LeftButton = value; + } + } + + /// <summary> + /// Shorthand to set the pressed state of the left mouse button. + /// </summary> + public bool leftButtonPressed + { + set + { + changedThisFrame |= m_LeftButton.isDown != value; + m_LeftButton.isDown = value; + } + } + + MouseButtonModel m_RightButton; + + /// <summary> + /// Cached data and button state representing a right mouse button on a mouse. + /// Used by Unity UI (UGUI) to keep track of persistent click, press, and drag states. + /// </summary> + public MouseButtonModel rightButton + { + get => m_RightButton; + set + { + changedThisFrame |= (value.lastFrameDelta != ButtonDeltaState.NoChange); + m_RightButton = value; + } + } + + /// <summary> + /// Shorthand to set the pressed state of the right mouse button. + /// </summary> + public bool rightButtonPressed + { + set + { + changedThisFrame |= m_RightButton.isDown != value; + m_RightButton.isDown = value; + } + } + + MouseButtonModel m_MiddleButton; + + /// <summary> + /// Cached data and button state representing a middle mouse button on a mouse. + /// Used by Unity UI (UGUI) to keep track of persistent click, press, and drag states. + /// </summary> + public MouseButtonModel middleButton + { + get => m_MiddleButton; + set + { + changedThisFrame |= (value.lastFrameDelta != ButtonDeltaState.NoChange); + m_MiddleButton = value; + } + } + + /// <summary> + /// Shorthand to set the pressed state of the middle mouse button. + /// </summary> + public bool middleButtonPressed + { + set + { + changedThisFrame |= m_MiddleButton.isDown != value; + m_MiddleButton.isDown = value; + } + } + + InternalData m_InternalData; + + public MouseModel(int pointerId) + { + this.pointerId = pointerId; + changedThisFrame = false; + m_Position = Vector2.zero; + deltaPosition = Vector2.zero; + m_ScrollDelta = Vector2.zero; + + m_LeftButton = new MouseButtonModel(); + m_RightButton = new MouseButtonModel(); + m_MiddleButton = new MouseButtonModel(); + m_LeftButton.Reset(); + m_RightButton.Reset(); + m_MiddleButton.Reset(); + + m_InternalData = new InternalData(); + m_InternalData.Reset(); + } + + /// <summary> + /// Call this at the end of polling for per-frame changes. This resets delta values, such as <see cref="deltaPosition"/>, <see cref="scrollDelta"/>, and <see cref="MouseButtonModel.lastFrameDelta"/>. + /// </summary> + public void OnFrameFinished() + { + changedThisFrame = false; + deltaPosition = Vector2.zero; + m_ScrollDelta = Vector2.zero; + m_LeftButton.OnFrameFinished(); + m_RightButton.OnFrameFinished(); + m_MiddleButton.OnFrameFinished(); + } + + public void CopyTo(PointerEventData eventData) + { + eventData.pointerId = pointerId; + eventData.position = position; + eventData.delta = deltaPosition; + eventData.scrollDelta = scrollDelta; + + eventData.pointerEnter = m_InternalData.pointerTarget; + eventData.hovered.Clear(); + eventData.hovered.AddRange(m_InternalData.hoverTargets); + + // This is unset in legacy systems and can safely assumed to stay true. + eventData.useDragThreshold = true; + } + + public void CopyFrom(PointerEventData eventData) + { + var hoverTargets = m_InternalData.hoverTargets; + m_InternalData.hoverTargets.Clear(); + m_InternalData.hoverTargets.AddRange(eventData.hovered); + m_InternalData.hoverTargets = hoverTargets; + m_InternalData.pointerTarget = eventData.pointerEnter; + } + } +} diff --git a/Assets/Plugins/XRUIInputModuleFix/MouseModel.cs.meta b/Assets/Plugins/XRUIInputModuleFix/MouseModel.cs.meta new file mode 100644 index 0000000..6597f44 --- /dev/null +++ b/Assets/Plugins/XRUIInputModuleFix/MouseModel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cfe2a6fdc09d5f847ba128acac8cc882 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/XRUIInputModuleFix/TouchModel.cs b/Assets/Plugins/XRUIInputModuleFix/TouchModel.cs new file mode 100644 index 0000000..e9c0186 --- /dev/null +++ b/Assets/Plugins/XRUIInputModuleFix/TouchModel.cs @@ -0,0 +1,198 @@ +using System.Collections.Generic; +using UnityEngine.EventSystems; + +namespace UnityEngine.XR.Interaction.Toolkit.UI +{ + struct TouchModel + { + internal struct ImplementationData + { + /// <summary> + /// This tracks the current GUI targets being hovered over. + /// </summary> + /// <seealso cref="PointerEventData.hovered"/> + public List<GameObject> hoverTargets { get; set; } + + /// <summary> + /// Tracks the current enter/exit target being hovered over at any given moment. + /// </summary> + /// <seealso cref="PointerEventData.pointerEnter"/> + public GameObject pointerTarget { get; set; } + + /// <summary> + /// Used to cache whether or not the current mouse button is being dragged. + /// </summary> + /// <seealso cref="PointerEventData.dragging"/> + public bool isDragging { get; set; } + + /// <summary> + /// Used to cache the last time this button was pressed. + /// </summary> + /// <seealso cref="PointerEventData.clickTime"/> + public float pressedTime { get; set; } + + /// <summary> + /// The position on the screen that this button was last pressed. + /// In the same scale as <see cref="position"/>, and caches the same value as <see cref="PointerEventData.pressPosition"/>. + /// </summary> + /// <seealso cref="PointerEventData.pressPosition"/> + public Vector2 pressedPosition { get; set; } + + /// <summary> + /// The Raycast data from the time it was pressed. + /// </summary> + /// <seealso cref="PointerEventData.pointerPressRaycast"/> + public RaycastResult pressedRaycast { get; set; } + + /// <summary> + /// The last GameObject pressed on that can handle press or click events. + /// </summary> + /// <seealso cref="PointerEventData.pointerPress"/> + public GameObject pressedGameObject { get; set; } + + /// <summary> + /// The last GameObject pressed on regardless of whether it can handle events or not. + /// </summary> + /// <seealso cref="PointerEventData.rawPointerPress"/> + public GameObject pressedGameObjectRaw { get; set; } + + /// <summary> + /// The GameObject currently being dragged if any. + /// </summary> + /// <seealso cref="PointerEventData.pointerDrag"/> + public GameObject draggedGameObject { get; set; } + + /// <summary> + /// Resets this object to it's default, unused state. + /// </summary> + public void Reset() + { + isDragging = false; + pressedTime = 0f; + pressedPosition = Vector2.zero; + pressedRaycast = new RaycastResult(); + pressedGameObject = pressedGameObjectRaw = draggedGameObject = null; + + if (hoverTargets == null) + hoverTargets = new List<GameObject>(); + else + hoverTargets.Clear(); + } + } + + public int pointerId { get; } + + public TouchPhase selectPhase + { + get => m_SelectPhase; + set + { + if (m_SelectPhase != value) + { + if (value == TouchPhase.Began) + selectDelta |= ButtonDeltaState.Pressed; + + if (value == TouchPhase.Ended || value == TouchPhase.Canceled) + selectDelta |= ButtonDeltaState.Released; + + m_SelectPhase = value; + + changedThisFrame = true; + } + } + } + + public ButtonDeltaState selectDelta { get; private set; } + + public bool changedThisFrame { get; private set; } + + /// <summary> + /// The pixel-space position of the touch object. + /// </summary> + public Vector2 position + { + get => m_Position; + set + { + if (m_Position != value) + { + deltaPosition = value - m_Position; + m_Position = value; + changedThisFrame = true; + } + } + } + + /// <summary> + /// The pixel-space change in <see cref="position"/> since the last call to <see cref="OnFrameFinished"/>. + /// </summary> + public Vector2 deltaPosition { get; private set; } + + TouchPhase m_SelectPhase; + Vector2 m_Position; + ImplementationData m_ImplementationData; + + public TouchModel(int pointerId) + { + this.pointerId = pointerId; + + m_Position = deltaPosition = Vector2.zero; + + m_SelectPhase = TouchPhase.Canceled; + changedThisFrame = false; + selectDelta = ButtonDeltaState.NoChange; + + m_ImplementationData = new ImplementationData(); + m_ImplementationData.Reset(); + } + + public void Reset() + { + m_Position = deltaPosition = Vector2.zero; + changedThisFrame = false; + selectDelta = ButtonDeltaState.NoChange; + m_ImplementationData.Reset(); + } + + public void OnFrameFinished() + { + deltaPosition = Vector2.zero; + selectDelta = ButtonDeltaState.NoChange; + changedThisFrame = false; + } + + public void CopyTo(PointerEventData eventData) + { + eventData.pointerId = pointerId; + eventData.position = position; + eventData.delta = ((selectDelta & ButtonDeltaState.Pressed) != 0) ? Vector2.zero : deltaPosition; + + eventData.pointerEnter = m_ImplementationData.pointerTarget; + eventData.dragging = m_ImplementationData.isDragging; + eventData.clickTime = m_ImplementationData.pressedTime; + eventData.pressPosition = m_ImplementationData.pressedPosition; + eventData.pointerPressRaycast = m_ImplementationData.pressedRaycast; + eventData.pointerPress = m_ImplementationData.pressedGameObject; + eventData.rawPointerPress = m_ImplementationData.pressedGameObjectRaw; + eventData.pointerDrag = m_ImplementationData.draggedGameObject; + + eventData.hovered.Clear(); + eventData.hovered.AddRange(m_ImplementationData.hoverTargets); + } + + public void CopyFrom(PointerEventData eventData) + { + m_ImplementationData.pointerTarget = eventData.pointerEnter; + m_ImplementationData.isDragging = eventData.dragging; + m_ImplementationData.pressedTime = eventData.clickTime; + m_ImplementationData.pressedPosition = eventData.pressPosition; + m_ImplementationData.pressedRaycast = eventData.pointerPressRaycast; + m_ImplementationData.pressedGameObject = eventData.pointerPress; + m_ImplementationData.pressedGameObjectRaw = eventData.rawPointerPress; + m_ImplementationData.draggedGameObject = eventData.pointerDrag; + + m_ImplementationData.hoverTargets.Clear(); + m_ImplementationData.hoverTargets.AddRange(eventData.hovered); + } + } +} diff --git a/Assets/Plugins/XRUIInputModuleFix/TouchModel.cs.meta b/Assets/Plugins/XRUIInputModuleFix/TouchModel.cs.meta new file mode 100644 index 0000000..4d73690 --- /dev/null +++ b/Assets/Plugins/XRUIInputModuleFix/TouchModel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 09733180af2b84849b64e1380f4aec9a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/XRUIInputModuleFix/UIInputModuleFix.Events.cs b/Assets/Plugins/XRUIInputModuleFix/UIInputModuleFix.Events.cs new file mode 100644 index 0000000..5729bdf --- /dev/null +++ b/Assets/Plugins/XRUIInputModuleFix/UIInputModuleFix.Events.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using UnityEngine.EventSystems; + +namespace UnityEngine.XR.Interaction.Toolkit.UI +{ + public abstract partial class UIInputModuleFix + { + /// <summary> + /// Calls the methods in its invocation list after the input module collects a list of type <see cref="RaycastResult"/>, but before the results are used. + /// Note that not all fields of the event data are still valid or up to date at this point in the UI event processing. + /// This event can be used to read, modify, or reorder results. + /// After the event, the first result in the list with a non-null GameObject will be used. + /// </summary> + public event Action<PointerEventData, List<RaycastResult>> finalizeRaycastResults; + + /// <summary> + /// This occurs when a UI pointer enters an element. + /// </summary> + public event Action<GameObject, PointerEventData> pointerEnter; + + /// <summary> + /// This occurs when a UI pointer exits an element. + /// </summary> + public event Action<GameObject, PointerEventData> pointerExit; + + /// <summary> + /// This occurs when a select button down occurs while a UI pointer is hovering an element. + /// This event is executed using ExecuteEvents.ExecuteHierarchy when sent to the target element. + /// </summary> + public event Action<GameObject, PointerEventData> pointerDown; + + /// <summary> + /// This occurs when a select button up occurs while a UI pointer is hovering an element. + /// </summary> + public event Action<GameObject, PointerEventData> pointerUp; + + /// <summary> + /// This occurs when a select button click occurs while a UI pointer is hovering an element. + /// </summary> + public event Action<GameObject, PointerEventData> pointerClick; + + /// <summary> + /// This occurs when a potential drag occurs on an element. + /// </summary> + public event Action<GameObject, PointerEventData> initializePotentialDrag; + + /// <summary> + /// This occurs when a drag first occurs on an element. + /// </summary> + public event Action<GameObject, PointerEventData> beginDrag; + + /// <summary> + /// This occurs every frame while dragging an element. + /// </summary> + public event Action<GameObject, PointerEventData> drag; + + /// <summary> + /// This occurs on the last frame an element is dragged. + /// </summary> + public event Action<GameObject, PointerEventData> endDrag; + + /// <summary> + /// This occurs when a dragged element is dropped on a drop handler. + /// </summary> + public event Action<GameObject, PointerEventData> drop; + + /// <summary> + /// This occurs when an element is scrolled + /// This event is executed using ExecuteEvents.ExecuteHierarchy when sent to the target element. + /// </summary> + public event Action<GameObject, PointerEventData> scroll; + + /// <summary> + /// This occurs on update for the currently selected object. + /// </summary> + public event Action<GameObject, BaseEventData> updateSelected; + + /// <summary> + /// This occurs when the move axis is activated. + /// </summary> + public event Action<GameObject, AxisEventData> move; + + /// <summary> + /// This occurs when the submit button is pressed. + /// </summary> + public event Action<GameObject, BaseEventData> submit; + + /// <summary> + /// This occurs when the cancel button is pressed. + /// </summary> + public event Action<GameObject, BaseEventData> cancel; + } +} diff --git a/Assets/Plugins/XRUIInputModuleFix/UIInputModuleFix.Events.cs.meta b/Assets/Plugins/XRUIInputModuleFix/UIInputModuleFix.Events.cs.meta new file mode 100644 index 0000000..7a098a1 --- /dev/null +++ b/Assets/Plugins/XRUIInputModuleFix/UIInputModuleFix.Events.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1a975c68a753baa4ebb9ec53f42d332e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/XRUIInputModuleFix/UIInputModuleFix.cs b/Assets/Plugins/XRUIInputModuleFix/UIInputModuleFix.cs new file mode 100644 index 0000000..7c0adca --- /dev/null +++ b/Assets/Plugins/XRUIInputModuleFix/UIInputModuleFix.cs @@ -0,0 +1,639 @@ +using System; +using UnityEngine.EventSystems; +using UnityEngine.Serialization; + +namespace UnityEngine.XR.Interaction.Toolkit.UI +{ + /// <summary> + /// Base class for input modules that send UI input. + /// </summary> + /// <remarks> + /// Multiple input modules may be placed on the same event system. In such a setup, + /// the modules will synchronize with each other. + /// </remarks> + [DefaultExecutionOrder(XRInteractionUpdateOrder.k_UIInputModule)] + public abstract partial class UIInputModuleFix : BaseInputModule + { + [SerializeField, FormerlySerializedAs("clickSpeed")] + [Tooltip("The maximum time (in seconds) between two mouse presses for it to be consecutive click.")] + float m_ClickSpeed = 0.3f; + /// <summary> + /// The maximum time (in seconds) between two mouse presses for it to be consecutive click. + /// </summary> + public float clickSpeed + { + get => m_ClickSpeed; + set => m_ClickSpeed = value; + } + + [SerializeField, FormerlySerializedAs("moveDeadzone")] + [Tooltip("The absolute value required by a move action on either axis required to trigger a move event.")] + float m_MoveDeadzone = 0.6f; + /// <summary> + /// The absolute value required by a move action on either axis required to trigger a move event. + /// </summary> + public float moveDeadzone + { + get => m_MoveDeadzone; + set => m_MoveDeadzone = value; + } + + [SerializeField, FormerlySerializedAs("repeatDelay")] + [Tooltip("The Initial delay (in seconds) between an initial move action and a repeated move action.")] + float m_RepeatDelay = 0.5f; + /// <summary> + /// The Initial delay (in seconds) between an initial move action and a repeated move action. + /// </summary> + public float repeatDelay + { + get => m_RepeatDelay; + set => m_RepeatDelay = value; + } + + [FormerlySerializedAs("repeatRate")] + [SerializeField, Tooltip("The speed (in seconds) that the move action repeats itself once repeating.")] + float m_RepeatRate = 0.1f; + /// <summary> + /// The speed (in seconds) that the move action repeats itself once repeating. + /// </summary> + public float repeatRate + { + get => m_RepeatRate; + set => m_RepeatRate = value; + } + + [FormerlySerializedAs("trackedDeviceDragThresholdMultiplier")] + [SerializeField, Tooltip("Scales the EventSystem.pixelDragThreshold, for tracked devices, to make selection easier.")] + float m_TrackedDeviceDragThresholdMultiplier = 2f; + /// <summary> + /// Scales the <see cref="EventSystem.pixelDragThreshold"/>, for tracked devices, to make selection easier. + /// </summary> + public float trackedDeviceDragThresholdMultiplier + { + get => m_TrackedDeviceDragThresholdMultiplier; + set => m_TrackedDeviceDragThresholdMultiplier = value; + } + + /// <summary> + /// The <see cref="Camera"/> that is used to perform 2D raycasts when determining the screen space location of a tracked device cursor. + /// </summary> + public Camera uiCamera { get; set; } + + AxisEventData m_CachedAxisEvent; + PointerEventData m_CachedPointerEvent; + TrackedDeviceEventData m_CachedTrackedDeviceEventData; + + /// <summary> + /// See <see cref="MonoBehaviour"/>. + /// </summary> + /// <remarks> + /// Processing is postponed from earlier in the frame (<see cref="EventSystem"/> has a + /// script execution order of <c>-1000</c>) until this Update to allow other systems to + /// update the poses that will be used to generate the raycasts used by this input module. + /// <br /> + /// For Ray Interactor, it must wait until after the Controller pose updates and Locomotion + /// moves the Rig in order to generate the current sample points used to create the rays used + /// for this frame. Those positions will be determined during <see cref="DoProcess"/>. + /// Ray Interactor needs the UI raycasts to be completed by the time <see cref="XRInteractionManager"/> + /// calls into <see cref="XRBaseInteractor.GetValidTargets"/> since that is dependent on + /// whether a UI hit was closer than a 3D hit. This processing must therefore be done + /// between Locomotion and <see cref="XRBaseInteractor.ProcessInteractor"/> to minimize latency. + /// </remarks> + protected virtual void Update() + { + // Check to make sure that Process should still be called. + // It would likely cause unexpected results if processing was done + // when this module is no longer the current one. + if (eventSystem.IsActive() && eventSystem.currentInputModule == this && eventSystem == EventSystem.current) + { + DoProcess(); + } + } + + /// <summary> + /// Process the current tick for the module. + /// </summary> + /// <remarks> + /// Executed once per Update call. Override for custom processing. + /// </remarks> + /// <seealso cref="Process"/> + protected virtual void DoProcess() + { + SendUpdateEventToSelectedObject(); + } + + /// <inheritdoc /> + public override void Process() + { + // Postpone processing until later in the frame + } + + /// <summary> + /// Sends an update event to the currently selected object. + /// </summary> + /// <returns>Returns whether the update event was used by the selected object.</returns> + protected bool SendUpdateEventToSelectedObject() + { + var selectedGameObject = eventSystem.currentSelectedGameObject; + if (selectedGameObject == null) + return false; + + var data = GetBaseEventData(); + updateSelected?.Invoke(selectedGameObject, data); + ExecuteEvents.Execute(selectedGameObject, data, ExecuteEvents.updateSelectedHandler); + return data.used; + } + + RaycastResult PerformRaycast(PointerEventData eventData) + { + if (eventData == null) + throw new ArgumentNullException(nameof(eventData)); + + eventSystem.RaycastAll(eventData, m_RaycastResultCache); + finalizeRaycastResults?.Invoke(eventData, m_RaycastResultCache); + var result = FindFirstRaycast(m_RaycastResultCache); + m_RaycastResultCache.Clear(); + return result; + } + + /// <summary> + /// Takes an existing <see cref="MouseModel"/> and dispatches all relevant changes through the event system. + /// It also updates the internal data of the <see cref="MouseModel"/>. + /// </summary> + /// <param name="mouseState">The mouse state you want to forward into the UI Event System.</param> + internal void ProcessMouse(ref MouseModel mouseState) + { + if (!mouseState.changedThisFrame) + return; + + var eventData = GetOrCreateCachedPointerEvent(); + eventData.Reset(); + + mouseState.CopyTo(eventData); + + eventData.pointerCurrentRaycast = PerformRaycast(eventData); + + // Left Mouse Button + // The left mouse button is 'dominant' and we want to also process hover and scroll events as if the occurred during the left click. + var buttonState = mouseState.leftButton; + buttonState.CopyTo(eventData); + ProcessMouseButton(buttonState.lastFrameDelta, eventData); + + ProcessMouseMovement(eventData); + ProcessMouseScroll(eventData); + + mouseState.CopyFrom(eventData); + + ProcessMouseButtonDrag(eventData); + + buttonState.CopyFrom(eventData); + mouseState.leftButton = buttonState; + + // Right Mouse Button + buttonState = mouseState.rightButton; + buttonState.CopyTo(eventData); + + ProcessMouseButton(buttonState.lastFrameDelta, eventData); + ProcessMouseButtonDrag(eventData); + + buttonState.CopyFrom(eventData); + mouseState.rightButton = buttonState; + + // Middle Mouse Button + buttonState = mouseState.middleButton; + buttonState.CopyTo(eventData); + + ProcessMouseButton(buttonState.lastFrameDelta, eventData); + ProcessMouseButtonDrag(eventData); + + buttonState.CopyFrom(eventData); + mouseState.middleButton = buttonState; + + mouseState.OnFrameFinished(); + } + + void ProcessMouseMovement(PointerEventData eventData) + { + var currentPointerTarget = eventData.pointerCurrentRaycast.gameObject; + + foreach (GameObject hovered in eventData.hovered) + ExecuteEvents.Execute(hovered, eventData, ExecuteEvents.pointerMoveHandler); + + // another way of triggering the pointerMoveHandler + // if (currentPointerTarget != null) + // ExecuteEvents.Execute(currentPointerTarget, eventData, ExecuteEvents.pointerMoveHandler); + + // If we have no target or pointerEnter has been deleted, + // we just send exit events to anything we are tracking + // and then exit. + if (currentPointerTarget == null || eventData.pointerEnter == null) + { + foreach (var hovered in eventData.hovered) + { + pointerExit?.Invoke(hovered, eventData); + ExecuteEvents.Execute(hovered, eventData, ExecuteEvents.pointerExitHandler); + } + + eventData.hovered.Clear(); + + if (currentPointerTarget == null) + { + eventData.pointerEnter = null; + return; + } + } + + if (eventData.pointerEnter == currentPointerTarget) + return; + + var commonRoot = FindCommonRoot(eventData.pointerEnter, currentPointerTarget); + + // 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. + if (eventData.pointerEnter != null) + { + var target = eventData.pointerEnter.transform; + + while (target != null) + { + if (commonRoot != null && commonRoot.transform == target) + break; + + var targetGameObject = target.gameObject; + pointerExit?.Invoke(targetGameObject, eventData); + ExecuteEvents.Execute(targetGameObject, eventData, ExecuteEvents.pointerExitHandler); + + eventData.hovered.Remove(targetGameObject); + + target = target.parent; + } + } + + eventData.pointerEnter = currentPointerTarget; + // ReSharper disable once ConditionIsAlwaysTrueOrFalse -- Could be null if it was destroyed immediately after executing above + if (currentPointerTarget != null) + { + var target = currentPointerTarget.transform; + + while (target != null && target.gameObject != commonRoot) + { + var targetGameObject = target.gameObject; + pointerEnter?.Invoke(targetGameObject, eventData); + ExecuteEvents.Execute(targetGameObject, eventData, ExecuteEvents.pointerEnterHandler); + + eventData.hovered.Add(targetGameObject); + + target = target.parent; + } + } + } + + void ProcessMouseButton(ButtonDeltaState mouseButtonChanges, PointerEventData eventData) + { + var hoverTarget = eventData.pointerCurrentRaycast.gameObject; + + if ((mouseButtonChanges & ButtonDeltaState.Pressed) != 0) + { + eventData.eligibleForClick = true; + eventData.delta = Vector2.zero; + eventData.dragging = false; + eventData.pressPosition = eventData.position; + eventData.pointerPressRaycast = eventData.pointerCurrentRaycast; + + var selectHandler = ExecuteEvents.GetEventHandler<ISelectHandler>(hoverTarget); + + // If we have clicked something new, deselect the old thing + // and leave 'selection handling' up to the press event. + if (selectHandler != eventSystem.currentSelectedGameObject) + eventSystem.SetSelectedGameObject(null, eventData); + + // search for the control that will receive the press. + // if we can't find a press handler set the press + // handler to be what would receive a click. + + pointerDown?.Invoke(hoverTarget, eventData); + var newPressed = ExecuteEvents.ExecuteHierarchy(hoverTarget, eventData, ExecuteEvents.pointerDownHandler); + + // We didn't find a press handler, so we search for a click handler. + if (newPressed == null) + newPressed = ExecuteEvents.GetEventHandler<IPointerClickHandler>(hoverTarget); + + var time = Time.unscaledTime; + + if (newPressed == eventData.lastPress && ((time - eventData.clickTime) < m_ClickSpeed)) + ++eventData.clickCount; + else + eventData.clickCount = 1; + + eventData.clickTime = time; + + eventData.pointerPress = newPressed; + eventData.rawPointerPress = hoverTarget; + + // Save the drag handler for drag events during this mouse down. + var dragObject = ExecuteEvents.GetEventHandler<IDragHandler>(hoverTarget); + eventData.pointerDrag = dragObject; + + if (dragObject != null) + { + initializePotentialDrag?.Invoke(dragObject, eventData); + ExecuteEvents.Execute(dragObject, eventData, ExecuteEvents.initializePotentialDrag); + } + } + + if ((mouseButtonChanges & ButtonDeltaState.Released) != 0) + { + var target = eventData.pointerPress; + pointerUp?.Invoke(target, eventData); + ExecuteEvents.Execute(target, eventData, ExecuteEvents.pointerUpHandler); + + var pointerUpHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(hoverTarget); + var pointerDrag = eventData.pointerDrag; + if (target == pointerUpHandler && eventData.eligibleForClick) + { + pointerClick?.Invoke(target, eventData); + ExecuteEvents.Execute(target, eventData, ExecuteEvents.pointerClickHandler); + } + else if (eventData.dragging && pointerDrag != null) + { + drop?.Invoke(hoverTarget, eventData); + ExecuteEvents.ExecuteHierarchy(hoverTarget, eventData, ExecuteEvents.dropHandler); + + endDrag?.Invoke(pointerDrag, eventData); + ExecuteEvents.Execute(pointerDrag, eventData, ExecuteEvents.endDragHandler); + } + + eventData.eligibleForClick = eventData.dragging = false; + eventData.pointerPress = eventData.rawPointerPress = eventData.pointerDrag = null; + } + } + + void ProcessMouseButtonDrag(PointerEventData eventData, float pixelDragThresholdMultiplier = 1.0f) + { + if (!eventData.IsPointerMoving() || + Cursor.lockState == CursorLockMode.Locked || + eventData.pointerDrag == null) + { + return; + } + + if (!eventData.dragging) + { + if ((eventData.pressPosition - eventData.position).sqrMagnitude >= ((eventSystem.pixelDragThreshold * eventSystem.pixelDragThreshold) * pixelDragThresholdMultiplier)) + { + var target = eventData.pointerDrag; + beginDrag?.Invoke(target, eventData); + ExecuteEvents.Execute(target, eventData, ExecuteEvents.beginDragHandler); + eventData.dragging = true; + } + } + + if (eventData.dragging) + { + // If we moved from our initial press object, process an up for that object. + var target = eventData.pointerPress; + if (target != eventData.pointerDrag) + { + pointerUp?.Invoke(target, eventData); + ExecuteEvents.Execute(target, eventData, ExecuteEvents.pointerUpHandler); + + eventData.eligibleForClick = false; + eventData.pointerPress = null; + eventData.rawPointerPress = null; + } + + drag?.Invoke(eventData.pointerDrag, eventData); + ExecuteEvents.Execute(eventData.pointerDrag, eventData, ExecuteEvents.dragHandler); + } + } + + void ProcessMouseScroll(PointerEventData eventData) + { + var scrollDelta = eventData.scrollDelta; + if (!Mathf.Approximately(scrollDelta.sqrMagnitude, 0f)) + { + var scrollHandler = ExecuteEvents.GetEventHandler<IScrollHandler>(eventData.pointerEnter); + scroll?.Invoke(scrollHandler, eventData); + ExecuteEvents.ExecuteHierarchy(scrollHandler, eventData, ExecuteEvents.scrollHandler); + } + } + + internal void ProcessTouch(ref TouchModel touchState) + { + if (!touchState.changedThisFrame) + return; + + var eventData = GetOrCreateCachedPointerEvent(); + eventData.Reset(); + + touchState.CopyTo(eventData); + + eventData.pointerCurrentRaycast = (touchState.selectPhase == TouchPhase.Canceled) ? new RaycastResult() : PerformRaycast(eventData); + eventData.button = PointerEventData.InputButton.Left; + + ProcessMouseButton(touchState.selectDelta, eventData); + ProcessMouseMovement(eventData); + ProcessMouseButtonDrag(eventData); + + touchState.CopyFrom(eventData); + + touchState.OnFrameFinished(); + } + + internal void ProcessTrackedDevice(ref TrackedDeviceModel deviceState, bool force = false) + { + if (!deviceState.changedThisFrame && !force) + return; + + var eventData = GetOrCreateCachedTrackedDeviceEvent(); + eventData.Reset(); + deviceState.CopyTo(eventData); + + eventData.button = PointerEventData.InputButton.Left; + + // Demolish the screen position so we don't trigger any hits from a GraphicRaycaster component on a Canvas. + // The position value is not used by the TrackedDeviceGraphicRaycaster. + // Restore the original value after the Raycast is complete. + var savedPosition = eventData.position; + eventData.position = new Vector2(float.MinValue, float.MinValue); + eventData.pointerCurrentRaycast = PerformRaycast(eventData); + eventData.position = savedPosition; + + // Get associated camera, or main-tagged camera, or camera from raycast, and if *nothing* exists, then abort processing this frame. + // ReSharper disable once LocalVariableHidesMember + var camera = uiCamera != null ? uiCamera : (uiCamera = Camera.main); + if (camera == null) + { + var module = eventData.pointerCurrentRaycast.module; + if (module != null) + camera = module.eventCamera; + } + + if (camera != null) + { + Vector2 screenPosition; + if (eventData.pointerCurrentRaycast.isValid) + { + screenPosition = camera.WorldToScreenPoint(eventData.pointerCurrentRaycast.worldPosition); + } + else + { + var endPosition = eventData.rayPoints.Count > 0 ? eventData.rayPoints[eventData.rayPoints.Count - 1] : Vector3.zero; + screenPosition = camera.WorldToScreenPoint(endPosition); + eventData.position = screenPosition; + } + + var thisFrameDelta = screenPosition - eventData.position; + eventData.position = screenPosition; + eventData.delta = thisFrameDelta; + + ProcessMouseButton(deviceState.selectDelta, eventData); + ProcessMouseMovement(eventData); + ProcessMouseScroll(eventData); + ProcessMouseButtonDrag(eventData, m_TrackedDeviceDragThresholdMultiplier); + + deviceState.CopyFrom(eventData); + } + + deviceState.OnFrameFinished(); + } + + // TODO Update UIInputModule to make use of unused ProcessJoystick method + /// <summary> + /// Takes an existing JoystickModel and dispatches all relevant changes through the event system. + /// It also updates the internal data of the JoystickModel. + /// </summary> + /// <param name="joystickState">The joystick state you want to forward into the UI Event System</param> + internal void ProcessJoystick(ref JoystickModel joystickState) + { + var implementationData = joystickState.implementationData; + + var usedSelectionChange = false; + var selectedGameObject = eventSystem.currentSelectedGameObject; + if (selectedGameObject != null) + { + var data = GetBaseEventData(); + updateSelected?.Invoke(selectedGameObject, data); + ExecuteEvents.Execute(selectedGameObject, data, ExecuteEvents.updateSelectedHandler); + usedSelectionChange = data.used; + } + + // Don't send move events if disabled in the EventSystem. + if (!eventSystem.sendNavigationEvents) + return; + + var movement = joystickState.move; + if (!usedSelectionChange && (!Mathf.Approximately(movement.x, 0f) || !Mathf.Approximately(movement.y, 0f))) + { + var time = Time.unscaledTime; + + var moveVector = joystickState.move; + + var moveDirection = MoveDirection.None; + if (moveVector.sqrMagnitude > m_MoveDeadzone * m_MoveDeadzone) + { + if (Mathf.Abs(moveVector.x) > Mathf.Abs(moveVector.y)) + moveDirection = (moveVector.x > 0) ? MoveDirection.Right : MoveDirection.Left; + else + moveDirection = (moveVector.y > 0) ? MoveDirection.Up : MoveDirection.Down; + } + + if (moveDirection != implementationData.lastMoveDirection) + { + implementationData.consecutiveMoveCount = 0; + } + + if (moveDirection != MoveDirection.None) + { + var allow = true; + if (implementationData.consecutiveMoveCount != 0) + { + if (implementationData.consecutiveMoveCount > 1) + allow = (time > (implementationData.lastMoveTime + m_RepeatRate)); + else + allow = (time > (implementationData.lastMoveTime + m_RepeatDelay)); + } + + if (allow) + { + var eventData = GetOrCreateCachedAxisEvent(); + eventData.Reset(); + + eventData.moveVector = moveVector; + eventData.moveDir = moveDirection; + + move?.Invoke(selectedGameObject, eventData); + ExecuteEvents.Execute(selectedGameObject, eventData, ExecuteEvents.moveHandler); + usedSelectionChange = eventData.used; + + implementationData.consecutiveMoveCount++; + implementationData.lastMoveTime = time; + implementationData.lastMoveDirection = moveDirection; + } + } + else + implementationData.consecutiveMoveCount = 0; + } + else + implementationData.consecutiveMoveCount = 0; + + if (!usedSelectionChange) + { + if (selectedGameObject != null) + { + var data = GetBaseEventData(); + if ((joystickState.submitButtonDelta & ButtonDeltaState.Pressed) != 0) + { + submit?.Invoke(selectedGameObject, data); + ExecuteEvents.Execute(selectedGameObject, data, ExecuteEvents.submitHandler); + } + + if (!data.used && (joystickState.cancelButtonDelta & ButtonDeltaState.Pressed) != 0) + { + cancel?.Invoke(selectedGameObject, data); + ExecuteEvents.Execute(selectedGameObject, data, ExecuteEvents.cancelHandler); + } + } + } + + joystickState.implementationData = implementationData; + joystickState.OnFrameFinished(); + } + + PointerEventData GetOrCreateCachedPointerEvent() + { + var result = m_CachedPointerEvent; + if (result == null) + { + result = new PointerEventData(eventSystem); + m_CachedPointerEvent = result; + } + + return result; + } + + TrackedDeviceEventData GetOrCreateCachedTrackedDeviceEvent() + { + var result = m_CachedTrackedDeviceEventData; + if (result == null) + { + result = new TrackedDeviceEventData(eventSystem); + m_CachedTrackedDeviceEventData = result; + } + + return result; + } + + AxisEventData GetOrCreateCachedAxisEvent() + { + var result = m_CachedAxisEvent; + if (result == null) + { + result = new AxisEventData(eventSystem); + m_CachedAxisEvent = result; + } + + return result; + } + } +} diff --git a/Assets/Plugins/XRUIInputModuleFix/UIInputModuleFix.cs.meta b/Assets/Plugins/XRUIInputModuleFix/UIInputModuleFix.cs.meta new file mode 100644 index 0000000..e3cf61e --- /dev/null +++ b/Assets/Plugins/XRUIInputModuleFix/UIInputModuleFix.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 35ac9e67600f67849810a275d97ed64c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/XRUIInputModuleFix/XRUIInputModuleFix.cs b/Assets/Plugins/XRUIInputModuleFix/XRUIInputModuleFix.cs new file mode 100644 index 0000000..bba96e8 --- /dev/null +++ b/Assets/Plugins/XRUIInputModuleFix/XRUIInputModuleFix.cs @@ -0,0 +1,325 @@ +using System; +using System.Collections.Generic; +using UnityEngine.InputSystem; +using UnityEngine.EventSystems; +using UnityEngine.UIElements; + +namespace UnityEngine.XR.Interaction.Toolkit.UI +{ + /// <summary> + /// Custom class for input modules that send UI input in XR. Adapted for UIToolkit runtime worldspace panels. + /// </summary> + public class XRUIInputModuleFix : UIInputModuleFix + { + struct RegisteredInteractor + { + public IUIInteractor interactor; + public TrackedDeviceModel model; + + public RegisteredInteractor(IUIInteractor interactor, int deviceIndex) + { + this.interactor = interactor; + model = new TrackedDeviceModel(deviceIndex); + } + } + + struct RegisteredTouch + { + public bool isValid; + public int touchId; + public TouchModel model; + + public RegisteredTouch(Touch touch, int deviceIndex) + { + touchId = touch.fingerId; + model = new TouchModel(deviceIndex); + isValid = true; + } + } + + [SerializeField, HideInInspector] + [Tooltip("The maximum distance to raycast with tracked devices to find hit objects.")] + float m_MaxTrackedDeviceRaycastDistance = 1000f; + + /// <summary> + /// The maximum distance to raycast with tracked devices to find hit objects. + /// </summary> + [Obsolete("maxRaycastDistance has been deprecated. Its value was unused, calling this property is unnecessary and should be removed.")] + public float maxRaycastDistance + { + get => m_MaxTrackedDeviceRaycastDistance; + set => m_MaxTrackedDeviceRaycastDistance = value; + } + + [SerializeField] + [Tooltip("If true, will forward 3D tracked device data to UI elements.")] + bool m_EnableXRInput = true; + + /// <summary> + /// If <see langword="true"/>, will forward 3D tracked device data to UI elements. + /// </summary> + public bool enableXRInput + { + get => m_EnableXRInput; + set => m_EnableXRInput = value; + } + + [SerializeField] + [Tooltip("If true, will forward 2D mouse data to UI elements.")] + bool m_EnableMouseInput = true; + + /// <summary> + /// If <see langword="true"/>, will forward 2D mouse data to UI elements. + /// </summary> + public bool enableMouseInput + { + get => m_EnableMouseInput; + set => m_EnableMouseInput = value; + } + + [SerializeField] + [Tooltip("If true, will forward 2D touch data to UI elements.")] + bool m_EnableTouchInput = true; + + [SerializeField] + protected bool usePenPointerIdBase = false; + + /// <summary> + /// If <see langword="true"/>, will forward 2D touch data to UI elements. + /// </summary> + public bool enableTouchInput + { + get => m_EnableTouchInput; + set => m_EnableTouchInput = value; + } + + MouseModel m_Mouse; + + readonly List<RegisteredTouch> m_RegisteredTouches = new List<RegisteredTouch>(); + + int m_RollingInteractorIndex = PointerId.touchPointerIdBase; + bool m_penBaseIdAssigned = false; + + readonly List<RegisteredInteractor> m_RegisteredInteractors = new List<RegisteredInteractor>(); + + /// <inheritdoc /> + protected override void OnEnable() + { + base.OnEnable(); + m_Mouse = new MouseModel(0); + } + + public override int ConvertUIToolkitPointerId(PointerEventData sourcePointerData) + { + return sourcePointerData.pointerId; + } + + /// <summary> + /// Register an <see cref="IUIInteractor"/> with the UI system. + /// Calling this will enable it to start interacting with UI. + /// </summary> + /// <param name="interactor">The <see cref="IUIInteractor"/> to use.</param> + public void RegisterInteractor(IUIInteractor interactor) + { + for (var i = 0; i < m_RegisteredInteractors.Count; i++) + { + if (m_RegisteredInteractors[i].interactor == interactor) + return; + } + + if (usePenPointerIdBase && !m_penBaseIdAssigned) + { + m_penBaseIdAssigned = true; + m_RegisteredInteractors.Add(new RegisteredInteractor(interactor, PointerId.penPointerIdBase)); + } + else + { + if (usePenPointerIdBase && m_RollingInteractorIndex == PointerId.penPointerIdBase) + ++m_RollingInteractorIndex; + + m_RegisteredInteractors.Add(new RegisteredInteractor(interactor, m_RollingInteractorIndex++)); + } + } + + /// <summary> + /// Unregisters an <see cref="IUIInteractor"/> with the UI system. + /// This cancels all UI Interaction and makes the <see cref="IUIInteractor"/> no longer able to affect UI. + /// </summary> + /// <param name="interactor">The <see cref="IUIInteractor"/> to stop using.</param> + public void UnregisterInteractor(IUIInteractor interactor) + { + for (var i = 0; i < m_RegisteredInteractors.Count; i++) + { + if (m_RegisteredInteractors[i].interactor == interactor) + { + var registeredInteractor = m_RegisteredInteractors[i]; + registeredInteractor.interactor = null; + m_RegisteredInteractors[i] = registeredInteractor; + return; + } + } + } + + /// <summary> + /// Gets an <see cref="IUIInteractor"/> from its corresponding Unity UI Pointer Id. + /// This can be used to identify individual Interactors from the underlying UI Events. + /// </summary> + /// <param name="pointerId">A unique integer representing an object that can point at UI.</param> + /// <returns>Returns the interactor associated with <paramref name="pointerId"/>. + /// Returns <see langword="null"/> if no Interactor is associated (e.g. if it's a mouse event).</returns> + public IUIInteractor GetInteractor(int pointerId) + { + for (var i = 0; i < m_RegisteredInteractors.Count; i++) + { + if (m_RegisteredInteractors[i].model.pointerId == pointerId) + { + return m_RegisteredInteractors[i].interactor; + } + } + + return null; + } + + /// <summary>Retrieves the UI Model for a selected <see cref="IUIInteractor"/>.</summary> + /// <param name="interactor">The <see cref="IUIInteractor"/> you want the model for.</param> + /// <param name="model">The returned model that reflects the UI state of the <paramref name="interactor"/>.</param> + /// <returns>Returns <see langword="true"/> if the model was able to retrieved. Otherwise, returns <see langword="false"/>.</returns> + public bool GetTrackedDeviceModel(IUIInteractor interactor, out TrackedDeviceModel model) + { + for (var i = 0; i < m_RegisteredInteractors.Count; i++) + { + if (m_RegisteredInteractors[i].interactor == interactor) + { + model = m_RegisteredInteractors[i].model; + return true; + } + } + + model = new TrackedDeviceModel(-1); + return false; + } + + /// <inheritdoc /> + protected override void DoProcess() + { + base.DoProcess(); + + if (m_EnableXRInput) + { + for (var i = 0; i < m_RegisteredInteractors.Count; i++) + { + var registeredInteractor = m_RegisteredInteractors[i]; + + // If device is removed, we send a default state to unclick any UI + if (registeredInteractor.interactor == null) + { + registeredInteractor.model.Reset(false); + ProcessTrackedDevice(ref registeredInteractor.model, true); + m_RegisteredInteractors.RemoveAt(i--); + } + else + { + registeredInteractor.interactor.UpdateUIModel(ref registeredInteractor.model); + ProcessTrackedDevice(ref registeredInteractor.model); + m_RegisteredInteractors[i] = registeredInteractor; + } + } + } + + if (m_EnableMouseInput) + ProcessMouse(); + + if (m_EnableTouchInput) + ProcessTouches(); + } + + void ProcessMouse() + { + if (Mouse.current != null) + { + // The Input System reports scroll in pixels, whereas the old Input class reported in lines. + // Example, scrolling down by one notch of a mouse wheel for Input would be (0, -1), + // but would be (0, -120) from Input System. + // For consistency between the two Active Input Handling modes and with StandaloneInputModule, + // scale the scroll value to the range expected by UI. + const float kPixelsPerLine = 120f; + m_Mouse.position = Mouse.current.position.ReadValue(); + m_Mouse.scrollDelta = Mouse.current.scroll.ReadValue() * (1 / kPixelsPerLine); + m_Mouse.leftButtonPressed = Mouse.current.leftButton.isPressed; + m_Mouse.rightButtonPressed = Mouse.current.rightButton.isPressed; + m_Mouse.middleButtonPressed = Mouse.current.middleButton.isPressed; + + ProcessMouse(ref m_Mouse); + } + else if (Input.mousePresent) + { + m_Mouse.position = Input.mousePosition; + m_Mouse.scrollDelta = Input.mouseScrollDelta; + m_Mouse.leftButtonPressed = Input.GetMouseButton(0); + m_Mouse.rightButtonPressed = Input.GetMouseButton(1); + m_Mouse.middleButtonPressed = Input.GetMouseButton(2); + + ProcessMouse(ref m_Mouse); + } + } + + void ProcessTouches() + { + if (Input.touchCount > 0) + { + var touches = Input.touches; + foreach (var touch in touches) + { + var registeredTouchIndex = -1; + + // Find if touch already exists + for (var j = 0; j < m_RegisteredTouches.Count; j++) + { + if (touch.fingerId == m_RegisteredTouches[j].touchId) + { + registeredTouchIndex = j; + break; + } + } + + if (registeredTouchIndex < 0) + { + // Not found, search empty pool + for (var j = 0; j < m_RegisteredTouches.Count; j++) + { + if (!m_RegisteredTouches[j].isValid) + { + // Re-use the Id + var pointerId = m_RegisteredTouches[j].model.pointerId; + m_RegisteredTouches[j] = new RegisteredTouch(touch, pointerId); + registeredTouchIndex = j; + break; + } + } + + if (registeredTouchIndex < 0) + { + // No Empty slots, add one + registeredTouchIndex = m_RegisteredTouches.Count; + m_RegisteredTouches.Add(new RegisteredTouch(touch, m_RollingInteractorIndex++)); + } + } + + var registeredTouch = m_RegisteredTouches[registeredTouchIndex]; + registeredTouch.model.selectPhase = touch.phase; + registeredTouch.model.position = touch.position; + m_RegisteredTouches[registeredTouchIndex] = registeredTouch; + } + + for (var i = 0; i < m_RegisteredTouches.Count; i++) + { + var registeredTouch = m_RegisteredTouches[i]; + ProcessTouch(ref registeredTouch.model); + if (registeredTouch.model.selectPhase == TouchPhase.Ended || registeredTouch.model.selectPhase == TouchPhase.Canceled) + registeredTouch.isValid = false; + m_RegisteredTouches[i] = registeredTouch; + } + } + } + } +} diff --git a/Assets/Plugins/XRUIInputModuleFix/XRUIInputModuleFix.cs.meta b/Assets/Plugins/XRUIInputModuleFix/XRUIInputModuleFix.cs.meta new file mode 100644 index 0000000..e4739dc --- /dev/null +++ b/Assets/Plugins/XRUIInputModuleFix/XRUIInputModuleFix.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 428d96dc7d2b75c4d94bc24fe82df2b5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity index 12f1e2b..c222e9b 100644 --- a/Assets/Scenes/SampleScene.unity +++ b/Assets/Scenes/SampleScene.unity @@ -123,6 +123,77 @@ NavMeshSettings: debug: m_Flags: 0 m_NavMeshData: {fileID: 0} +--- !u!1 &649525595 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 649525598} + - component: {fileID: 649525597} + - component: {fileID: 649525596} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &649525596 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 649525595} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 428d96dc7d2b75c4d94bc24fe82df2b5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_SendPointerHoverToParent: 1 + m_ClickSpeed: 0.1 + m_MoveDeadzone: 0.6 + m_RepeatDelay: 0.5 + m_RepeatRate: 0.1 + m_TrackedDeviceDragThresholdMultiplier: 2 + m_MaxTrackedDeviceRaycastDistance: 1000 + m_EnableXRInput: 1 + m_EnableMouseInput: 1 + m_EnableTouchInput: 1 + usePenPointerIdBase: 0 +--- !u!114 &649525597 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 649525595} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &649525598 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 649525595} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0.8125458, y: -0.20923173, z: 1.4384191} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1019091453 GameObject: m_ObjectHideFlags: 0 @@ -295,6 +366,23 @@ PrefabInstance: objectReference: {fileID: 0} m_RemovedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: 1159b36af7d7c11498e7a839d21a5268, type: 3} +--- !u!1 &1871036619 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 8170552307152042535, guid: 8cfe3f32eae0d304285287fececf058b, type: 3} + m_PrefabInstance: {fileID: 2094756388} + m_PrefabAsset: {fileID: 0} +--- !u!114 &1871036628 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1871036619} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fb62641662b746c4ba1436abd6f193eb, type: 3} + m_Name: + m_EditorClassIdentifier: --- !u!1001 &2094756388 PrefabInstance: m_ObjectHideFlags: 0 diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset index d189f14..b7c4fcb 100644 --- a/ProjectSettings/ProjectSettings.asset +++ b/ProjectSettings/ProjectSettings.asset @@ -137,8 +137,6 @@ PlayerSettings: bundleVersion: 0.1 preloadedAssets: - {fileID: 0} - - {fileID: 2022272034958044215, guid: 9c92df50043c4c24988b77aaa754ec15, type: 2} - - {fileID: 11400000, guid: 63a6df10c42823e4593558dd07e5406f, type: 2} metroInputSource: 0 wsaTransparentSwapchain: 0 m_HolographicPauseOnTrackingLoss: 1 -- GitLab