#if INPUTSYSTEM_SUPPORT using System.Collections.Generic; using AlicizaX; using AlicizaX.UI.Runtime; using UnityEngine.EventSystems; namespace UnityEngine.UI { internal sealed class UXNavigationRuntime : MonoBehaviour { private static UXNavigationRuntime _instance; private const float DiscoveryInterval = 0.5f; private readonly HashSet _scopeSet = new(); private readonly List _scopes = new(32); private readonly HashSet _interactiveLayerRoots = new(); private Transform _uiCanvasRoot; private UXNavigationScope _topScope; private GameObject _lastObservedSelection; private float _nextDiscoveryTime; private bool _discoveryDirty = true; private ulong _activationSerial; private bool _missingEventSystemLogged; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] private static void Bootstrap() { EnsureInstance(); UXInputModeService.EnsureInstance(); } internal static UXNavigationRuntime EnsureInstance() { if (_instance != null) { return _instance; } var 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; } internal static bool IsHolderWithinTopScope(UIHolderObjectBase holder) { if (_instance == null || _instance._topScope == null || holder == null) { return true; } return _instance.IsHolderOwnedByScope(holder, _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 || !_scopeSet.Add(scope)) { return; } _scopes.Add(scope); MarkDiscoveryDirty(); } internal void UnregisterScope(UXNavigationScope scope) { if (scope == null || !_scopeSet.Remove(scope)) { return; } if (_topScope == scope) { _topScope = null; } scope.SetNavigationSuppressed(false); _scopes.Remove(scope); MarkDiscoveryDirty(); } internal void MarkDiscoveryDirty() { _discoveryDirty = true; } private void Update() { TryBindUIRoot(); if (_uiCanvasRoot == null) { return; } if (_discoveryDirty || Time.unscaledTime >= _nextDiscoveryTime) { DiscoverScopes(); } UXNavigationScope newTopScope = FindTopScope(); if (!ReferenceEquals(_topScope, newTopScope)) { _topScope = newTopScope; } ApplyScopeSuppression(); if (UXInputModeService.CurrentMode == UXInputMode.Gamepad) { EnsureGamepadSelection(); if (_topScope != null) { Cursor.visible = false; } } TrackSelection(); } private void TryBindUIRoot() { if (_uiCanvasRoot != null) { return; } IUIModule uiModule = ModuleSystem.GetModule(); if (uiModule?.UICanvasRoot == null) { return; } _uiCanvasRoot = uiModule.UICanvasRoot; CacheInteractiveLayers(); DiscoverScopes(); } private void CacheInteractiveLayers() { _interactiveLayerRoots.Clear(); if (_uiCanvasRoot == null) { return; } string cacheLayerName = $"Layer{(int)UILayer.All}-{UILayer.All}"; for (int i = 0; i < _uiCanvasRoot.childCount; i++) { Transform child = _uiCanvasRoot.GetChild(i); if (child == null || child.name == cacheLayerName) { continue; } _interactiveLayerRoots.Add(child); if (child.GetComponent() == null) { var watcher = child.gameObject.AddComponent(); watcher.Initialize(this); } } } private void DiscoverScopes() { _discoveryDirty = false; _nextDiscoveryTime = Time.unscaledTime + DiscoveryInterval; CacheInteractiveLayers(); bool addedScope = false; foreach (Transform layerRoot in _interactiveLayerRoots) { if (layerRoot == null || IsNavigationSkipped(layerRoot)) { continue; } UIHolderObjectBase[] holders = layerRoot.GetComponentsInChildren(true); for (int i = 0; i < holders.Length; i++) { UIHolderObjectBase holder = holders[i]; if (holder == null || holder.GetComponent() != null || IsNavigationSkipped(holder.transform)) { continue; } holder.gameObject.AddComponent(); addedScope = true; } } if (addedScope) { for (int i = 0; i < _scopes.Count; i++) { _scopes[i]?.InvalidateSelectableCache(); } } } private UXNavigationScope FindTopScope() { UXNavigationScope bestScope = null; for (int i = 0; i < _scopes.Count; i++) { UXNavigationScope scope = _scopes[i]; if (scope == null) { continue; } bool available = IsScopeAvailable(scope); if (scope.WasAvailable != available) { scope.WasAvailable = available; if (available) { scope.ActivationSerial = ++_activationSerial; } } if (!available) { continue; } if (bestScope == null || CompareScopePriority(scope, bestScope) < 0) { bestScope = scope; } } return bestScope; } private bool IsScopeAvailable(UXNavigationScope scope) { if (scope == null || !scope.isActiveAndEnabled || !scope.gameObject.activeInHierarchy) { return false; } Canvas canvas = scope.Canvas; if (canvas == null || canvas.gameObject.layer != UIComponent.UIShowLayer) { return false; } return !IsNavigationSkipped(scope.transform) && scope.HasAvailableSelectable() && TryGetInteractiveLayerRoot(scope.transform, out _); } private static bool IsNavigationSkipped(Transform current) { return current != null && current.GetComponentInParent(true) != null; } private bool TryGetInteractiveLayerRoot(Transform current, out Transform layerRoot) { layerRoot = null; if (current == null || _uiCanvasRoot == null) { return false; } while (current != null) { if (current.parent == _uiCanvasRoot) { layerRoot = current; return _interactiveLayerRoots.Contains(current); } current = current.parent; } return false; } private bool IsHolderOwnedByScope(UIHolderObjectBase holder, UXNavigationScope scope) { if (holder == null || scope == null) { return false; } UXNavigationScope nearestScope = holder.GetComponent(); if (nearestScope == null) { nearestScope = holder.GetComponentInParent(true); } return nearestScope == scope; } private void ApplyScopeSuppression() { for (int i = 0; i < _scopes.Count; i++) { UXNavigationScope scope = _scopes[i]; if (scope == null) { continue; } bool suppress = IsScopeAvailable(scope) && _topScope != null && !ReferenceEquals(scope, _topScope) && _topScope.BlockLowerScopes && CompareScopePriority(_topScope, scope) < 0; scope.SetNavigationSuppressed(suppress); } } private void EnsureGamepadSelection() { EventSystem eventSystem = EventSystem.current; if (eventSystem == null) { if (!_missingEventSystemLogged) { Debug.LogWarning("UXNavigationRuntime requires an active EventSystem for gamepad navigation."); _missingEventSystemLogged = true; } return; } _missingEventSystemLogged = false; if (_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); _lastObservedSelection = eventSystem.currentSelectedGameObject; if (_lastObservedSelection != null) { _topScope.RecordSelection(_lastObservedSelection); } } private void TrackSelection() { EventSystem eventSystem = EventSystem.current; if (eventSystem == null) { _lastObservedSelection = null; return; } GameObject currentSelected = eventSystem.currentSelectedGameObject; if (ReferenceEquals(_lastObservedSelection, currentSelected)) { return; } _lastObservedSelection = currentSelected; if (_topScope != null && _topScope.IsSelectableOwnedAndValid(currentSelected)) { _topScope.RecordSelection(currentSelected); } } private void OnInputModeChanged(UXInputMode mode) { EventSystem eventSystem = EventSystem.current; if (mode == UXInputMode.Pointer) { if (_topScope != null) { Cursor.visible = true; if (eventSystem != null) { eventSystem.SetSelectedGameObject(null); } } _lastObservedSelection = null; return; } if (_topScope != null) { Cursor.visible = false; } EnsureGamepadSelection(); } private static int CompareScopePriority(UXNavigationScope left, UXNavigationScope right) { int leftOrder = left.Canvas != null ? left.Canvas.sortingOrder : int.MinValue; int rightOrder = right.Canvas != null ? right.Canvas.sortingOrder : int.MinValue; int orderCompare = rightOrder.CompareTo(leftOrder); if (orderCompare != 0) { return orderCompare; } int hierarchyCompare = right.GetHierarchyDepth().CompareTo(left.GetHierarchyDepth()); if (hierarchyCompare != 0) { return hierarchyCompare; } return right.ActivationSerial.CompareTo(left.ActivationSerial); } } } #endif