using System; using System.Collections.Generic; using UnityEngine.EventSystems; using UnityEngine.InputSystem.UI; using UnityEngine.Serialization; using UnityEngine.XR.Interaction.Toolkit.Interactors; using UnityEngine.UI; namespace UnityEngine.XR.Interaction.Toolkit.UI { /// /// Base class for input modules that send UI input. /// /// /// Multiple input modules may be placed on the same event system. In such a setup, /// the modules will synchronize with each other. /// [DefaultExecutionOrder(XRInteractionUpdateOrder.k_UIInputModule)] public abstract partial class UIInputModule : BaseInputModule { [Header("Configuration")] [SerializeField, FormerlySerializedAs("clickSpeed")] [Tooltip("The maximum time (in seconds) between two mouse presses for it to be consecutive click.")] float m_ClickSpeed = 0.3f; /// /// The maximum time (in seconds) between two mouse presses for it to be consecutive click. /// 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; /// /// The absolute value required by a move action on either axis required to trigger a move event. /// 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; /// /// The Initial delay (in seconds) between an initial move action and a repeated move action. /// 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; /// /// The speed (in seconds) that the move action repeats itself once repeating. /// 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; /// /// Scales the , for tracked devices, to make selection easier. /// public float trackedDeviceDragThresholdMultiplier { get => m_TrackedDeviceDragThresholdMultiplier; set => m_TrackedDeviceDragThresholdMultiplier = value; } [SerializeField, Tooltip("Scales the scrollDelta in event data, for tracked devices, to scroll at an expected speed.")] float m_TrackedScrollDeltaMultiplier = 5f; /// /// Scales the scrollDelta in event data, for tracked devices, to scroll at an expected speed. /// public float trackedScrollDeltaMultiplier { get => m_TrackedScrollDeltaMultiplier; set => m_TrackedScrollDeltaMultiplier = value; } [SerializeField, Tooltip("Disables sending events from Event System to UI Toolkit on behalf of this Input Module.")] bool m_BypassUIToolkitEvents; /// /// Disables sending events from Event System to UI Toolkit on behalf of this Input Module. /// public bool bypassUIToolkitEvents { get => m_BypassUIToolkitEvents; set => m_BypassUIToolkitEvents = value; } Camera m_UICamera; /// /// The that Unity uses to perform ray casts when determining the screen space location of a tracked device cursor. /// public Camera uiCamera { get { // If set by the user, always use this Camera. if (m_UICamera != null) return m_UICamera; // Camera.main returns the first active and enabled Main Camera, so if the cached one // is no longer enabled, find the new Main Camera. This is to support, for example, // toggling between different XROrigin rigs each with their own Main Camera. if (m_MainCameraCache == null || !m_MainCameraCache.isActiveAndEnabled) m_MainCameraCache = Camera.main; return m_MainCameraCache; } set => m_UICamera = value; } Camera m_MainCameraCache; AxisEventData m_CachedAxisEvent; readonly Dictionary m_PointerEventByPointerId = new Dictionary(); readonly Dictionary m_TrackedDeviceEventByPointerId = new Dictionary(); /// /// See . /// /// /// Processing is postponed from earlier in the frame ( has a /// script execution order of -1000) until this Update to allow other systems to /// update the poses that will be used to generate the ray casts used by this input module. ///
/// 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 . /// Ray Interactor needs the UI ray casts to be completed by the time /// calls into since that is dependent on /// whether a UI hit was closer than a 3D hit. This processing must therefore be done /// between Locomotion and to minimize latency. ///
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(); } } /// /// Process the current tick for the module. /// /// /// Executed once per Update call. Override for custom processing. /// /// protected virtual void DoProcess() { SendUpdateEventToSelectedObject(); } /// /// See BaseInputModule.Process(). /// public override void Process() { // Postpone processing until later in the frame } /// /// Sends an update event to the currently selected object. /// /// Returns whether the update event was used by the selected object. 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; } /// /// Called by EventSystem when the input module is made current. /// public override void ActivateModule() { base.ActivateModule(); // This is required for mouse/pointer events to work with UI Toolkit if (bypassUIToolkitEvents) EventSystem.SetUITookitEventSystemOverride(eventSystem, false, false); // Select firstSelectedGameObject if nothing is selected ATM. var toSelect = eventSystem.currentSelectedGameObject; if (toSelect == null) toSelect = eventSystem.firstSelectedGameObject; eventSystem.SetSelectedGameObject(toSelect, GetBaseEventData()); } /// /// This will check the existing lists of pointer events and hand back the most current /// GameObject entered by the current pointer. /// /// ID of the XR device pointer, mouse pointer or touch registered with the UIInputModule. /// Meaning this should correspond to either .pointerId or .pointerId. /// /// The GameObject that triggered the event. /// /// Any negative value used for will be treated as any. The first event /// from a tracked device will be used first, then to standard pointer devices such as mice and touchscreens. /// /// public GameObject GetCurrentGameObject(int pointerId) { // For negative pointer IDs, find any cached pointer events that have a registered pointerEnter object if (pointerId < 0) { foreach (var trackedEvent in m_TrackedDeviceEventByPointerId.Values) { if (trackedEvent != null && trackedEvent.pointerEnter != null) return trackedEvent.pointerEnter; } foreach (var trackedEvent in m_PointerEventByPointerId.Values) { if (trackedEvent != null && trackedEvent.pointerEnter != null) return trackedEvent.pointerEnter; } } else { if (m_TrackedDeviceEventByPointerId.TryGetValue(pointerId, out var trackedDeviceEvent)) return trackedDeviceEvent?.pointerEnter; if (m_PointerEventByPointerId.TryGetValue(pointerId, out var pointerEvent)) return pointerEvent?.pointerEnter; } return null; } /// /// Is the pointer with the given ID over an EventSystem object? /// /// ID of the XR device pointer, mouse pointer or touch registered with the UIInputModule. /// Meaning this should correspond to either .pointerId or .pointerId. /// /// Returns if the given pointer is currently hovering over a GameObject. Otherwise, returns . /// /// The pointer IDs are generated at runtime by the UIInputModule as devices are registered. Calling this method /// without any parameters will attempt to use the Left Mouse Button and will likely result in unexpected behavior. /// A negative pointerId value will be interpreted as "any pointer" and will return true if any XR pointer is /// currently over a GameObject. /// Note: The IDs used to check for interaction are not the same as standard InputDevice device IDs. /// /// /// /// /// using UnityEngine; /// using UnityEngine.EventSystems; /// using UnityEngine.XR.Interaction.Toolkit.UI; /// /// public class ClickExample : MonoBehaviour /// { /// [SerializeField] /// UIInputModule inputModule; /// /// private void OnEnable() /// { /// if (inputModule != null) /// { /// inputModule.pointerClick += OnDeviceButtonClick; /// } /// } /// /// private void OnDisable() /// { /// if (inputModule != null) /// { /// inputModule.pointerClick -= OnDeviceButtonClick; /// } /// } /// /// // This method will fire after registering with the UIInputModule callbacks. The UIInputModule will /// // pass the PointerEventData for the device responsible for triggering the callback and can be used to /// // find the pointerId registered with the EventSystem for that device-specific event. /// private void OnDeviceButtonClick(GameObject selected, PointerEventData pointerData) /// { /// if (EventSystem.current.IsPointerOverGameObject(pointerData.pointerId)) /// { /// Debug.Log($"Clicked on {EventSystem.current.currentSelectedGameObject}", this); /// } /// } /// } /// /// public override bool IsPointerOverGameObject(int pointerId) { return GetCurrentGameObject(pointerId) != null; } 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; } /// /// Takes an existing and dispatches all relevant changes through the event system. /// It also updates the internal data of the . /// /// The pointer state you want to forward into the UI Event System. private protected void ProcessPointerState(ref PointerModel pointerState) { if (!pointerState.changedThisFrame) return; var eventData = GetOrCreateCachedPointerEvent(pointerState.pointerId); eventData.Reset(); pointerState.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 = pointerState.leftButton; eventData.button = PointerEventData.InputButton.Left; buttonState.CopyTo(eventData); ProcessPointerButton(buttonState.lastFrameDelta, eventData); ProcessPointerMovement(eventData); ProcessScrollWheel(eventData); pointerState.CopyFrom(eventData); ProcessPointerButtonDrag(eventData, UIPointerType.MouseOrPen); buttonState.CopyFrom(eventData); pointerState.leftButton = buttonState; // Right Mouse Button buttonState = pointerState.rightButton; eventData.button = PointerEventData.InputButton.Right; buttonState.CopyTo(eventData); ProcessPointerButton(buttonState.lastFrameDelta, eventData); ProcessPointerButtonDrag(eventData, UIPointerType.MouseOrPen); buttonState.CopyFrom(eventData); pointerState.rightButton = buttonState; // Middle Mouse Button buttonState = pointerState.middleButton; eventData.button = PointerEventData.InputButton.Middle; buttonState.CopyTo(eventData); ProcessPointerButton(buttonState.lastFrameDelta, eventData); ProcessPointerButtonDrag(eventData, UIPointerType.MouseOrPen); buttonState.CopyFrom(eventData); pointerState.middleButton = buttonState; pointerState.OnFrameFinished(); } void ProcessPointerMovement(PointerEventData eventData) { var currentPointerTarget = eventData.pointerCurrentRaycast.gameObject; #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(); if (wasMoved) { for (var i = 0; i < eventData.hovered.Count; ++i) { pointerMove?.Invoke(eventData.hovered[i], eventData); ExecuteEvents.Execute(eventData.hovered[i], eventData, ExecuteEvents.pointerMoveHandler); } } #endif // 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); #if UNITY_2021_1_OR_NEWER if (wasMoved) { pointerMove?.Invoke(targetGameObject, eventData); ExecuteEvents.Execute(targetGameObject, eventData, ExecuteEvents.pointerMoveHandler); } #endif eventData.hovered.Add(targetGameObject); target = target.parent; } } } void ProcessPointerButton(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; eventData.useDragThreshold = true; var selectHandler = ExecuteEvents.GetEventHandler(hoverTarget); // If we have clicked something new, deselect the old thing // and leave 'selection handling' up to the press event. if (eventSystem.currentSelectedGameObject != null && 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(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(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(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); } eventData.eligibleForClick = false; eventData.pointerPress = null; eventData.rawPointerPress = null; if (eventData.dragging && pointerDrag != null) { endDrag?.Invoke(pointerDrag, eventData); ExecuteEvents.Execute(pointerDrag, eventData, ExecuteEvents.endDragHandler); } eventData.dragging = false; eventData.pointerDrag = null; } } void ProcessPointerButtonDrag(PointerEventData eventData, UIPointerType pointerType, float pixelDragThresholdMultiplier = 1.0f) { if (!eventData.IsPointerMoving() || (pointerType == UIPointerType.MouseOrPen && Cursor.lockState == CursorLockMode.Locked) || eventData.pointerDrag == null) { return; } if (!eventData.dragging) { var threshold = eventSystem.pixelDragThreshold * pixelDragThresholdMultiplier; if (!eventData.useDragThreshold || (eventData.pressPosition - eventData.position).sqrMagnitude >= (threshold * threshold)) { 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 ProcessScrollWheel(PointerEventData eventData) { var scrollDelta = eventData.scrollDelta; if (!Mathf.Approximately(scrollDelta.sqrMagnitude, 0f)) { var scrollHandler = ExecuteEvents.GetEventHandler(eventData.pointerEnter); scroll?.Invoke(scrollHandler, eventData); ExecuteEvents.ExecuteHierarchy(scrollHandler, eventData, ExecuteEvents.scrollHandler); } } #if ENABLE_LEGACY_INPUT_MANAGER && !ENABLE_INPUT_SYSTEM private protected void ProcessTouch(ref TouchModel touchState) { if (!touchState.changedThisFrame) return; var eventData = GetOrCreateCachedPointerEvent(touchState.pointerId); eventData.Reset(); touchState.CopyTo(eventData); eventData.pointerCurrentRaycast = (touchState.selectPhase == TouchPhase.Canceled) ? new RaycastResult() : PerformRaycast(eventData); eventData.button = PointerEventData.InputButton.Left; ProcessPointerButton(touchState.selectDelta, eventData); ProcessPointerMovement(eventData); ProcessPointerButtonDrag(eventData, UIPointerType.Touch); touchState.CopyFrom(eventData); touchState.OnFrameFinished(); } #endif private protected void ProcessTrackedDevice(ref TrackedDeviceModel deviceState, bool force = false) { if (!deviceState.changedThisFrame && !force) return; var eventData = GetOrCreateCachedTrackedDeviceEvent(deviceState.pointerId); eventData.Reset(); deviceState.CopyTo(eventData); eventData.scrollDelta *= m_TrackedScrollDeltaMultiplier; 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; if (TryGetCamera(eventData, out var screenPointCamera)) { Vector2 screenPosition; if (eventData.pointerCurrentRaycast.isValid) { screenPosition = screenPointCamera.WorldToScreenPoint(eventData.pointerCurrentRaycast.worldPosition); if ((deviceState.selectDelta & ButtonDeltaState.Pressed) != 0) { eventData.pressWorldPosition = eventData.pointerCurrentRaycast.worldPosition; } } else { var endPosition = eventData.rayPoints.Count > 0 ? eventData.rayPoints[eventData.rayPoints.Count - 1] : Vector3.zero; screenPosition = screenPointCamera.WorldToScreenPoint(endPosition); eventData.position = screenPosition; } var thisFrameDelta = screenPosition - eventData.position; eventData.position = screenPosition; eventData.delta = thisFrameDelta; ProcessPointerButton(deviceState.selectDelta, eventData); ProcessPointerMovement(eventData); ProcessScrollWheel(eventData); // In a VR headset context, the camera can move while the pointer/controller stays put, but this // breaks the standard 2D screen space model. This will ensure that the initial press position used // for drag detection is updated as head-movement updates each frame. if (eventData.pressPosition != Vector2.zero) { eventData.pressPosition = screenPointCamera.WorldToScreenPoint(eventData.pressWorldPosition); } ProcessPointerButtonDrag(eventData, UIPointerType.Tracked, m_TrackedDeviceDragThresholdMultiplier); var oldTarget = deviceState.implementationData.pointerTarget; deviceState.CopyFrom(eventData); var newTarget = deviceState.implementationData.pointerTarget; if (oldTarget != newTarget) { if (newTarget != null) { var selectable = newTarget.GetComponentInParent(); var scrollable = newTarget.GetComponentInParent(); deviceState.selectableObject = (selectable as Component)?.gameObject; deviceState.isScrollable = scrollable != null; } else { deviceState.selectableObject = null; deviceState.isScrollable = false; } } } deviceState.OnFrameFinished(); } bool TryGetCamera(PointerEventData eventData, out Camera screenPointCamera) { // Get associated Camera, or Main Camera, or Camera from ray cast, and if *nothing* exists, then abort processing this frame. screenPointCamera = uiCamera; if (screenPointCamera != null) return true; var module = eventData.pointerCurrentRaycast.module; if (module != null) { screenPointCamera = module.eventCamera; return screenPointCamera != null; } return false; } /// /// Takes an existing NavigationModel and dispatches all relevant changes through the event system. /// It also updates the internal data of the NavigationModel. /// /// The navigation state you want to forward into the UI Event System private protected void ProcessNavigationState(ref NavigationModel navigationState) { var usedSelectionChange = SendUpdateEventToSelectedObject(); // Don't send move events if disabled in the EventSystem. if (!eventSystem.sendNavigationEvents) return; var implementationData = navigationState.implementationData; var selectedGameObject = eventSystem.currentSelectedGameObject; var movement = navigationState.move; if (!usedSelectionChange && (!Mathf.Approximately(movement.x, 0f) || !Mathf.Approximately(movement.y, 0f))) { var time = Time.unscaledTime; var moveDirection = MoveDirection.None; if (movement.sqrMagnitude > m_MoveDeadzone * m_MoveDeadzone) { if (Mathf.Abs(movement.x) > Mathf.Abs(movement.y)) moveDirection = (movement.x > 0f) ? MoveDirection.Right : MoveDirection.Left; else moveDirection = (movement.y > 0f) ? 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 = movement; 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 ((navigationState.submitButtonDelta & ButtonDeltaState.Pressed) != 0) { submit?.Invoke(selectedGameObject, data); ExecuteEvents.Execute(selectedGameObject, data, ExecuteEvents.submitHandler); } if (!data.used && (navigationState.cancelButtonDelta & ButtonDeltaState.Pressed) != 0) { cancel?.Invoke(selectedGameObject, data); ExecuteEvents.Execute(selectedGameObject, data, ExecuteEvents.cancelHandler); } } } navigationState.implementationData = implementationData; navigationState.OnFrameFinished(); } /// /// Remove the associated with the given ID. /// /// ID of the XR device pointer, mouse pointer or touch registered with the UIInputModule. /// Meaning this should correspond to either .pointerId or .pointerId. /// private protected void RemovePointerEventData(int pointerId) { if (!m_TrackedDeviceEventByPointerId.Remove(pointerId)) m_PointerEventByPointerId.Remove(pointerId); } PointerEventData GetOrCreateCachedPointerEvent(int pointerId) { if (!m_PointerEventByPointerId.TryGetValue(pointerId, out var result)) { result = new PointerEventData(eventSystem); m_PointerEventByPointerId.Add(pointerId, result); } return result; } TrackedDeviceEventData GetOrCreateCachedTrackedDeviceEvent(int pointerId) { if (!m_TrackedDeviceEventByPointerId.TryGetValue(pointerId, out var result)) { result = new TrackedDeviceEventData(eventSystem); m_TrackedDeviceEventByPointerId.Add(pointerId, result); } return result; } AxisEventData GetOrCreateCachedAxisEvent() { var result = m_CachedAxisEvent; if (result == null) { result = new AxisEventData(eventSystem); m_CachedAxisEvent = result; } return result; } } }