#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION using AlicizaX; using AlicizaX.UI.Runtime; using UnityEngine.EventSystems; namespace UnityEngine.UI { public interface IUXNavigationCursorPolicy { void OnInputModeChanged(UXInputMode mode, UXNavigationScope topScope); } public sealed class UXNavigationRuntime : MonoBehaviour { private const int InitialScopeCapacity = 64; private const int InvalidIndex = -1; private static UXNavigationRuntime _instance; private static IUXNavigationCursorPolicy _cursorPolicy; private UXNavigationScope[] _scopes = new UXNavigationScope[InitialScopeCapacity]; private int[] _freeIndices = new int[InitialScopeCapacity]; private int _freeCount; private int _scopeCount; private int _scopeCapacityHighWater; private UXNavigationScope _topScope; private ulong _activationSerial; private bool _stateDirty = true; private bool _suppressionDirty = true; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] private static void Bootstrap() { if (!AppServices.TryGetApp(out var uiService) || uiService == null) { return; } EnsureInstance(); UXInputModeService.EnsureInstance(); } internal static UXNavigationRuntime EnsureInstance() { if (_instance != null) { return _instance; } GameObject go = new GameObject("[UXNavigationRuntime]"); go.hideFlags = HideFlags.HideAndDontSave; DontDestroyOnLoad(go); _instance = go.AddComponent(); return _instance; } internal static bool TryGetInstance(out UXNavigationRuntime runtime) { runtime = _instance; return runtime != null; } public static void SetCursorPolicy(IUXNavigationCursorPolicy cursorPolicy) { _cursorPolicy = cursorPolicy; } public static void NotifySelection(GameObject selectedObject) { if (_instance != null) { _instance.RecordSelection(selectedObject); } } public static bool IsHolderWithinTopScope(UIHolderObjectBase holder) { if (_instance == null || _instance._topScope == null || holder == null) { return true; } UXNavigationScope scope = holder.GetComponent(); if (scope == null) { scope = holder.GetComponentInParent(true); } return scope == _instance._topScope; } private void Awake() { if (_instance != null && _instance != this) { Destroy(gameObject); return; } _instance = this; DontDestroyOnLoad(gameObject); hideFlags = HideFlags.HideAndDontSave; } private void OnEnable() { UXInputModeService.OnModeChanged += OnInputModeChanged; } private void OnDisable() { UXInputModeService.OnModeChanged -= OnInputModeChanged; } private void OnDestroy() { UXInputModeService.OnModeChanged -= OnInputModeChanged; if (_instance == this) { _instance = null; } } internal void RegisterScope(UXNavigationScope scope) { if (scope == null || scope.RuntimeIndex != InvalidIndex) { return; } int index; if (_freeCount > 0) { index = _freeIndices[--_freeCount]; } else { if (_scopeCapacityHighWater >= _scopes.Length) { ReportCapacityExceeded("UXNavigationRuntime scope capacity exceeded."); return; } index = _scopeCapacityHighWater++; } _scopes[index] = scope; scope.RuntimeIndex = index; _scopeCount++; MarkStateDirty(); } internal void UnregisterScope(UXNavigationScope scope) { if (scope == null) { return; } int index = scope.RuntimeIndex; if (index < 0 || index >= _scopes.Length || _scopes[index] != scope) { return; } if (_topScope == scope) { _topScope = null; } scope.IsAvailable = false; scope.WasAvailable = false; scope.SetNavigationSuppressed(false); scope.RuntimeIndex = InvalidIndex; _scopes[index] = null; _freeIndices[_freeCount++] = index; _scopeCount--; MarkStateDirty(); } internal void MarkStateDirty() { _stateDirty = true; _suppressionDirty = true; } internal void MarkSuppressionDirty() { _suppressionDirty = true; } internal void InvalidateSkipCaches() { for (int i = 0; i < _scopeCapacityHighWater; i++) { UXNavigationScope scope = _scopes[i]; if (scope != null) { scope.InvalidateSkipCacheOnly(); } } MarkStateDirty(); } private void FlushStateIfDirty() { if (_stateDirty) { UXNavigationScope newTopScope = FindTopScope(); _stateDirty = false; if (!ReferenceEquals(_topScope, newTopScope)) { _topScope = newTopScope; _suppressionDirty = true; } } if (_suppressionDirty) { ApplyScopeSuppression(); _suppressionDirty = false; } if (UXInputModeService.CurrentMode == UXInputMode.Gamepad || UXInputModeService.CurrentMode == UXInputMode.Keyboard) { EnsureNavigationSelection(); } } private UXNavigationScope FindTopScope() { UXNavigationScope bestScope = null; for (int i = 0; i < _scopeCapacityHighWater; i++) { UXNavigationScope scope = _scopes[i]; if (scope == null) { continue; } bool available = IsScopeAvailable(scope); scope.IsAvailable = available; if (scope.WasAvailable != available) { scope.WasAvailable = available; if (available) { scope.ActivationSerial = ++_activationSerial; } _suppressionDirty = true; } if (available && (bestScope == null || IsHigherPriority(scope, bestScope))) { bestScope = scope; } } return bestScope; } private static bool IsScopeAvailable(UXNavigationScope scope) { if (scope == null || !scope.isActiveAndEnabled || !scope.gameObject.activeInHierarchy) { return false; } Canvas canvas = scope.Canvas; return canvas != null && canvas.gameObject.layer == UIComponent.UIShowLayer && !scope.IsNavigationSkipped && scope.HasAvailableSelectable(); } private void ApplyScopeSuppression() { for (int i = 0; i < _scopeCapacityHighWater; i++) { UXNavigationScope scope = _scopes[i]; if (scope == null) { continue; } bool suppress = scope.IsAvailable && _topScope != null && scope != _topScope && _topScope.BlockLowerScopes && IsHigherPriority(_topScope, scope); scope.SetNavigationSuppressed(suppress); } } private void EnsureNavigationSelection() { EventSystem eventSystem = EventSystem.current; if (eventSystem == null || _topScope == null || !_topScope.RequireSelectionWhenGamepad) { return; } GameObject currentSelected = eventSystem.currentSelectedGameObject; if (_topScope.IsSelectableOwnedAndValid(currentSelected)) { _topScope.RecordSelection(currentSelected); return; } Selectable preferred = _topScope.GetPreferredSelectable(); eventSystem.SetSelectedGameObject(preferred != null ? preferred.gameObject : null); GameObject selectedObject = eventSystem.currentSelectedGameObject; if (selectedObject != null) { _topScope.RecordSelection(selectedObject); } } private void RecordSelection(GameObject selectedObject) { if (_stateDirty || _suppressionDirty) { FlushStateIfDirty(); } if (_topScope != null && _topScope.IsSelectableOwnedAndValid(selectedObject)) { _topScope.RecordSelection(selectedObject); } } private void OnInputModeChanged(UXInputMode mode) { _cursorPolicy?.OnInputModeChanged(mode, _topScope); if (mode == UXInputMode.Gamepad || mode == UXInputMode.Keyboard) { FlushStateIfDirty(); EnsureNavigationSelection(); } } private static bool IsHigherPriority(UXNavigationScope left, UXNavigationScope right) { int leftOrder = left.Canvas != null ? left.Canvas.sortingOrder : int.MinValue; int rightOrder = right.Canvas != null ? right.Canvas.sortingOrder : int.MinValue; if (leftOrder != rightOrder) { return leftOrder > rightOrder; } int leftDepth = left.GetHierarchyDepth(); int rightDepth = right.GetHierarchyDepth(); if (leftDepth != rightDepth) { return leftDepth > rightDepth; } return left.ActivationSerial > right.ActivationSerial; } private static void ReportCapacityExceeded(string message) { #if UNITY_EDITOR || DEVELOPMENT_BUILD Debug.LogError(message); #endif } } } #endif