#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION using AlicizaX.UI.Runtime; using UnityEngine.EventSystems; namespace UnityEngine.UI { public interface IUXNavigationCursorPolicy { void OnNavigationContextChanged(UXInputMode mode, UXNavigationScope previousTopScope, UXNavigationScope currentTopScope); } public sealed class UXNavigationRuntime : MonoBehaviour { private const int ScopeCapacity = 128; private const int InvalidIndex = -1; private static UXNavigationRuntime _instance; private static IUXNavigationCursorPolicy _cursorPolicy; private readonly UXNavigationScope[] _scopes = new UXNavigationScope[ScopeCapacity]; private int _scopeCount; private UXNavigationScope _topScope; private ulong _activationSerial; private bool _stateDirty = true; private bool _suppressionDirty = true; private bool _isFlushingState; private bool _isEnsuringSelection; private bool _contextNotificationDirty; private UXNavigationScope _pendingPreviousTopScope; 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; if (_instance != null) { _cursorPolicy?.OnNavigationContextChanged(UXInputModeService.CurrentMode, _instance._topScope, _instance._topScope); } } 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; _cursorPolicy = null; } } internal void RegisterScope(UXNavigationScope scope) { if (scope == null || scope.RuntimeIndex != InvalidIndex) { return; } if (_scopeCount >= _scopes.Length) { ReportCapacityExceeded(); return; } int index = _scopeCount++; _scopes[index] = scope; scope.RuntimeIndex = index; UXInputModeService.EnsureInstance(); MarkStateDirty(); } internal void UnregisterScope(UXNavigationScope scope) { if (scope == null) { return; } int index = scope.RuntimeIndex; if (index < 0 || index >= _scopeCount || _scopes[index] != scope) { return; } if (_topScope == scope) { SetTopScope(null, false); } scope.IsAvailable = false; scope.WasAvailable = false; scope.SetNavigationSuppressed(false); scope.RuntimeIndex = InvalidIndex; int last = --_scopeCount; UXNavigationScope movedScope = _scopes[last]; _scopes[last] = null; if (index != last) { _scopes[index] = movedScope; movedScope.RuntimeIndex = index; } MarkStateDirty(); } internal void MarkStateDirty() { _stateDirty = true; _suppressionDirty = true; } internal void MarkSuppressionDirty() { _suppressionDirty = true; } internal void InvalidateSkipCaches() { for (int i = 0; i < _scopeCount; i++) { _scopes[i].InvalidateSkipCacheOnly(); } MarkStateDirty(); } private void FlushStateIfDirty(bool notifyContext) { if (_isFlushingState) { return; } _isFlushingState = true; UXNavigationScope previousTopScope = _topScope; if (_stateDirty) { UXNavigationScope newTopScope = FindTopScope(); _stateDirty = false; SetTopScope(newTopScope, false); } if (_suppressionDirty) { ApplyScopeSuppression(); _suppressionDirty = false; } if (UXInputModeService.CurrentMode == UXInputMode.Gamepad || UXInputModeService.CurrentMode == UXInputMode.Keyboard) { EnsureNavigationSelection(); } _isFlushingState = false; if (notifyContext) { NotifyContextIfChanged(previousTopScope, _topScope); } else if (!ReferenceEquals(previousTopScope, _topScope)) { QueueContextNotification(previousTopScope); } } private UXNavigationScope FindTopScope() { UXNavigationScope bestScope = null; for (int i = 0; i < _scopeCount; i++) { UXNavigationScope scope = _scopes[i]; 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 < _scopeCount; i++) { UXNavigationScope scope = _scopes[i]; 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(); _isEnsuringSelection = true; eventSystem.SetSelectedGameObject(preferred != null ? preferred.gameObject : null); _isEnsuringSelection = false; GameObject selectedObject = eventSystem.currentSelectedGameObject; if (selectedObject != null) { _topScope.RecordSelection(selectedObject); } } private void RecordSelection(GameObject selectedObject) { if (!_isEnsuringSelection && (_stateDirty || _suppressionDirty)) { FlushStateIfDirty(true); } if (_topScope != null && _topScope.IsSelectableOwnedAndValid(selectedObject)) { _topScope.RecordSelection(selectedObject); } } private void OnInputModeChanged(UXInputMode mode) { UXNavigationScope previousTopScope = _topScope; if (mode == UXInputMode.Gamepad || mode == UXInputMode.Keyboard) { FlushStateIfDirty(false); } NotifyContextIfChanged(previousTopScope, _topScope); } private void SetTopScope(UXNavigationScope topScope, bool notifyContext) { if (ReferenceEquals(_topScope, topScope)) { return; } UXNavigationScope previousTopScope = _topScope; _topScope = topScope; _suppressionDirty = true; if (notifyContext) { NotifyContextIfChanged(previousTopScope, _topScope); } else { QueueContextNotification(previousTopScope); } } private void QueueContextNotification(UXNavigationScope previousTopScope) { if (!_contextNotificationDirty) { _pendingPreviousTopScope = previousTopScope; _contextNotificationDirty = true; } } private void NotifyContextIfChanged(UXNavigationScope previousTopScope, UXNavigationScope currentTopScope) { if (_contextNotificationDirty) { previousTopScope = _pendingPreviousTopScope; _pendingPreviousTopScope = null; _contextNotificationDirty = false; } _cursorPolicy?.OnNavigationContextChanged(UXInputModeService.CurrentMode, previousTopScope, currentTopScope); } 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() { #if UNITY_EDITOR || DEVELOPMENT_BUILD Debug.LogError("UXNavigationRuntime scope capacity exceeded."); #endif } } } #endif