From 756be161fd7767087092f01db4c1c1d353cfded3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=80=9D=E6=B5=B7?= <1464576565@qq.com> Date: Fri, 20 Mar 2026 16:46:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0UXNavigation=20=E7=94=A8?= =?UTF-8?q?=E4=BA=8E=E9=80=82=E9=85=8D=E6=89=8B=E6=9F=84=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=20=E9=80=82=E9=85=8DUI=E6=A1=86=E6=9E=B6=20Scope=E6=9E=B6?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Hotkey/UXHotkeyRegisterManager.cs | 6 +- Runtime/UXComponent/Navigation.meta | 8 + Runtime/UXComponent/Navigation/UXInputMode.cs | 10 + .../Navigation/UXInputMode.cs.meta | 11 + .../Navigation/UXInputModeService.cs | 169 +++++++ .../Navigation/UXInputModeService.cs.meta | 11 + .../Navigation/UXNavigationLayerWatcher.cs | 20 + .../UXNavigationLayerWatcher.cs.meta | 11 + .../Navigation/UXNavigationRuntime.cs | 455 ++++++++++++++++++ .../Navigation/UXNavigationRuntime.cs.meta | 11 + .../Navigation/UXNavigationScope.cs | 302 ++++++++++++ .../Navigation/UXNavigationScope.cs.meta | 11 + 12 files changed, 1023 insertions(+), 2 deletions(-) create mode 100644 Runtime/UXComponent/Navigation.meta create mode 100644 Runtime/UXComponent/Navigation/UXInputMode.cs create mode 100644 Runtime/UXComponent/Navigation/UXInputMode.cs.meta create mode 100644 Runtime/UXComponent/Navigation/UXInputModeService.cs create mode 100644 Runtime/UXComponent/Navigation/UXInputModeService.cs.meta create mode 100644 Runtime/UXComponent/Navigation/UXNavigationLayerWatcher.cs create mode 100644 Runtime/UXComponent/Navigation/UXNavigationLayerWatcher.cs.meta create mode 100644 Runtime/UXComponent/Navigation/UXNavigationRuntime.cs create mode 100644 Runtime/UXComponent/Navigation/UXNavigationRuntime.cs.meta create mode 100644 Runtime/UXComponent/Navigation/UXNavigationScope.cs create mode 100644 Runtime/UXComponent/Navigation/UXNavigationScope.cs.meta diff --git a/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs b/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs index bcc389d..b2a8e45 100644 --- a/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs +++ b/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs @@ -477,7 +477,7 @@ namespace UnityEngine.UI foreach (var scope in _scopes.Values) { - if (!IsScopeActive(scope)) + if (!IsScopeActive(scope) || !UXNavigationRuntime.IsHolderWithinTopScope(scope.Holder)) { continue; } @@ -492,7 +492,9 @@ namespace UnityEngine.UI foreach (var scope in _scopes.Values) { - if (IsScopeActive(scope) && !_ancestorHolders.Contains(scope.Holder)) + if (IsScopeActive(scope) + && UXNavigationRuntime.IsHolderWithinTopScope(scope.Holder) + && !_ancestorHolders.Contains(scope.Holder)) { _leafScopes.Add(scope); } diff --git a/Runtime/UXComponent/Navigation.meta b/Runtime/UXComponent/Navigation.meta new file mode 100644 index 0000000..e711f12 --- /dev/null +++ b/Runtime/UXComponent/Navigation.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 454112a9b68b5d140aa6ba4085af80a8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UXComponent/Navigation/UXInputMode.cs b/Runtime/UXComponent/Navigation/UXInputMode.cs new file mode 100644 index 0000000..f3d8674 --- /dev/null +++ b/Runtime/UXComponent/Navigation/UXInputMode.cs @@ -0,0 +1,10 @@ +#if INPUTSYSTEM_SUPPORT +namespace UnityEngine.UI +{ + public enum UXInputMode : byte + { + Pointer = 0, + Gamepad = 1 + } +} +#endif diff --git a/Runtime/UXComponent/Navigation/UXInputMode.cs.meta b/Runtime/UXComponent/Navigation/UXInputMode.cs.meta new file mode 100644 index 0000000..34682ce --- /dev/null +++ b/Runtime/UXComponent/Navigation/UXInputMode.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b1d6174ce87303c419a2ca88b7d1fcc8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UXComponent/Navigation/UXInputModeService.cs b/Runtime/UXComponent/Navigation/UXInputModeService.cs new file mode 100644 index 0000000..4c87e48 --- /dev/null +++ b/Runtime/UXComponent/Navigation/UXInputModeService.cs @@ -0,0 +1,169 @@ +#if INPUTSYSTEM_SUPPORT +using System; +using UnityEngine.InputSystem; +using UnityEngine.InputSystem.Controls; + +namespace UnityEngine.UI +{ + internal sealed class UXInputModeService : MonoBehaviour + { + private static UXInputModeService _instance; + + private InputAction _pointerAction; + private InputAction _gamepadAction; + + public static UXInputMode CurrentMode { get; private set; } = UXInputMode.Pointer; + + public static event Action OnModeChanged; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] + private static void Bootstrap() + { + EnsureInstance(); + } + + internal static UXInputModeService EnsureInstance() + { + if (_instance != null) + { + return _instance; + } + + var go = new GameObject("[UXInputModeService]"); + go.hideFlags = HideFlags.HideAndDontSave; + DontDestroyOnLoad(go); + _instance = go.AddComponent(); + return _instance; + } + + private void Awake() + { + if (_instance != null && _instance != this) + { + Destroy(gameObject); + return; + } + + _instance = this; + DontDestroyOnLoad(gameObject); + hideFlags = HideFlags.HideAndDontSave; + } + + private void OnEnable() + { + CreateActions(); + } + + private void OnDisable() + { + DisposeActions(); + } + + private void OnDestroy() + { + DisposeActions(); + if (_instance == this) + { + _instance = null; + } + } + + private void CreateActions() + { + if (_pointerAction != null || _gamepadAction != null) + { + return; + } + + _pointerAction = new InputAction("UXPointerInput", InputActionType.PassThrough); + _pointerAction.AddBinding("/anyKey"); + _pointerAction.AddBinding("/delta"); + _pointerAction.AddBinding("/scroll"); + _pointerAction.AddBinding("/leftButton"); + _pointerAction.AddBinding("/rightButton"); + _pointerAction.AddBinding("/middleButton"); + _pointerAction.performed += OnPointerInput; + _pointerAction.Enable(); + + _gamepadAction = new InputAction("UXGamepadInput", InputActionType.PassThrough); + _gamepadAction.AddBinding("/*"); + _gamepadAction.AddBinding("/*"); + _gamepadAction.performed += OnGamepadInput; + _gamepadAction.Enable(); + } + + private void DisposeActions() + { + if (_pointerAction != null) + { + _pointerAction.performed -= OnPointerInput; + _pointerAction.Disable(); + _pointerAction.Dispose(); + _pointerAction = null; + } + + if (_gamepadAction != null) + { + _gamepadAction.performed -= OnGamepadInput; + _gamepadAction.Disable(); + _gamepadAction.Dispose(); + _gamepadAction = null; + } + } + + private static void OnPointerInput(InputAction.CallbackContext context) + { + if (!IsInputMeaningful(context.control)) + { + return; + } + + SetMode(UXInputMode.Pointer); + } + + private static void OnGamepadInput(InputAction.CallbackContext context) + { + if (!IsInputMeaningful(context.control)) + { + return; + } + + SetMode(UXInputMode.Gamepad); + } + + private static bool IsInputMeaningful(InputControl control) + { + if (control == null || control.device == null || control.synthetic) + { + return false; + } + + switch (control) + { + case ButtonControl button: + return button.IsPressed(); + case StickControl stick: + return stick.ReadValue().sqrMagnitude >= 0.04f; + case Vector2Control vector2: + return vector2.ReadValue().sqrMagnitude >= 0.04f; + case AxisControl axis: + return Mathf.Abs(axis.ReadValue()) >= 0.2f; + default: + return !control.noisy; + } + } + + internal static void SetMode(UXInputMode mode) + { + EnsureInstance(); + if (CurrentMode == mode) + { + return; + } + + CurrentMode = mode; + OnModeChanged?.Invoke(mode); + } + } +} +#endif diff --git a/Runtime/UXComponent/Navigation/UXInputModeService.cs.meta b/Runtime/UXComponent/Navigation/UXInputModeService.cs.meta new file mode 100644 index 0000000..379c0a4 --- /dev/null +++ b/Runtime/UXComponent/Navigation/UXInputModeService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ca8818b35acd2324198177d2fd3f359b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UXComponent/Navigation/UXNavigationLayerWatcher.cs b/Runtime/UXComponent/Navigation/UXNavigationLayerWatcher.cs new file mode 100644 index 0000000..fb7d8ff --- /dev/null +++ b/Runtime/UXComponent/Navigation/UXNavigationLayerWatcher.cs @@ -0,0 +1,20 @@ +#if INPUTSYSTEM_SUPPORT +namespace UnityEngine.UI +{ + [DisallowMultipleComponent] + internal sealed class UXNavigationLayerWatcher : MonoBehaviour + { + private UXNavigationRuntime _runtime; + + internal void Initialize(UXNavigationRuntime runtime) + { + _runtime = runtime; + } + + private void OnTransformChildrenChanged() + { + _runtime?.MarkDiscoveryDirty(); + } + } +} +#endif diff --git a/Runtime/UXComponent/Navigation/UXNavigationLayerWatcher.cs.meta b/Runtime/UXComponent/Navigation/UXNavigationLayerWatcher.cs.meta new file mode 100644 index 0000000..97da493 --- /dev/null +++ b/Runtime/UXComponent/Navigation/UXNavigationLayerWatcher.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0268df3dd46bb194fa4ae7ec48be7702 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UXComponent/Navigation/UXNavigationRuntime.cs b/Runtime/UXComponent/Navigation/UXNavigationRuntime.cs new file mode 100644 index 0000000..ebc6e1a --- /dev/null +++ b/Runtime/UXComponent/Navigation/UXNavigationRuntime.cs @@ -0,0 +1,455 @@ +#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) + { + continue; + } + + UIHolderObjectBase[] holders = layerRoot.GetComponentsInChildren(true); + for (int i = 0; i < holders.Length; i++) + { + UIHolderObjectBase holder = holders[i]; + if (holder == null || holder.GetComponent() != null) + { + 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 TryGetInteractiveLayerRoot(scope.transform, out _); + } + + 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 diff --git a/Runtime/UXComponent/Navigation/UXNavigationRuntime.cs.meta b/Runtime/UXComponent/Navigation/UXNavigationRuntime.cs.meta new file mode 100644 index 0000000..d2a50c8 --- /dev/null +++ b/Runtime/UXComponent/Navigation/UXNavigationRuntime.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4d755cbf1369cfc4288d7b1261c523bf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UXComponent/Navigation/UXNavigationScope.cs b/Runtime/UXComponent/Navigation/UXNavigationScope.cs new file mode 100644 index 0000000..9468a8e --- /dev/null +++ b/Runtime/UXComponent/Navigation/UXNavigationScope.cs @@ -0,0 +1,302 @@ +#if INPUTSYSTEM_SUPPORT +using System.Collections.Generic; +using AlicizaX.UI.Runtime; + +namespace UnityEngine.UI +{ + [DisallowMultipleComponent] + [AddComponentMenu("UI/UX Navigation Scope")] + public sealed class UXNavigationScope : MonoBehaviour + { + [SerializeField, Header("默认选中控件")] private Selectable _defaultSelectable; + + [SerializeField, Header("显式导航控件列表")] private List _explicitSelectables = new(); + + [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 readonly List _cachedSelectables = new(16); + private readonly Dictionary _baselineNavigation = new(); + private readonly List _removeBuffer = new(8); + + private Canvas _cachedCanvas; + private UIHolderObjectBase _cachedHolder; + private Selectable _lastSelected; + private bool _cacheDirty = true; + private bool _navigationSuppressed; + + internal ulong ActivationSerial { get; set; } + internal bool WasAvailable { get; set; } + + public Selectable DefaultSelectable + { + get => _defaultSelectable; + set => _defaultSelectable = value; + } + + public bool RememberLastSelection => _rememberLastSelection; + public bool RequireSelectionWhenGamepad => _requireSelectionWhenGamepad; + public bool BlockLowerScopes => _blockLowerScopes; + + 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 OnEnable() + { + _cacheDirty = true; + 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() + { + _cacheDirty = true; + } + +#if UNITY_EDITOR + private void OnValidate() + { + _cacheDirty = true; + } +#endif + + internal int GetHierarchyDepth() + { + int depth = 0; + Transform current = transform; + while (current != null) + { + depth++; + current = current.parent; + } + + return depth; + } + + internal bool Owns(GameObject target) + { + if (target == null) + { + return false; + } + + var nearestScope = target.GetComponentInParent(true); + return nearestScope == this; + } + + internal Selectable GetPreferredSelectable() + { + RefreshSelectableCache(); + + if (_rememberLastSelection && IsSelectableValid(_lastSelected)) + { + return _lastSelected; + } + + if (IsSelectableValid(_defaultSelectable)) + { + return _defaultSelectable; + } + + if (!_autoSelectFirstAvailable) + { + return null; + } + + for (int i = 0; i < _cachedSelectables.Count; i++) + { + Selectable selectable = _cachedSelectables[i]; + if (IsSelectableValid(selectable)) + { + return selectable; + } + } + + return null; + } + + internal void RecordSelection(GameObject selectedObject) + { + if (!_rememberLastSelection || selectedObject == null) + { + return; + } + + Selectable selectable = selectedObject.GetComponent(); + if (selectable == null) + { + selectable = selectedObject.GetComponentInParent(); + } + + if (IsSelectableValid(selectable)) + { + _lastSelected = selectable; + } + } + + internal bool IsSelectableOwnedAndValid(GameObject selectedObject) + { + if (selectedObject == null || !Owns(selectedObject)) + { + return false; + } + + Selectable selectable = selectedObject.GetComponent(); + if (selectable == null) + { + selectable = selectedObject.GetComponentInParent(); + } + + return IsSelectableValid(selectable); + } + + internal void SetNavigationSuppressed(bool suppressed) + { + RefreshSelectableCache(); + if (_navigationSuppressed == suppressed) + { + return; + } + + _navigationSuppressed = suppressed; + for (int i = 0; i < _cachedSelectables.Count; i++) + { + Selectable selectable = _cachedSelectables[i]; + if (selectable == null) + { + continue; + } + + if (!_baselineNavigation.ContainsKey(selectable)) + { + _baselineNavigation[selectable] = selectable.navigation; + } + + if (suppressed) + { + Navigation navigation = selectable.navigation; + navigation.mode = Navigation.Mode.None; + selectable.navigation = navigation; + } + else if (_baselineNavigation.TryGetValue(selectable, out Navigation navigation)) + { + selectable.navigation = navigation; + } + } + } + + internal void RefreshSelectableCache() + { + if (!_cacheDirty) + { + return; + } + + _cacheDirty = false; + _cachedSelectables.Clear(); + + if (_explicitSelectables != null && _explicitSelectables.Count > 0) + { + for (int i = 0; i < _explicitSelectables.Count; i++) + { + TryAddSelectable(_explicitSelectables[i]); + } + } + else + { + Selectable[] selectables = GetComponentsInChildren(true); + for (int i = 0; i < selectables.Length; i++) + { + TryAddSelectable(selectables[i]); + } + } + + _removeBuffer.Clear(); + foreach (var pair in _baselineNavigation) + { + if (!_cachedSelectables.Contains(pair.Key)) + { + _removeBuffer.Add(pair.Key); + } + } + + for (int i = 0; i < _removeBuffer.Count; i++) + { + _baselineNavigation.Remove(_removeBuffer[i]); + } + } + + internal void InvalidateSelectableCache() + { + _cacheDirty = true; + } + + private void TryAddSelectable(Selectable selectable) + { + if (selectable == null || !Owns(selectable.gameObject)) + { + return; + } + + _cachedSelectables.Add(selectable); + if (!_baselineNavigation.ContainsKey(selectable) || !_navigationSuppressed) + { + _baselineNavigation[selectable] = selectable.navigation; + } + } + + private bool IsSelectableValid(Selectable selectable) + { + return selectable != null + && selectable.IsActive() + && selectable.IsInteractable() + && Owns(selectable.gameObject); + } + } +} +#endif diff --git a/Runtime/UXComponent/Navigation/UXNavigationScope.cs.meta b/Runtime/UXComponent/Navigation/UXNavigationScope.cs.meta new file mode 100644 index 0000000..eda0a30 --- /dev/null +++ b/Runtime/UXComponent/Navigation/UXNavigationScope.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 85e4995cea5647b46995bb92b329207d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: