com.alicizax.unity.ui.exten.../Runtime/UXComponent/Navigation/UXNavigationRuntime.cs

407 lines
12 KiB
C#

#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<UXNavigationRuntime>();
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<UXNavigationScope>();
if (scope == null)
{
scope = holder.GetComponentInParent<UXNavigationScope>(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