com.alicizax.unity.ui.exten.../Runtime/UXComponent/Navigation/UXNavigationRuntime.cs
2026-04-15 14:23:30 +08:00

511 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
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 static readonly string CacheLayerName = $"Layer{(int)UILayer.All}-{UILayer.All}";
private readonly HashSet<UXNavigationScope> _scopeSet = new();
private readonly List<UXNavigationScope> _scopes = new(32);
private readonly HashSet<Transform> _interactiveLayerRoots = new();
private readonly List<UIHolderObjectBase> _holderBuffer = new(32);
private Transform _uiCanvasRoot;
private UXNavigationScope _topScope;
private GameObject _lastObservedSelection;
private bool _discoveryDirty = true;
private ulong _activationSerial;
private bool _missingEventSystemLogged;
private bool _topScopeDirty = true;
private bool _suppressionDirty = true;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void Bootstrap()
{
if (AppServices.App == null || AppServices.App.Require<IUIService>() == null) return;
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);
_topScopeDirty = true;
_suppressionDirty = true;
}
internal void UnregisterScope(UXNavigationScope scope)
{
if (scope == null || !_scopeSet.Remove(scope))
{
return;
}
if (_topScope == scope)
{
_topScope = null;
}
scope.IsAvailable = false;
scope.SetNavigationSuppressed(false);
int idx = _scopes.IndexOf(scope);
if (idx >= 0)
{
int last = _scopes.Count - 1;
_scopes[idx] = _scopes[last];
_scopes.RemoveAt(last);
}
_topScopeDirty = true;
_suppressionDirty = true;
}
internal void MarkDiscoveryDirty()
{
_discoveryDirty = true;
}
private void Update()
{
TryBindUIRoot();
if (_uiCanvasRoot == null)
{
return;
}
if (_discoveryDirty)
{
DiscoverScopes();
}
if (_topScopeDirty)
{
UXNavigationScope newTopScope = FindTopScope();
_topScopeDirty = false;
if (!ReferenceEquals(_topScope, newTopScope))
{
_topScope = newTopScope;
_suppressionDirty = true;
}
}
if (_suppressionDirty)
{
ApplyScopeSuppression();
_suppressionDirty = false;
}
if (UXInputModeService.CurrentMode == UXInputMode.Gamepad)
{
EnsureGamepadSelection();
if (_topScope != null)
{
Cursor.visible = false;
}
}
TrackSelection();
}
private void TryBindUIRoot()
{
if (_uiCanvasRoot != null)
{
return;
}
IUIService uiModule = AppServices.App.Require<IUIService>();
if (uiModule?.UICanvasRoot == null)
{
return;
}
_uiCanvasRoot = uiModule.UICanvasRoot;
EnsureWatcher(_uiCanvasRoot);
CacheInteractiveLayers();
DiscoverScopes();
}
private void CacheInteractiveLayers()
{
_interactiveLayerRoots.Clear();
if (_uiCanvasRoot == null)
{
return;
}
EnsureWatcher(_uiCanvasRoot);
for (int i = 0; i < _uiCanvasRoot.childCount; i++)
{
Transform child = _uiCanvasRoot.GetChild(i);
if (child == null || child.name == CacheLayerName)
{
continue;
}
_interactiveLayerRoots.Add(child);
EnsureWatcher(child);
}
}
private void DiscoverScopes()
{
_discoveryDirty = false;
CacheInteractiveLayers();
bool addedScope = false;
foreach (Transform layerRoot in _interactiveLayerRoots)
{
if (layerRoot == null || IsNavigationSkipped(layerRoot))
{
continue;
}
_holderBuffer.Clear();
layerRoot.GetComponentsInChildren(true, _holderBuffer);
for (int i = 0; i < _holderBuffer.Count; i++)
{
UIHolderObjectBase holder = _holderBuffer[i];
if (holder == null
|| holder.GetComponent<UXNavigationScope>() != null
|| IsNavigationSkipped(holder.transform))
{
continue;
}
holder.gameObject.AddComponent<UXNavigationScope>();
addedScope = true;
}
}
_holderBuffer.Clear();
if (addedScope)
{
for (int i = 0; i < _scopes.Count; i++)
{
_scopes[i]?.InvalidateSelectableCache();
}
_topScopeDirty = true;
_suppressionDirty = true;
}
}
private void EnsureWatcher(Transform target)
{
if (target == null)
{
return;
}
if (!target.TryGetComponent(out UXNavigationLayerWatcher watcher))
{
watcher = target.gameObject.AddComponent<UXNavigationLayerWatcher>();
}
watcher.Initialize(this);
}
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);
scope.IsAvailable = available;
if (scope.WasAvailable != available)
{
scope.WasAvailable = available;
if (available)
{
scope.ActivationSerial = ++_activationSerial;
}
_suppressionDirty = true;
}
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 !scope.IsNavigationSkipped
&& scope.HasAvailableSelectable()
&& TryGetInteractiveLayerRoot(scope.transform, out _);
}
// 保留静态方法用于 DiscoverScopes 中对 LayerRoot / Holder 节点的检测
// 这些节点无 UXNavigationScope调用频次低仅在 dirty 时),无需缓存
private static bool IsNavigationSkipped(Transform current)
{
return current != null && current.GetComponentInParent<UXNavigationSkip>(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<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 = scope.IsAvailable
&& _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