#if UIELEMENTS_MODULE_PRESENT && UNITY_6000_2_OR_NEWER #define UITOOLKIT_WORLDSPACE_ENABLED #endif using System.Collections.Generic; using UnityEngine.EventSystems; #if UIELEMENTS_MODULE_PRESENT using UnityEngine.UIElements; #endif using UnityEngine.XR.Interaction.Toolkit.Interactors; namespace UnityEngine.XR.Interaction.Toolkit.UI { /// /// Helper struct to contain relevant pointer hit data from UI Toolkit /// internal struct PointerHitData { public Vector3 worldPosition; public Quaternion worldOrientation; public float hitDistance; public Collider hitCollider; #if UIELEMENTS_MODULE_PRESENT public UIDocument hitDocument; public VisualElement hitElement; #endif } /// /// Helper struct to contain physical collision data with UI Documents /// internal struct InteractorHitData { public Vector3 closestPoint; // On document collider surface public Vector3 interactorOrigin; public Vector3 interactorDirection; #if UIELEMENTS_MODULE_PRESENT public UIDocument hitDocument; #endif } /// /// Static utility class to assist with processing UI Toolkit events and registration. /// internal static class XRUIToolkitHandler { const int k_MaxInteractors = 8; const int k_InvalidIndex = -1; static readonly Vector3 k_ResetPos = new Vector3(0f, -1000f, 0f); class InteractorInfo { public IXRInteractor interactor; public int index; } /// /// Used to determine whether or not to process UI Toolkit events from interactors. /// public static bool uiToolkitSupportEnabled { get; set; } /// /// Gets the number of registered interactors. /// public static int count => s_RegisteredInteractors.Count; static readonly Dictionary s_RegisteredInteractors = new(); static readonly Dictionary s_InteractorHitData = new(); static readonly bool[] s_UsedIndices = new bool[k_MaxInteractors]; static readonly Dictionary s_LastWasDown = new(); static readonly Dictionary s_WasReset = new(); #if UITOOLKIT_WORLDSPACE_ENABLED static PanelInputConfiguration s_PanelInputConfigurationRef; static bool s_EventSystemValidated; static bool s_PanelInputConfigurationValidated; static bool s_DidCheckPanelInputConfiguration; /// /// Dictionary tracking which VisualElements are being manipulated by which interactors /// static readonly Dictionary s_InteractorElements = new(); /// /// Dictionary to store the initial Z depth values of manipulated VisualElements /// static readonly Dictionary s_InitialZDepth = new Dictionary(); #endif /// /// Registers an interactor with the XRUIToolkitHandler. /// /// The interactor to register. /// The index assigned to the interactor. public static int Register(IXRInteractor interactor) { #if UITOOLKIT_WORLDSPACE_ENABLED if (s_RegisteredInteractors.TryGetValue(interactor, out var registeredInteractor)) { Debug.LogWarning($"interactor {interactor} is already registered with XR UI Toolkit Handler."); return registeredInteractor.index; } // Find first available index from within the k_MaxInteractors var availableIndex = k_InvalidIndex; for (var i = 0; i < k_MaxInteractors; i++) { if (!s_UsedIndices[i]) { availableIndex = i; s_UsedIndices[i] = true; break; } } if (availableIndex == k_InvalidIndex) { Debug.LogError("No available indices for pointer registration."); return k_InvalidIndex; } var info = new InteractorInfo { interactor = interactor, index = availableIndex, }; s_RegisteredInteractors.Add(interactor, info); return availableIndex; #else return k_InvalidIndex; #endif } /// /// Unregisters an interactor from the XRUIToolkitHandler and cleans up its resources. /// /// The interactor to unregister. public static void Unregister(IXRInteractor interactor) { #if UITOOLKIT_WORLDSPACE_ENABLED if (!s_RegisteredInteractors.TryGetValue(interactor, out var info)) return; s_LastWasDown.Remove(info.index); s_WasReset.Remove(info.index); s_UsedIndices[info.index] = false; s_RegisteredInteractors.Remove(interactor); s_InteractorHitData.Remove(interactor); // Clean up any depth changes made by this interactor ClearZDepthForInteractor(interactor); #endif } /// /// Attempts to get the pointer index for a registered interactor. /// /// The interactor to get the index for. /// The output index if found. /// True if the interactor was found, false otherwise. public static bool TryGetPointerIndex(IXRInteractor interactor, out int index) { if (s_RegisteredInteractors.TryGetValue(interactor, out var info)) { index = info.index; return true; } index = -1; return false; } /// /// Updates the hit data for an interactor. /// /// The interactor to update hit data for. /// The hit data to update with. public static void UpdateInteractorHitData(IXRInteractor interactor, InteractorHitData hitData) { s_InteractorHitData[interactor] = hitData; } /// /// Attempts to get the hit data for an interactor. /// /// The interactor to get hit data for. /// The output hit data if found. /// True if hit data was found, false otherwise. public static bool TryGetInteractorHitData(IXRInteractor interactor, out InteractorHitData hitData) { return s_InteractorHitData.TryGetValue(interactor, out hitData); } /// /// Clears the hit data for an interactor. /// /// The interactor to clear hit data for. public static void ClearInteractorHitData(IXRInteractor interactor) { #if UITOOLKIT_WORLDSPACE_ENABLED ClearZDepthForInteractor(interactor); #endif s_InteractorHitData.Remove(interactor); } /// /// Clears all registered interactors and associated data. /// public static void Clear() { s_RegisteredInteractors.Clear(); s_LastWasDown.Clear(); s_WasReset.Clear(); s_InteractorHitData.Clear(); #if UITOOLKIT_WORLDSPACE_ENABLED s_InteractorElements.Clear(); // Clear interactor-element associations #endif for (int i = 0; i < k_MaxInteractors; i++) { s_UsedIndices[i] = false; } } /// /// Checks if an interactor is registered. /// /// The interactor to check. /// True if the interactor is registered, false otherwise. public static bool IsRegistered(IXRInteractor interactor) { return s_RegisteredInteractors.ContainsKey(interactor); } /// /// Handles pointer updates for an interactor. /// /// The interactor to update. /// The position of the interactor. /// The rotation of the interactor. /// Whether the UI select input is active. /// Whether the pointer should be reset. public static void HandlePointerUpdate( IXRInteractor interactor, Vector3 pos, Quaternion rot, bool isUiSelectInputActive, bool shouldReset) { if (!TryGetPointerIndex(interactor, out var pointerIndex)) return; // Ensure dictionary entries exist s_LastWasDown.TryAdd(pointerIndex, false); // Use shouldReset as initial TryAdd entry to skip reset call if it is the first call. s_WasReset.TryAdd(pointerIndex, shouldReset); // If we're in a reset state and we already reset last time, return early if (shouldReset && s_WasReset[pointerIndex]) return; // Validate the panel input configuration. // Check for UI Toolkit EventSystem interoperability upon first process. // Wait to do this until as late as possible (i.e., not upon startup or Register) since // the PanelInputConfiguration.current may not be assigned yet. if (ShouldCheckPanelInputConfigurationValidation()) ValidatePanelInputConfiguration(); var eventPos = shouldReset ? k_ResetPos : pos; var eventRot = shouldReset ? Quaternion.identity : rot; #if UITOOLKIT_WORLDSPACE_ENABLED // Handle pointer movement var moveEvent = new InputForUI.PointerEvent { pointerIndex = pointerIndex, type = InputForUI.PointerEvent.Type.PointerMoved, worldPosition = eventPos, worldOrientation = eventRot, eventSource = InputForUI.EventSource.TrackedDevice, maxDistance = 10f, }; InputForUI.EventProvider.Dispatch(InputForUI.Event.From(moveEvent)); // Handle button states bool currentPressed = !shouldReset && isUiSelectInputActive; if (currentPressed != s_LastWasDown[pointerIndex]) { s_LastWasDown[pointerIndex] = currentPressed; var eventType = currentPressed ? InputForUI.PointerEvent.Type.ButtonPressed : InputForUI.PointerEvent.Type.ButtonReleased; var buttonEvent = new InputForUI.PointerEvent { pointerIndex = pointerIndex, type = eventType, button = InputForUI.PointerEvent.Button.Primary, clickCount = 1, worldPosition = eventPos, worldOrientation = eventRot, eventSource = InputForUI.EventSource.TrackedDevice, maxDistance = 10f, }; InputForUI.EventProvider.Dispatch(InputForUI.Event.From(buttonEvent)); } #endif // Update reset state s_WasReset[pointerIndex] = shouldReset; if (shouldReset) ClearInteractorHitData(interactor); } /// /// Tries to get the current pointer hit data for the specified interactor /// /// The interactor to get hit data for /// The hit data if successful /// True if hit data was successfully retrieved public static bool TryGetPointerHitData(IXRInteractor interactor, out PointerHitData hitData) { hitData = default; #if UITOOLKIT_WORLDSPACE_ENABLED if (!TryGetPointerIndex(interactor, out var pointerIndex)) return false; PointerDeviceState.TrackedPointerState trackedState = PointerDeviceState.GetTrackedState(PointerId.trackedPointerIdBase + pointerIndex); if (trackedState == null) return false; hitData = new PointerHitData { worldPosition = trackedState.worldPosition, worldOrientation = trackedState.worldOrientation, hitDistance = trackedState.hit.distance, hitCollider = trackedState.hit.collider, hitDocument = trackedState.hit.document, hitElement = trackedState.hit.element, }; return true; #else return false; #endif } #if UITOOLKIT_WORLDSPACE_ENABLED || PACKAGE_DOCS_GENERATION /// /// Sets the Z depth for a VisualElement associated with a specific interactor. /// /// The VisualElement to set the Z depth for. /// The interactor that's manipulating this element. /// The new Z depth value. /// The new Z value after setting the depth. public static float SetZDepthForInteractor(VisualElement ve, IXRInteractor interactor, float z) { // Track which interactor is working with this element s_InteractorElements[interactor] = ve; // Store original Z position if not already stored var translateVal = ve.style.translate.value; if (!s_InitialZDepth.TryAdd(ve.controlid, translateVal.z)) s_InitialZDepth[ve.controlid] = translateVal.z; // Apply the new Z position ve.style.translate = new Translate(translateVal.x.value, translateVal.y.value, z); return z; } #endif #if UITOOLKIT_WORLDSPACE_ENABLED || PACKAGE_DOCS_GENERATION /// /// Resets the Z depth for a VisualElement to its original value. /// /// The VisualElement to reset. /// The original Z depth value. static float ResetDepth(VisualElement ve) { var translateVal = ve.style.translate.value; // Use the stored initial value if available if (s_InitialZDepth.TryGetValue(ve.controlid, out var originalZ)) { ve.style.translate = new Translate(translateVal.x.value, translateVal.y.value, originalZ); } else { // Otherwise reset to 0 ve.style.translate = new Translate(translateVal.x.value, translateVal.y.value, 0f); } return originalZ; } #endif #if UITOOLKIT_WORLDSPACE_ENABLED || PACKAGE_DOCS_GENERATION /// /// Clears Z depth for all VisualElements associated with the specified interactor, /// resetting them to their original depths. /// /// The interactor whose UI element depths should be reset. public static void ClearZDepthForInteractor(IXRInteractor interactor) { if (s_InteractorElements.TryGetValue(interactor, out var element) && element != null) { ResetDepth(element); s_InteractorElements.Remove(interactor); } } #endif /// /// Trigger updates to the Event System /// public static void UpdateEventSystem() { #if UITOOLKIT_WORLDSPACE_ENABLED if (count > 0) UIElementsRuntimeUtility.UpdateEventSystem(); #endif } /// /// Takes a set of colliders and checks if the first one in the list has a /// UI Document component. /// /// List of type . /// Will return if a UI Document is found on the first collider in the list. public static bool IsValidUIToolkitInteraction(List colliders) { return colliders.Count > 0 && HasUIDocument(colliders[0]); } /// /// Takes a single collider and checks to see if that collider has a UI Document component. /// /// The to check. /// Will return if a UI Document is found on the . public static bool HasUIDocument(Collider collider) { #if UIELEMENTS_MODULE_PRESENT return collider.TryGetComponent(out UIDocument _); #else return false; #endif } /// /// Checks the need for and validation. /// /// Returns if the or the have not been validated or checked, or /// if the has changed. Otherwise, returns . /// /// This does not check or indicate need for revalidation if the existing /// has changed and is now invalid. /// static bool ShouldCheckPanelInputConfigurationValidation() { #if UITOOLKIT_WORLDSPACE_ENABLED // Re-validate panel input configuration if it has changed if (s_PanelInputConfigurationRef != PanelInputConfiguration.current) { s_EventSystemValidated = false; s_PanelInputConfigurationValidated = false; s_DidCheckPanelInputConfiguration = false; return true; } var didCheckConfiguration = s_DidCheckPanelInputConfiguration || (s_EventSystemValidated && s_PanelInputConfigurationValidated); return !didCheckConfiguration; #else return false; #endif } /// /// Validates the and for UI Toolkit support. /// static void ValidatePanelInputConfiguration() { #if UITOOLKIT_WORLDSPACE_ENABLED s_DidCheckPanelInputConfiguration = true; s_PanelInputConfigurationValidated = false; // Validate event system if (!s_EventSystemValidated && EventSystem.current == null) return; s_EventSystemValidated = true; // Validate Panel Input Configuration var panelInputConfiguration = PanelInputConfiguration.current; // Keeps a reference to current panel input configuration to re-validate if panel input configuration changes s_PanelInputConfigurationRef = panelInputConfiguration; if (panelInputConfiguration == null) { // Warn user if a Panel Input Configuration was null and could not be found Debug.LogWarning("Detected an Event System component that could interfere with UI Toolkit input." + " Create a Panel Input Configuration component and configured it by setting Panel Input Redirection to No input redirection to prevent interactions with the Event System."); } else if (panelInputConfiguration.panelInputRedirection != PanelInputConfiguration.PanelInputRedirection.Never) { // Warn user if the detected Panel Input Configuration is found but is improperly configured Debug.LogWarning("Detected an Event System component that could interfere with UI Toolkit input." + " Configure your Panel Input Configuration component to set Panel Input Redirection to No input redirection to prevent interactions with the Event System."); } else { s_PanelInputConfigurationValidated = true; } #endif } } }