#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION using AlicizaX.UI.Runtime; using UnityEngine.EventSystems; namespace UnityEngine.UI { [DisallowMultipleComponent] [AddComponentMenu("UI/UX Navigation Scope")] public sealed class UXNavigationScope : MonoBehaviour, ISelectHandler { private const int InvalidIndex = -1; private const int DefaultRuntimeSelectableCapacity = 16; [SerializeField, Header("默认选中控件")] private Selectable _defaultSelectable; [SerializeField, Header("编辑器烘焙导航控件")] private Selectable[] _bakedSelectables = System.Array.Empty(); [SerializeField, Header("动态控件容量")] private int _runtimeSelectableCapacity = DefaultRuntimeSelectableCapacity; [SerializeField, Header("记住上次选中")] private bool _rememberLastSelection = true; [SerializeField, Header("手柄/键盘模式必须有选中")] private bool _requireSelectionWhenGamepad = true; [SerializeField, Header("阻断下层导航域")] private bool _blockLowerScopes = true; [SerializeField, Header("自动选中首个可用控件")] private bool _autoSelectFirstAvailable = true; private Selectable[] _runtimeSelectables; private Navigation[] _bakedBaselineNavigation; private Navigation[] _runtimeBaselineNavigation; private Canvas _cachedCanvas; private UIHolderObjectBase _cachedHolder; private Selectable _lastSelected; private bool _navigationSuppressed; private int _cachedHierarchyDepth = -1; private bool _cachedIsSkipped; private bool _isSkippedCacheValid; private int _runtimeSelectableCount; internal int RuntimeIndex { get; set; } = InvalidIndex; internal ulong ActivationSerial { get; set; } internal bool IsAvailable { get; set; } internal bool WasAvailable { get; set; } public bool NavigationSuppressed => _navigationSuppressed; internal int BakedSelectableCount => _bakedSelectables != null ? _bakedSelectables.Length : 0; public int RuntimeSelectableCount => _runtimeSelectableCount; public Selectable DefaultSelectable { get => _defaultSelectable; set { _defaultSelectable = value; MarkRuntimeStateDirty(); } } public bool RememberLastSelection => _rememberLastSelection; public bool RequireSelectionWhenGamepad => _requireSelectionWhenGamepad; public bool BlockLowerScopes => _blockLowerScopes; internal bool IsNavigationSkipped { get { if (!_isSkippedCacheValid) { _cachedIsSkipped = GetComponentInParent(true) != null; _isSkippedCacheValid = true; } return _cachedIsSkipped; } } internal Canvas Canvas { get { if (_cachedCanvas == null) { _cachedCanvas = GetComponentInParent(true); } return _cachedCanvas; } } internal UIHolderObjectBase Holder { get { if (_cachedHolder == null) { _cachedHolder = GetComponent(); if (_cachedHolder == null) { _cachedHolder = GetComponentInParent(true); } } return _cachedHolder; } } private void Awake() { EnsureRuntimeBuffers(); CaptureAllBaselines(); } private void OnEnable() { EnsureRuntimeBuffers(); CaptureAllBaselines(); UXNavigationRuntime.EnsureInstance().RegisterScope(this); } private void OnDisable() { SetNavigationSuppressed(false); if (UXNavigationRuntime.TryGetInstance(out var runtime)) { runtime.UnregisterScope(this); } } private void OnDestroy() { SetNavigationSuppressed(false); } private void OnTransformChildrenChanged() { MarkRuntimeStateDirty(); } private void OnTransformParentChanged() { _cachedCanvas = null; _cachedHolder = null; _cachedHierarchyDepth = -1; _isSkippedCacheValid = false; MarkRuntimeStateDirty(); } #if UNITY_EDITOR private void OnValidate() { if (_runtimeSelectableCapacity < 0) { _runtimeSelectableCapacity = 0; } } #endif public bool RegisterSelectable(Selectable selectable) { if (selectable == null || !Owns(selectable.gameObject) || ContainsSelectable(selectable)) { return false; } EnsureRuntimeBuffers(); if (_runtimeSelectableCount >= _runtimeSelectables.Length) { ReportCapacityExceeded(); return false; } _runtimeSelectables[_runtimeSelectableCount] = selectable; _runtimeBaselineNavigation[_runtimeSelectableCount] = selectable.navigation; _runtimeSelectableCount++; if (_navigationSuppressed) { SetSelectableSuppressed(selectable, true); } MarkRuntimeStateDirty(); return true; } public bool UnregisterSelectable(Selectable selectable) { if (selectable == null || _runtimeSelectables == null) { return false; } for (int i = 0; i < _runtimeSelectableCount; i++) { if (_runtimeSelectables[i] != selectable) { continue; } if (_navigationSuppressed) { selectable.navigation = _runtimeBaselineNavigation[i]; } int last = _runtimeSelectableCount - 1; _runtimeSelectables[i] = _runtimeSelectables[last]; _runtimeBaselineNavigation[i] = _runtimeBaselineNavigation[last]; _runtimeSelectables[last] = null; _runtimeBaselineNavigation[last] = default(Navigation); _runtimeSelectableCount--; if (_lastSelected == selectable) { _lastSelected = null; } MarkRuntimeStateDirty(); return true; } return false; } public void InvalidateSelectableCache() { CaptureAllBaselines(); MarkRuntimeStateDirty(); } public void OnSelect(BaseEventData eventData) { GameObject selectedObject = eventData != null ? eventData.selectedObject : null; UXNavigationRuntime.NotifySelection(selectedObject); } internal void InvalidateSkipCacheOnly() { _isSkippedCacheValid = false; } internal int GetHierarchyDepth() { if (_cachedHierarchyDepth >= 0) { return _cachedHierarchyDepth; } int depth = 0; Transform current = transform; while (current != null) { depth++; current = current.parent; } _cachedHierarchyDepth = depth; return _cachedHierarchyDepth; } internal bool Owns(GameObject target) { if (target == null) { return false; } UXNavigationScope nearestScope = target.GetComponentInParent(true); return nearestScope == this; } internal Selectable GetPreferredSelectable() { if (_rememberLastSelection && IsSelectableValid(_lastSelected)) { return _lastSelected; } if (IsSelectableValid(_defaultSelectable)) { return _defaultSelectable; } if (!_autoSelectFirstAvailable) { return null; } Selectable selectable = FirstUsable(_bakedSelectables, BakedSelectableCount); if (selectable != null) { return selectable; } return FirstUsable(_runtimeSelectables, _runtimeSelectableCount); } internal bool HasAvailableSelectable() { return IsSelectableValid(_defaultSelectable) || FirstUsable(_bakedSelectables, BakedSelectableCount) != null || FirstUsable(_runtimeSelectables, _runtimeSelectableCount) != null; } internal void RecordSelection(GameObject selectedObject) { if (!_rememberLastSelection || selectedObject == null) { return; } Selectable selectable = GetSelectableFromObject(selectedObject); if (IsSelectableValid(selectable)) { _lastSelected = selectable; } } internal bool IsSelectableOwnedAndValid(GameObject selectedObject) { return IsSelectableValid(GetSelectableFromObject(selectedObject)); } internal void SetNavigationSuppressed(bool suppressed) { if (_navigationSuppressed == suppressed) { return; } CaptureAllBaselines(); _navigationSuppressed = suppressed; ApplySuppression(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount, suppressed); ApplySuppression(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount, suppressed); } private void EnsureRuntimeBuffers() { int capacity = _runtimeSelectableCapacity > 0 ? _runtimeSelectableCapacity : 0; if (_runtimeSelectables == null || _runtimeSelectables.Length != capacity) { _runtimeSelectables = capacity > 0 ? new Selectable[capacity] : System.Array.Empty(); _runtimeBaselineNavigation = capacity > 0 ? new Navigation[capacity] : System.Array.Empty(); _runtimeSelectableCount = 0; } int bakedCount = BakedSelectableCount; if (_bakedBaselineNavigation == null || _bakedBaselineNavigation.Length != bakedCount) { _bakedBaselineNavigation = bakedCount > 0 ? new Navigation[bakedCount] : System.Array.Empty(); } } private void CaptureAllBaselines() { EnsureRuntimeBuffers(); CaptureBaseline(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount); CaptureBaseline(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount); } private static void CaptureBaseline(Selectable[] selectables, Navigation[] baseline, int count) { if (selectables == null || baseline == null) { return; } for (int i = 0; i < count; i++) { Selectable selectable = selectables[i]; if (selectable != null) { baseline[i] = selectable.navigation; } } } private static void ApplySuppression(Selectable[] selectables, Navigation[] baseline, int count, bool suppressed) { if (selectables == null || baseline == null) { return; } for (int i = 0; i < count; i++) { Selectable selectable = selectables[i]; if (selectable == null) { continue; } if (suppressed) { SetSelectableSuppressed(selectable, true); } else { selectable.navigation = baseline[i]; } } } private static void SetSelectableSuppressed(Selectable selectable, bool suppressed) { if (selectable == null || !suppressed) { return; } Navigation navigation = selectable.navigation; navigation.mode = Navigation.Mode.None; selectable.navigation = navigation; } private bool ContainsSelectable(Selectable selectable) { return IndexOf(_bakedSelectables, BakedSelectableCount, selectable) >= 0 || IndexOf(_runtimeSelectables, _runtimeSelectableCount, selectable) >= 0; } private bool IsSelectableValid(Selectable selectable) { return IsSelectableUsable(selectable) && ContainsSelectable(selectable); } private static Selectable FirstUsable(Selectable[] selectables, int count) { if (selectables == null) { return null; } for (int i = 0; i < count; i++) { Selectable selectable = selectables[i]; if (IsSelectableUsable(selectable)) { return selectable; } } return null; } private static int IndexOf(Selectable[] selectables, int count, Selectable selectable) { if (selectables == null || selectable == null) { return InvalidIndex; } for (int i = 0; i < count; i++) { if (selectables[i] == selectable) { return i; } } return InvalidIndex; } private static bool IsSelectableUsable(Selectable selectable) { return selectable != null && selectable.IsActive() && selectable.IsInteractable(); } private static Selectable GetSelectableFromObject(GameObject selectedObject) { if (selectedObject == null) { return null; } return selectedObject.TryGetComponent(out Selectable selectable) ? selectable : selectedObject.GetComponentInParent(); } private void MarkRuntimeStateDirty() { if (UXNavigationRuntime.TryGetInstance(out var runtime)) { runtime.MarkStateDirty(); } } private static void ReportCapacityExceeded() { #if UNITY_EDITOR || DEVELOPMENT_BUILD Debug.LogError("UXNavigationScope runtime selectable capacity exceeded."); #endif } } } #endif