This commit is contained in:
2026-06-07 11:49:55 +08:00
parent ff0f3bde54
commit 1897658a00
98 changed files with 9903 additions and 13907 deletions

View File

@@ -0,0 +1,182 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.SceneManagement;
using BaseGames.Core;
using BaseGames.Core.Events;
namespace BaseGames.UI
{
/// <summary>
/// <see cref="IUINavigator"/> 的实现:统一 UI 导航栈。常驻 Persistent 场景,
/// 经 ServiceLocator 暴露,主菜单与游戏内共用同一套栈语义。
///
/// <para>压栈:记录压栈前焦点;据新面板 <see cref="PushMode"/> 处理下方面板
/// Replace→停用Modal→保留可见但 <see cref="UIPanelBase.SetInteractableLayer"/>(false)
/// 使其退出导航图);激活新面板并延后一帧聚焦其首项。</para>
/// <para>出栈:关闭栈顶,按其压栈时的 mode 还原下方面板,恢复焦点到压栈前的选中项。</para>
/// <para>取消:本类是 EVT_UICancelPressed 的唯一消费者,仅在栈顶 <see cref="UIPanelBase.CanCancel"/>
/// 时出栈一层(逐层回退)。</para>
/// </summary>
[DefaultExecutionOrder(+40)] // 早于 UIManager(+50),确保委托方解析得到本服务
public class UINavigator : MonoBehaviour, IUINavigator
{
[Tooltip("UI 取消操作ESC / 手柄 B·Circle。本导航器为唯一订阅者按下时关闭栈顶一层。对应 EVT_UICancelPressed。")]
[SerializeField] private VoidEventChannelSO _onUICancelPressed;
private readonly Stack<UIStackEntry> _stack = new();
private readonly CompositeDisposable _subs = new();
private Coroutine _focusRoutine;
public UIPanelBase Top => _stack.Count > 0 ? _stack.Peek().Panel : null;
public int Depth => _stack.Count;
public event Action StackChanged;
// ── 生命周期 ──────────────────────────────────────────────────────────
private void OnEnable()
{
ServiceLocator.Register<IUINavigator>(this);
_onUICancelPressed?.Subscribe(HandleCancel).AddTo(_subs);
SceneManager.sceneUnloaded += OnSceneUnloaded;
}
private void OnDisable()
{
SceneManager.sceneUnloaded -= OnSceneUnloaded;
_subs.Clear();
ServiceLocator.Unregister<IUINavigator>(this);
}
// ── 压栈 ──────────────────────────────────────────────────────────────
public void Push(UIPanelBase panel, PushMode? mode = null)
{
if (panel == null) return;
PurgeDead();
PushMode m = mode ?? panel.DefaultMode;
// 处理下方面板Replace 停用、Modal 屏蔽交互(退出导航图,杜绝上下键穿透)。
if (_stack.Count > 0)
{
var below = _stack.Peek().Panel;
if (below != null)
{
below.OnFocusLost();
if (m == PushMode.Replace) below.gameObject.SetActive(false);
else below.SetInteractableLayer(false);
}
}
var entry = new UIStackEntry
{
Panel = panel,
Mode = m,
FocusToRestore = EventSystem.current != null ? EventSystem.current.currentSelectedGameObject : null,
OwningScene = panel.gameObject.scene,
};
_stack.Push(entry);
panel.gameObject.SetActive(true);
panel.SetInteractableLayer(true);
FocusNextFrame(panel.FirstSelectableGO);
StackChanged?.Invoke();
}
// ── 出栈 ──────────────────────────────────────────────────────────────
public void Pop()
{
PurgeDead();
if (_stack.Count == 0) return;
var top = _stack.Pop();
if (top.Panel != null) top.Panel.gameObject.SetActive(false);
UIPanelBase below = _stack.Count > 0 ? _stack.Peek().Panel : null;
if (below != null)
{
// 按 top 压栈时的 mode 还原下方面板。
if (top.Mode == PushMode.Replace) below.gameObject.SetActive(true);
else below.SetInteractableLayer(true);
below.OnFocusGained();
}
// 恢复焦点到压栈前的选中项(失效则回落到下层首项)。
GameObject restore = top.FocusToRestore != null && top.FocusToRestore.activeInHierarchy
? top.FocusToRestore
: below != null ? below.FirstSelectableGO : null;
FocusNextFrame(restore);
StackChanged?.Invoke();
}
public void PopToRoot()
{
while (_stack.Count > 0) Pop();
}
// ── 取消ESC / 手柄 B──────────────────────────────────────────────
private void HandleCancel()
{
PurgeDead();
if (_stack.Count == 0) return; // 栈空(如主菜单根):无操作
if (Top != null && !Top.CanCancel) return;
Pop(); // 仅关栈顶一层
}
// ── 结果面板 ──────────────────────────────────────────────────────────
public Task<T> PushForResultAsync<T>(UIResultPanel<T> panel, CancellationToken ct = default)
{
if (panel == null) return Task.FromResult<T>(default);
Task<T> task = panel.BeginResult(ct);
Push(panel, PushMode.Modal); // 结果对话框天然模态:下层保留可见但屏蔽交互
return task;
}
// ── 场景卸载清理 ──────────────────────────────────────────────────────
private void OnSceneUnloaded(Scene s)
{
if (_stack.Count == 0) return;
// Stack 无法删中间项:过滤后按原序重建(保留非本场景且未销毁的层)。
var kept = new List<UIStackEntry>();
foreach (var e in _stack) // 枚举顺序:栈顶→栈底
if (e.Panel != null && e.OwningScene != s) kept.Add(e);
if (kept.Count == _stack.Count) return; // 无变化
_stack.Clear();
for (int i = kept.Count - 1; i >= 0; i--) _stack.Push(kept[i]);
StackChanged?.Invoke();
}
private void PurgeDead()
{
bool changed = false;
while (_stack.Count > 0 && _stack.Peek().Panel == null) { _stack.Pop(); changed = true; }
if (changed) StackChanged?.Invoke();
}
// ── 焦点(延后一帧,避开 OnEnable / UISelectionRestorer 同帧竞争)────────
private void FocusNextFrame(GameObject target)
{
if (_focusRoutine != null) StopCoroutine(_focusRoutine);
if (!isActiveAndEnabled) return;
_focusRoutine = StartCoroutine(FocusRoutine(target));
}
private IEnumerator FocusRoutine(GameObject target)
{
yield return null;
_focusRoutine = null;
if (target == null || !target.activeInHierarchy) yield break;
if (EventSystem.current != null)
EventSystem.current.SetSelectedGameObject(target);
}
}
}