using GFramework.Core.Abstractions.Pause; using GFramework.Core.Extensions; using GFramework.Game.Abstractions.Enums; using GFramework.Game.Abstractions.UI; using GFramework.Game.Routing; namespace GFramework.Game.UI; /// /// UI路由基类,提供页面栈管理和层级UI管理功能 /// 负责UI页面的导航、显示、隐藏以及生命周期管理 /// public abstract class UiRouterBase : RouterBase, IUiRouter { private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger(nameof(UiRouterBase)); /// /// 层级管理字典(非栈层级),用于管理Overlay、Modal、Toast等浮层UI /// Key: UiLayer枚举值, Value: InstanceId到PageBehavior的映射字典 /// private readonly Dictionary> _layers = new(); /// /// 记录当前由页面可见性驱动持有的暂停令牌。 /// private readonly Dictionary _pauseTokens = new(); /// /// UI切换处理器管道,用于执行UI过渡动画和逻辑 /// private readonly UiTransitionPipeline _pipeline = new(); /// /// UI工厂实例,用于创建UI页面和相关对象 /// private IUiFactory _factory = null!; /// /// 实例ID计数器,用于生成唯一的UI实例标识符 /// private int _instanceCounter; /// /// 可选暂停栈管理器。 /// private IPauseStackManager? _pauseStackManager; /// /// UI根节点引用,用于添加和移除UI页面 /// private IUiRoot _uiRoot = null!; /// /// 注册UI切换处理器 /// /// UI切换处理器实例 /// 处理器选项配置 public void RegisterHandler(IUiTransitionHandler handler, UiTransitionHandlerOptions? options = null) { _pipeline.RegisterHandler(handler, options); } /// /// 注销UI切换处理器 /// /// 要注销的UI切换处理器实例 public void UnregisterHandler(IUiTransitionHandler handler) { _pipeline.UnregisterHandler(handler); } /// /// 绑定UI根节点 /// /// UI根节点实例 public void BindRoot(IUiRoot root) { _uiRoot = root; Log.Debug("Bind UI Root: {0}", root.GetType().Name); } #region Page Stack Management /// /// 将指定的UI界面压入路由栈 /// /// UI页面的唯一标识键 /// 页面进入参数 /// UI过渡策略 public async ValueTask PushAsync(string uiKey, IUiPageEnterParam? param = null, UiTransitionPolicy policy = UiTransitionPolicy.Exclusive) { if (IsTop(uiKey)) { Log.Warn("Push ignored: UI already on top: {0}", uiKey); return; } var @event = CreateEvent(uiKey, UiTransitionType.Push, policy, param); Log.Debug("Push UI Page: key={0}, policy={1}, stackBefore={2}", uiKey, policy, Stack.Count); await _pipeline.ExecuteAroundAsync(@event, async () => { await BeforeChangeAsync(@event).ConfigureAwait(true); await DoPushPageInternalAsync(uiKey, param, policy).ConfigureAwait(true); await AfterChangeAsync(@event).ConfigureAwait(true); }).ConfigureAwait(true); } /// /// 将已存在的UI页面压入栈顶 /// /// 已存在的UI页面行为实例 /// 页面进入参数 /// UI过渡策略 public async ValueTask PushAsync(IUiPageBehavior page, IUiPageEnterParam? param = null, UiTransitionPolicy policy = UiTransitionPolicy.Exclusive) { var uiKey = page.Key; if (IsTop(uiKey)) { Log.Warn("Push ignored: UI already on top: {0}", uiKey); return; } var @event = CreateEvent(uiKey, UiTransitionType.Push, policy, param); Log.Debug("Push existing UI Page: key={0}, policy={1}, stackBefore={2}", uiKey, policy, Stack.Count); await _pipeline.ExecuteAroundAsync(@event, async () => { await BeforeChangeAsync(@event).ConfigureAwait(true); DoPushPageInternal(page, param, policy); await AfterChangeAsync(@event).ConfigureAwait(true); }).ConfigureAwait(true); } /// /// 弹出栈顶页面 /// /// 页面弹出策略 public async ValueTask PopAsync(UiPopPolicy policy = UiPopPolicy.Destroy) { if (Stack.Count == 0) { Log.Debug("Pop ignored: stack is empty"); return; } var leavingUiKey = Stack.Peek().Key; if (!await ExecuteLeaveGuardsAsync(leavingUiKey).ConfigureAwait(true)) { Log.Warn("Pop blocked by guard: {0}", leavingUiKey); return; } var nextUiKey = Stack.Count > 1 ? Stack.ElementAt(1).Key : null; var @event = CreateEvent(nextUiKey, UiTransitionType.Pop); await _pipeline.ExecuteAroundAsync(@event, async () => { await BeforeChangeAsync(@event).ConfigureAwait(true); DoPopInternal(policy); await AfterChangeAsync(@event).ConfigureAwait(true); }).ConfigureAwait(true); } /// /// 替换当前所有页面为新页面(基于uiKey) /// /// 新UI页面的唯一标识键 /// 页面进入参数 /// 页面弹出策略 /// 页面压入策略 public async ValueTask ReplaceAsync(string uiKey, IUiPageEnterParam? param = null, UiPopPolicy popPolicy = UiPopPolicy.Destroy, UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive) { var @event = CreateEvent(uiKey, UiTransitionType.Replace, pushPolicy, param); Log.Debug("Replace UI Stack with page: key={0}, popPolicy={1}, pushPolicy={2}", uiKey, popPolicy, pushPolicy); await _pipeline.ExecuteAroundAsync(@event, async () => { await BeforeChangeAsync(@event).ConfigureAwait(true); DoClearInternal(popPolicy); var page = _factory.Create(uiKey); Log.Debug("Get/Create UI Page instance for Replace: {0}", page.GetType().Name); DoPushPageInternal(page, param, pushPolicy); await AfterChangeAsync(@event).ConfigureAwait(true); }).ConfigureAwait(true); } /// /// 替换当前所有页面为已存在的页面 /// /// 已存在的UI页面行为实例 /// 页面进入参数 /// 页面弹出策略 /// 页面压入策略 public async ValueTask ReplaceAsync(IUiPageBehavior page, IUiPageEnterParam? param = null, UiPopPolicy popPolicy = UiPopPolicy.Destroy, UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive) { var uiKey = page.Key; var @event = CreateEvent(uiKey, UiTransitionType.Replace, pushPolicy, param); Log.Debug("Replace UI Stack with existing page: key={0}, popPolicy={1}, pushPolicy={2}", uiKey, popPolicy, pushPolicy); await _pipeline.ExecuteAroundAsync(@event, async () => { await BeforeChangeAsync(@event).ConfigureAwait(true); DoClearInternal(popPolicy); Log.Debug("Use existing UI Page instance for Replace: {0}", page.GetType().Name); DoPushPageInternal(page, param, pushPolicy); await AfterChangeAsync(@event).ConfigureAwait(true); }).ConfigureAwait(true); } /// /// 清空所有页面栈 /// public async ValueTask ClearAsync() { var @event = CreateEvent(string.Empty, UiTransitionType.Clear); Log.Debug("Clear UI Stack, stackCount={0}", Stack.Count); await _pipeline.ExecuteAroundAsync(@event, async () => { await BeforeChangeAsync(@event).ConfigureAwait(true); DoClearInternal(UiPopPolicy.Destroy); await AfterChangeAsync(@event).ConfigureAwait(true); }).ConfigureAwait(true); } /// /// 获取栈顶元素的键值 /// /// 栈顶UI页面的键值,如果栈为空则返回空字符串 public new string PeekKey() { return Stack.Count == 0 ? string.Empty : Stack.Peek().Key; } /// /// 获取栈顶元素 /// /// 栈顶UI页面行为实例,如果栈为空则返回null public IUiPageBehavior? Peek() { return Stack.Count == 0 ? null : Stack.Peek(); } /// /// 判断栈顶是否为指定UI /// /// 要检查的UI页面键值 /// 如果栈顶是指定UI则返回true,否则返回false public new bool IsTop(string uiKey) { return Stack.Count != 0 && string.Equals(Stack.Peek().Key, uiKey, StringComparison.Ordinal); } /// /// 判断栈中是否包含指定UI /// /// 要检查的UI页面键值 /// 如果栈中包含指定UI则返回true,否则返回false public new bool Contains(string uiKey) { return Stack.Any(p => string.Equals(p.Key, uiKey, StringComparison.Ordinal)); } /// /// 获取栈深度 /// public new int Count => Stack.Count; #endregion #region Layer UI Management /// /// 在指定层级显示UI(基于 uiKey) /// /// UI页面的唯一标识键 /// UI显示层级 /// 页面进入参数 /// UI句柄实例 /// 当尝试在Page层级使用此方法时抛出 public UiHandle Show(string uiKey, UiLayer layer, IUiPageEnterParam? param = null) { if (layer == UiLayer.Page) throw new ArgumentException("Use Push() for Page layer", nameof(layer)); // 创建实例 var page = _factory.Create(uiKey); return ShowInternal(page, layer, param); } /// /// 在指定层级显示UI(基于实例) /// /// UI页面行为实例 /// UI显示层级 /// UI句柄实例 /// 当尝试在Page层级使用此方法时抛出 public UiHandle Show(IUiPageBehavior page, UiLayer layer) { if (layer == UiLayer.Page) throw new ArgumentException("Use Push() for Page layer", nameof(layer)); return ShowInternal(page, layer, null); } /// /// 隐藏指定层级的UI /// /// UI句柄 /// UI层级 /// 是否销毁UI实例,默认为false public void Hide(UiHandle handle, UiLayer layer, bool destroy = false) { if (!_layers.TryGetValue(layer, out var layerDict)) return; if (!layerDict.TryGetValue(handle.InstanceId, out var page)) return; if (destroy) { page.OnExit(); SyncPauseRequest(page, isVisible: false); _uiRoot.RemoveUiPage(page); layerDict.Remove(handle.InstanceId); Log.Debug("Hide & Destroy UI: instanceId={0}, layer={1}", handle.InstanceId, layer); } else { page.OnHide(); SyncPauseRequest(page, isVisible: false); Log.Debug("Hide UI (suspend): instanceId={0}, layer={1}", handle.InstanceId, layer); } } /// /// 恢复指定UI的显示 /// /// UI句柄 /// UI层级 public void Resume(UiHandle handle, UiLayer layer) { if (!_layers.TryGetValue(layer, out var layerDict)) return; if (!layerDict.TryGetValue(handle.InstanceId, out var page)) return; page.OnShow(); SyncPauseRequest(page, isVisible: true); Log.Debug("Resume UI: instanceId={0}, layer={1}", handle.InstanceId, layer); } /// /// 清空指定层级的所有UI /// /// 要清空的UI层级 /// 是否销毁UI实例,默认为false public void ClearLayer(UiLayer layer, bool destroy = false) { if (!_layers.TryGetValue(layer, out var layerDict)) return; var handles = layerDict.Keys .Select(instanceId => { var page = layerDict[instanceId]; return new UiHandle(page.Key, instanceId, layer); }) .ToArray(); foreach (var handle in handles) Hide(handle, layer, destroy); Log.Debug("Cleared layer: {0}, destroyed={1}", layer, destroy); } /// /// 获取指定层级的UI实例 /// /// UI句柄 /// UI层级 /// 如果找到则返回UI句柄,否则返回null public UiHandle? GetFromLayer(UiHandle handle, UiLayer layer) { if (!_layers.TryGetValue(layer, out var layerDict)) return null; return layerDict.ContainsKey(handle.InstanceId) ? handle : null; } /// /// 获取指定 uiKey 在指定层级的所有实例 /// /// UI页面的唯一标识键 /// UI层级 /// 指定UI在该层级的所有实例句柄列表 public IReadOnlyList GetAllFromLayer(string uiKey, UiLayer layer) { if (!_layers.TryGetValue(layer, out var layerDict)) return Array.Empty(); return layerDict .Where(kvp => string.Equals(kvp.Value.Key, uiKey, StringComparison.Ordinal)) .Select(kvp => new UiHandle(uiKey, kvp.Key, layer)) .ToList(); } /// /// 判断指定UI是否在层级中可见 /// /// UI句柄 /// UI层级 /// 如果UI在层级中且可见则返回true,否则返回false public bool HasVisibleInLayer(UiHandle handle, UiLayer layer) { if (!_layers.TryGetValue(layer, out var layerDict)) return false; if (!layerDict.TryGetValue(handle.InstanceId, out var page)) return false; return page.IsVisible; } /// /// 根据UI键隐藏指定层级中的UI。 /// /// UI的唯一标识键。 /// 要操作的UI层级。 /// 是否销毁UI实例,默认为false。 /// 是否隐藏所有匹配的UI实例,默认为false。 public void HideByKey(string uiKey, UiLayer layer, bool destroy = false, bool hideAll = false) { var handles = GetAllFromLayer(uiKey, layer); if (handles.Count == 0) return; if (hideAll) foreach (var h in handles) { Hide(h, layer, destroy); } else Hide(handles[0], layer, destroy); } /// /// 获取当前拥有指定 UI 语义动作捕获权的页面。 /// /// 要查询的动作。 /// 动作所有者;若没有页面声明捕获该动作则返回 public IUiPageBehavior? GetUiActionOwner(UiInputAction action) { return EnumerateVisiblePagesByPriority() .FirstOrDefault(page => UiInteractionProfiles.Captures(page.InteractionProfile, action)); } /// /// 尝试将语义动作分发给当前拥有捕获权的页面。 /// /// 当前动作。 /// 如果已有页面捕获该动作则返回 public bool TryDispatchUiAction(UiInputAction action) { var owner = GetUiActionOwner(action); if (owner is null) return false; var handled = owner.TryHandleUiAction(action); if (!handled) Log.Debug("UI action captured without explicit handler: key={0}, action={1}", owner.Key, action); return true; } /// /// 尝试将语义动作分发给当前拥有捕获权的页面。 /// /// 当前动作。 /// 如果已有页面捕获该动作则返回 [Obsolete( "Use TryDispatchUiAction(UiInputAction action) to emphasize dispatch semantics instead of handler success.")] public bool TryHandleUiAction(UiInputAction action) { return TryDispatchUiAction(action); } /// /// 判断当前可见 UI 是否阻断 World 指针输入。 /// /// 如果 World 指针输入应被阻断则返回 public bool BlocksWorldPointerInput() { return EnumerateVisiblePagesByPriority() .Any(page => page.InteractionProfile.BlocksWorldPointerInput); } /// /// 判断当前可见 UI 是否阻断 World 语义动作输入。 /// /// 如果 World 语义动作输入应被阻断则返回 public bool BlocksWorldActionInput() { return EnumerateVisiblePagesByPriority() .Any(page => page.InteractionProfile.BlocksWorldActionInput); } #endregion #region Initialization /// /// 初始化函数,在对象创建时调用。 /// 该函数负责获取UI工厂实例并注册处理程序。 /// protected override void OnInit() { // 获取UI工厂实例,并确保其不为null _factory = this.GetUtility()!; TryBindPauseStackManager(); // 输出调试日志,记录UI路由器基类已初始化及使用的工厂类型 Log.Debug("UiRouterBase initialized. Factory={0}", _factory.GetType().Name); // 调用抽象方法以注册具体的处理程序 RegisterHandlers(); } /// /// 抽象方法,用于注册具体的处理程序。 /// 子类必须实现此方法以完成特定的处理逻辑注册。 /// protected override abstract void RegisterHandlers(); /// /// 路由销毁时释放所有由页面持有的暂停请求。 /// protected override void OnDestroy() { base.OnDestroy(); if (_pauseStackManager is null) return; foreach (var token in _pauseTokens.Values.ToArray()) { _pauseStackManager.Pop(token); } _pauseTokens.Clear(); } #endregion #region Internal Helpers /// /// 生成唯一实例ID /// /// 格式为"ui_000001"的唯一实例标识符 private string GenerateInstanceId() { // 原子操作递增实例计数器,确保多线程环境下的唯一性 var id = Interlocked.Increment(ref _instanceCounter); // 返回格式化的实例ID字符串 return $"ui_{id:D6}"; } /// /// 内部Show实现,支持重入 /// /// UI页面行为实例 /// UI显示层级 /// 页面进入参数 /// UI句柄实例 /// 当UI不支持重入且已在该层级存在时抛出 private UiHandle ShowInternal(IUiPageBehavior page, UiLayer layer, IUiPageEnterParam? param) { var instanceId = GenerateInstanceId(); var handle = new UiHandle(page.Key, instanceId, layer); // 初始化层级字典 if (!_layers.TryGetValue(layer, out var layerDict)) { layerDict = new Dictionary(StringComparer.Ordinal); _layers[layer] = layerDict; } // 设置句柄 page.Handle = handle; // 检查重入性 if (!page.IsReentrant && layerDict.Values.Any(p => string.Equals(p.Key, page.Key, StringComparison.Ordinal))) { Log.Warn("UI {0} is not reentrant but already exists in layer {1}", page.Key, layer); throw new InvalidOperationException( $"UI {page.Key} does not support multiple instances in layer {layer}"); } // 添加到层级管理 layerDict[instanceId] = page; // 添加到UiRoot _uiRoot.AddUiPage(page, layer); // 生命周期 page.OnEnter(param); page.OnShow(); SyncPauseRequest(page, isVisible: true); Log.Debug("Show UI: key={0}, instanceId={1}, layer={2}", page.Key, instanceId, layer); return handle; } /// /// 创建UI过渡事件 /// /// 目标UI键值 /// 过渡类型 /// 过渡策略 /// 进入参数 /// UI过渡事件实例 private UiTransitionEvent CreateEvent(string? toUiKey, UiTransitionType type, UiTransitionPolicy? policy = null, IUiPageEnterParam? param = null) { return new UiTransitionEvent { FromUiKey = PeekKey(), ToUiKey = toUiKey, TransitionType = type, Policy = policy ?? UiTransitionPolicy.Exclusive, EnterParam = param }; } /// /// 执行过渡前阶段 /// /// UI过渡事件 private async Task BeforeChangeAsync(UiTransitionEvent @event) { Log.Debug("BeforeChange phases started: {0}", @event.TransitionType); await _pipeline.ExecuteAsync(@event, UiTransitionPhases.BeforeChange).ConfigureAwait(true); Log.Debug("BeforeChange phases completed: {0}", @event.TransitionType); } /// /// 执行过渡后阶段 /// /// UI过渡事件 private async Task AfterChangeAsync(UiTransitionEvent @event) { Log.Debug("AfterChange phases started: {0}", @event.TransitionType); await _pipeline.ExecuteAsync(@event, UiTransitionPhases.AfterChange).ConfigureAwait(true); Log.Debug("AfterChange phases completed: {0}", @event.TransitionType); } /// /// 内部异步压入页面实现 /// /// UI页面键值 /// 页面进入参数 /// 过渡策略 private async Task DoPushPageInternalAsync(string uiKey, IUiPageEnterParam? param, UiTransitionPolicy policy) { if (!await ExecuteEnterGuardsAsync(uiKey, param).ConfigureAwait(true)) { Log.Warn("Push blocked by guard: {0}", uiKey); return; } var page = _factory.Create(uiKey); Log.Debug("Get/Create UI Page instance: {0}", page.GetType().Name); DoPushPageInternal(page, param, policy); } /// /// 内部压入页面实现 /// /// UI页面行为实例 /// 页面进入参数 /// 过渡策略 private void DoPushPageInternal(IUiPageBehavior page, IUiPageEnterParam? param, UiTransitionPolicy policy) { if (Stack.Count > 0) { var current = Stack.Peek(); Log.Debug("Pause current page: {0}", current.View.GetType().Name); current.OnPause(); if (policy == UiTransitionPolicy.Exclusive) { Log.Debug("Suspend current page (Exclusive): {0}", current.View.GetType().Name); current.OnHide(); SyncPauseRequest(current, isVisible: false); } } Log.Debug("Add page to UiRoot: {0}", page.View.GetType().Name); _uiRoot.AddUiPage(page); Stack.Push(page); Log.Debug("Enter & Show page: {0}, stackAfter={1}", page.View.GetType().Name, Stack.Count); page.OnEnter(param); page.OnShow(); SyncPauseRequest(page, isVisible: true); } /// /// 内部弹出页面实现 /// /// 页面弹出策略 private void DoPopInternal(UiPopPolicy policy) { if (Stack.Count == 0) return; var top = Stack.Pop(); Log.Debug("Pop UI Page internal: {0}, policy={1}, stackAfterPop={2}", top.GetType().Name, policy, Stack.Count); if (policy == UiPopPolicy.Destroy) { top.OnExit(); _uiRoot.RemoveUiPage(top); } else { top.OnHide(); } SyncPauseRequest(top, isVisible: false); if (Stack.Count > 0) { var next = Stack.Peek(); next.OnShow(); SyncPauseRequest(next, isVisible: true); } } /// /// 内部清空页面实现 /// /// 页面弹出策略 private void DoClearInternal(UiPopPolicy policy) { Log.Debug("Clear UI Stack internal, count={0}", Stack.Count); while (Stack.Count > 0) DoPopInternal(policy); } /// /// 尝试绑定暂停栈管理器。 /// private void TryBindPauseStackManager() { try { _pauseStackManager = this.GetUtility(); } catch (InvalidOperationException) { _pauseStackManager = null; Log.Debug("PauseStackManager not available. Pause integration is disabled for the UI router."); } } /// /// 根据页面可见性同步暂停请求。 /// /// 页面行为。 /// 页面是否应视为可见。 private void SyncPauseRequest(IUiPageBehavior page, bool isVisible) { if (_pauseStackManager is null) return; var profile = page.InteractionProfile; if (!isVisible || profile.PauseMode == UiPauseMode.None) { ReleasePauseRequest(page); return; } if (_pauseTokens.ContainsKey(page)) return; var reason = string.IsNullOrWhiteSpace(profile.PauseReason) ? $"UI:{page.Key}" : profile.PauseReason; _pauseTokens[page] = _pauseStackManager.Push(reason, profile.PauseGroup); } /// /// 释放页面此前登记的暂停请求。 /// /// 目标页面。 private void ReleasePauseRequest(IUiPageBehavior page) { if (_pauseStackManager is null) return; if (!_pauseTokens.Remove(page, out var token)) return; _pauseStackManager.Pop(token); } /// /// 按输入优先级枚举当前所有可见页面。 /// /// 可见页面序列。 private IEnumerable EnumerateVisiblePagesByPriority() { foreach (var page in EnumerateVisibleLayerPages(UiLayer.Topmost)) yield return page; foreach (var page in EnumerateVisibleLayerPages(UiLayer.Modal)) yield return page; foreach (var page in EnumerateVisibleLayerPages(UiLayer.Overlay)) yield return page; foreach (var page in Stack.Where(static page => page.IsAlive && page.IsVisible)) yield return page; foreach (var page in EnumerateVisibleLayerPages(UiLayer.Toast)) yield return page; } /// /// 枚举指定层级中的可见页面,层内按最近显示优先。 /// /// 目标层级。 /// 该层级中的可见页面。 private IEnumerable EnumerateVisibleLayerPages(UiLayer layer) { if (!_layers.TryGetValue(layer, out var layerDict)) yield break; foreach (var page in layerDict // Use the numeric sequence encoded in the instance id so ordering stays correct after width overflow. .OrderByDescending(static pair => ExtractInstanceSequence(pair.Key)) .Select(static pair => pair.Value) .Where(static page => page.IsAlive && page.IsVisible)) { yield return page; } } /// /// 从实例标识符中提取自增序号,供层内最近显示优先排序使用。 /// /// 实例标识符,预期格式为 ui_000001。 /// 提取到的自增序号;若格式异常则返回 ,使异常值排在最后。 private static int ExtractInstanceSequence(string instanceId) { return instanceId.Length > 3 && int.TryParse(instanceId.AsSpan(3), NumberStyles.None, CultureInfo.InvariantCulture, out var sequence) ? sequence : int.MinValue; } #endregion }