2026-03-26 16:12:50 +08:00
|
|
|
#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
|
2026-03-20 16:46:06 +08:00
|
|
|
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<UXNavigationScope> _scopeSet = new();
|
|
|
|
|
private readonly List<UXNavigationScope> _scopes = new(32);
|
|
|
|
|
private readonly HashSet<Transform> _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<UXNavigationRuntime>();
|
|
|
|
|
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<IUIModule>();
|
|
|
|
|
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<UXNavigationLayerWatcher>() == null)
|
|
|
|
|
{
|
|
|
|
|
var watcher = child.gameObject.AddComponent<UXNavigationLayerWatcher>();
|
|
|
|
|
watcher.Initialize(this);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void DiscoverScopes()
|
|
|
|
|
{
|
|
|
|
|
_discoveryDirty = false;
|
|
|
|
|
_nextDiscoveryTime = Time.unscaledTime + DiscoveryInterval;
|
|
|
|
|
CacheInteractiveLayers();
|
|
|
|
|
bool addedScope = false;
|
|
|
|
|
|
|
|
|
|
foreach (Transform layerRoot in _interactiveLayerRoots)
|
|
|
|
|
{
|
2026-03-20 19:40:12 +08:00
|
|
|
if (layerRoot == null || IsNavigationSkipped(layerRoot))
|
2026-03-20 16:46:06 +08:00
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIHolderObjectBase[] holders = layerRoot.GetComponentsInChildren<UIHolderObjectBase>(true);
|
|
|
|
|
for (int i = 0; i < holders.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
UIHolderObjectBase holder = holders[i];
|
2026-03-20 19:40:12 +08:00
|
|
|
if (holder == null
|
|
|
|
|
|| holder.GetComponent<UXNavigationScope>() != null
|
|
|
|
|
|| IsNavigationSkipped(holder.transform))
|
2026-03-20 16:46:06 +08:00
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
holder.gameObject.AddComponent<UXNavigationScope>();
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 19:40:12 +08:00
|
|
|
return !IsNavigationSkipped(scope.transform)
|
|
|
|
|
&& scope.HasAvailableSelectable()
|
|
|
|
|
&& TryGetInteractiveLayerRoot(scope.transform, out _);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool IsNavigationSkipped(Transform current)
|
|
|
|
|
{
|
|
|
|
|
return current != null && current.GetComponentInParent<UXNavigationSkip>(true) != null;
|
2026-03-20 16:46:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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<UXNavigationScope>();
|
|
|
|
|
if (nearestScope == null)
|
|
|
|
|
{
|
|
|
|
|
nearestScope = holder.GetComponentInParent<UXNavigationScope>(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
|