From 2e7fd1fc87e6bcbcd4485d440f05136be1e78132 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Fri, 17 Apr 2026 22:00:33 +0800
Subject: [PATCH 1/6] =?UTF-8?q?feat(ui):=20=E6=B7=BB=E5=8A=A0UI=E8=B7=AF?=
=?UTF-8?q?=E7=94=B1=E5=9F=BA=E7=B1=BB=E5=92=8C=E7=9B=B8=E5=85=B3=E6=8E=A5?=
=?UTF-8?q?=E5=8F=A3=E5=AE=9A=E4=B9=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现UiRouterBase基类,提供页面栈管理和层级UI管理功能
- 定义IUiPageBehavior接口,规范UI页面生命周期方法和状态管理
- 定义IUiRouter接口,统一UI界面导航和切换操作规范
- 实现页面栈操作功能,包括Push、Pop、Replace、Clear等方法
- 实现层级UI管理功能,支持Overlay、Modal、Toast等浮层显示
- 集成UI过渡管道,支持UI切换动画和逻辑处理
- 添加暂停令牌管理,实现页面可见性驱动的暂停控制
- 实现UI动作捕获和分发机制,支持语义动作处理
---
.../UI/IUiActionHandler.cs | 17 ++
.../UI/IUiInteractionProfileProvider.cs | 16 ++
.../UI/IUiPageBehavior.cs | 14 +-
GFramework.Game.Abstractions/UI/IUiRouter.cs | 28 ++-
.../UI/UiInputAction.cs | 23 +++
.../UI/UiInputActionMask.cs | 23 +++
.../UI/UiInteractionProfile.cs | 90 +++++++++
.../UI/UiPauseMode.cs | 17 ++
GFramework.Game/UI/UiRouterBase.cs | 188 +++++++++++++++++-
.../UI/CanvasItemUiPageBehaviorBase.cs | 49 ++++-
10 files changed, 459 insertions(+), 6 deletions(-)
create mode 100644 GFramework.Game.Abstractions/UI/IUiActionHandler.cs
create mode 100644 GFramework.Game.Abstractions/UI/IUiInteractionProfileProvider.cs
create mode 100644 GFramework.Game.Abstractions/UI/UiInputAction.cs
create mode 100644 GFramework.Game.Abstractions/UI/UiInputActionMask.cs
create mode 100644 GFramework.Game.Abstractions/UI/UiInteractionProfile.cs
create mode 100644 GFramework.Game.Abstractions/UI/UiPauseMode.cs
diff --git a/GFramework.Game.Abstractions/UI/IUiActionHandler.cs b/GFramework.Game.Abstractions/UI/IUiActionHandler.cs
new file mode 100644
index 00000000..876b293b
--- /dev/null
+++ b/GFramework.Game.Abstractions/UI/IUiActionHandler.cs
@@ -0,0 +1,17 @@
+namespace GFramework.Game.Abstractions.UI;
+
+///
+/// 由页面视图实现,用于接收路由仲裁后的 UI 语义动作。
+///
+public interface IUiActionHandler
+{
+ ///
+ /// 处理一个 UI 语义动作。
+ ///
+ /// 当前要处理的动作。
+ ///
+ /// 如果页面已经完成处理则返回 ;
+ /// 返回 时,路由器仍会把声明捕获该动作视为已消费。
+ ///
+ bool TryHandleUiAction(UiInputAction action);
+}
diff --git a/GFramework.Game.Abstractions/UI/IUiInteractionProfileProvider.cs b/GFramework.Game.Abstractions/UI/IUiInteractionProfileProvider.cs
new file mode 100644
index 00000000..138ddc27
--- /dev/null
+++ b/GFramework.Game.Abstractions/UI/IUiInteractionProfileProvider.cs
@@ -0,0 +1,16 @@
+using GFramework.Game.Abstractions.Enums;
+
+namespace GFramework.Game.Abstractions.UI;
+
+///
+/// 由页面视图实现,用于按运行时状态动态提供交互语义配置。
+///
+public interface IUiInteractionProfileProvider
+{
+ ///
+ /// 获取页面当前应使用的交互配置。
+ ///
+ /// 页面绑定的默认 UI 层级。
+ /// 当前页面的交互配置。
+ UiInteractionProfile GetUiInteractionProfile(UiLayer layer);
+}
diff --git a/GFramework.Game.Abstractions/UI/IUiPageBehavior.cs b/GFramework.Game.Abstractions/UI/IUiPageBehavior.cs
index 17a3e686..450a3000 100644
--- a/GFramework.Game.Abstractions/UI/IUiPageBehavior.cs
+++ b/GFramework.Game.Abstractions/UI/IUiPageBehavior.cs
@@ -66,6 +66,11 @@ public interface IUiPageBehavior : IRoute
///
bool BlocksInput { get; }
+ ///
+ /// 获取页面当前的输入、阻断与暂停交互配置。
+ ///
+ UiInteractionProfile InteractionProfile { get; }
+
///
/// 页面进入时调用的方法
///
@@ -96,4 +101,11 @@ public interface IUiPageBehavior : IRoute
/// 页面重新显示时调用的方法
///
void OnShow();
-}
\ No newline at end of file
+
+ ///
+ /// 尝试处理一个经过路由器仲裁后的 UI 语义动作。
+ ///
+ /// 当前动作。
+ /// 如果页面已经显式处理该动作则返回 。
+ bool TryHandleUiAction(UiInputAction action);
+}
diff --git a/GFramework.Game.Abstractions/UI/IUiRouter.cs b/GFramework.Game.Abstractions/UI/IUiRouter.cs
index b6b18873..06510560 100644
--- a/GFramework.Game.Abstractions/UI/IUiRouter.cs
+++ b/GFramework.Game.Abstractions/UI/IUiRouter.cs
@@ -186,5 +186,31 @@ public interface IUiRouter : ISystem
/// 是否隐藏所有匹配的UI实例,默认为false。
void HideByKey(string uiKey, UiLayer layer, bool destroy = false, bool hideAll = false);
+ ///
+ /// 查询当前对指定 UI 语义动作拥有最高优先级捕获权的页面。
+ ///
+ /// 要查询的动作。
+ /// 动作所有者;如果当前没有页面声明捕获该动作则返回 。
+ IUiPageBehavior? GetUiActionOwner(UiInputAction action);
+
+ ///
+ /// 尝试把语义动作分发给当前拥有该动作的页面。
+ ///
+ /// 当前动作。
+ /// 如果该动作已被某个页面捕获并消费,则返回 。
+ bool TryHandleUiAction(UiInputAction action);
+
+ ///
+ /// 判断当前可见 UI 是否阻断 World 指针输入。
+ ///
+ /// 如果 World 指针输入应被阻断则返回 。
+ bool BlocksWorldPointerInput();
+
+ ///
+ /// 判断当前可见 UI 是否阻断 World 语义动作输入。
+ ///
+ /// 如果 World 动作输入应被阻断则返回 。
+ bool BlocksWorldActionInput();
+
#endregion
-}
\ No newline at end of file
+}
diff --git a/GFramework.Game.Abstractions/UI/UiInputAction.cs b/GFramework.Game.Abstractions/UI/UiInputAction.cs
new file mode 100644
index 00000000..c0aee070
--- /dev/null
+++ b/GFramework.Game.Abstractions/UI/UiInputAction.cs
@@ -0,0 +1,23 @@
+namespace GFramework.Game.Abstractions.UI;
+
+///
+/// 定义框架级 UI 语义动作。
+/// 这些动作由输入层映射后交给 UI 路由统一仲裁,避免页面直接依赖具体按键或设备事件。
+///
+public enum UiInputAction
+{
+ ///
+ /// 未指定动作。
+ ///
+ None = 0,
+
+ ///
+ /// 取消、返回或关闭当前 UI。
+ ///
+ Cancel = 1,
+
+ ///
+ /// 确认当前 UI 操作。
+ ///
+ Confirm = 2
+}
diff --git a/GFramework.Game.Abstractions/UI/UiInputActionMask.cs b/GFramework.Game.Abstractions/UI/UiInputActionMask.cs
new file mode 100644
index 00000000..09c7b54e
--- /dev/null
+++ b/GFramework.Game.Abstractions/UI/UiInputActionMask.cs
@@ -0,0 +1,23 @@
+namespace GFramework.Game.Abstractions.UI;
+
+///
+/// 以位标记形式声明 UI 页面要捕获的语义动作集合。
+///
+[Flags]
+public enum UiInputActionMask
+{
+ ///
+ /// 不捕获任何动作。
+ ///
+ None = 0,
+
+ ///
+ /// 捕获取消动作。
+ ///
+ Cancel = 1 << 0,
+
+ ///
+ /// 捕获确认动作。
+ ///
+ Confirm = 1 << 1
+}
diff --git a/GFramework.Game.Abstractions/UI/UiInteractionProfile.cs b/GFramework.Game.Abstractions/UI/UiInteractionProfile.cs
new file mode 100644
index 00000000..32069d48
--- /dev/null
+++ b/GFramework.Game.Abstractions/UI/UiInteractionProfile.cs
@@ -0,0 +1,90 @@
+using GFramework.Core.Abstractions.Pause;
+using GFramework.Game.Abstractions.Enums;
+
+namespace GFramework.Game.Abstractions.UI;
+
+///
+/// 描述一个 UI 页面在输入、World 阻断与暂停上的运行时语义。
+///
+public sealed class UiInteractionProfile
+{
+ ///
+ /// 获取默认值实例。
+ ///
+ public static UiInteractionProfile Default { get; } = new();
+
+ ///
+ /// 声明当前页面要捕获的语义动作集合。
+ ///
+ public UiInputActionMask CapturedActions { get; init; } = UiInputActionMask.None;
+
+ ///
+ /// 指示当前页面是否阻断 World 指针输入,例如地图点击或相机拖拽。
+ ///
+ public bool BlocksWorldPointerInput { get; init; }
+
+ ///
+ /// 指示当前页面是否阻断 World 语义动作输入,例如 gameplay 快捷键。
+ ///
+ public bool BlocksWorldActionInput { get; init; }
+
+ ///
+ /// 指示当前页面的可见性是否应驱动暂停栈。
+ ///
+ public UiPauseMode PauseMode { get; init; } = UiPauseMode.None;
+
+ ///
+ /// 当 生效时使用的暂停组。
+ ///
+ public PauseGroup PauseGroup { get; init; } = PauseGroup.Global;
+
+ ///
+ /// 当场景树暂停时,该页面是否仍需继续处理输入与动画。
+ ///
+ public bool ContinueProcessingWhenPaused { get; init; }
+
+ ///
+ /// 页面向暂停栈登记时使用的原因文本。
+ ///
+ public string PauseReason { get; init; } = string.Empty;
+
+ ///
+ /// 判断当前配置是否捕获了指定动作。
+ ///
+ /// 要查询的语义动作。
+ /// 如果当前配置捕获该动作则返回 。
+ public bool Captures(UiInputAction action)
+ {
+ return action switch
+ {
+ UiInputAction.Cancel => CapturedActions.HasFlag(UiInputActionMask.Cancel),
+ UiInputAction.Confirm => CapturedActions.HasFlag(UiInputActionMask.Confirm),
+ _ => false
+ };
+ }
+
+ ///
+ /// 为指定层级生成默认交互配置。
+ ///
+ /// UI 层级。
+ /// 该层级的默认交互语义。
+ public static UiInteractionProfile CreateDefault(UiLayer layer)
+ {
+ return layer switch
+ {
+ UiLayer.Modal => new UiInteractionProfile
+ {
+ CapturedActions = UiInputActionMask.Cancel,
+ BlocksWorldPointerInput = true,
+ BlocksWorldActionInput = true
+ },
+ UiLayer.Topmost => new UiInteractionProfile
+ {
+ CapturedActions = UiInputActionMask.Cancel,
+ BlocksWorldPointerInput = true,
+ BlocksWorldActionInput = true
+ },
+ _ => Default
+ };
+ }
+}
diff --git a/GFramework.Game.Abstractions/UI/UiPauseMode.cs b/GFramework.Game.Abstractions/UI/UiPauseMode.cs
new file mode 100644
index 00000000..5462c171
--- /dev/null
+++ b/GFramework.Game.Abstractions/UI/UiPauseMode.cs
@@ -0,0 +1,17 @@
+namespace GFramework.Game.Abstractions.UI;
+
+///
+/// 定义页面显示时与暂停系统的协作模式。
+///
+public enum UiPauseMode
+{
+ ///
+ /// 页面显示不会触发暂停请求。
+ ///
+ None = 0,
+
+ ///
+ /// 页面在可见期间持有一个暂停请求,隐藏或销毁时释放。
+ ///
+ WhileVisible = 1
+}
diff --git a/GFramework.Game/UI/UiRouterBase.cs b/GFramework.Game/UI/UiRouterBase.cs
index f105cbb6..8bb376e9 100644
--- a/GFramework.Game/UI/UiRouterBase.cs
+++ b/GFramework.Game/UI/UiRouterBase.cs
@@ -1,6 +1,6 @@
using GFramework.Core.Abstractions.Logging;
+using GFramework.Core.Abstractions.Pause;
using GFramework.Core.Extensions;
-using GFramework.Core.Logging;
using GFramework.Game.Abstractions.Enums;
using GFramework.Game.Abstractions.UI;
using GFramework.Game.Routing;
@@ -21,6 +21,11 @@ public abstract class UiRouterBase : RouterBase
private readonly Dictionary> _layers = new();
+ ///
+ /// 记录当前由页面可见性驱动持有的暂停令牌。
+ ///
+ private readonly Dictionary _pauseTokens = new();
+
///
/// UI切换处理器管道,用于执行UI过渡动画和逻辑
///
@@ -36,6 +41,11 @@ public abstract class UiRouterBase : RouterBase
private int _instanceCounter;
+ ///
+ /// 可选暂停栈管理器。
+ ///
+ private IPauseStackManager? _pauseStackManager;
+
///
/// UI根节点引用,用于添加和移除UI页面
///
@@ -324,6 +334,7 @@ public abstract class UiRouterBase : RouterBase
+ /// 获取当前拥有指定 UI 语义动作捕获权的页面。
+ ///
+ /// 要查询的动作。
+ /// 动作所有者;若没有页面声明捕获该动作则返回 。
+ public IUiPageBehavior? GetUiActionOwner(UiInputAction action)
+ {
+ return EnumerateVisiblePagesByPriority()
+ .FirstOrDefault(page => page.InteractionProfile.Captures(action));
+ }
+
+ ///
+ /// 尝试将语义动作分发给当前拥有捕获权的页面。
+ ///
+ /// 当前动作。
+ /// 如果已有页面捕获该动作则返回 。
+ public bool TryHandleUiAction(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;
+ }
+
+ ///
+ /// 判断当前可见 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
@@ -458,6 +520,7 @@ public abstract class UiRouterBase : RouterBase()!;
+ TryBindPauseStackManager();
// 输出调试日志,记录UI路由器基类已初始化及使用的工厂类型
Log.Debug("UiRouterBase initialized. Factory={0}", _factory.GetType().Name);
@@ -472,6 +535,24 @@ public abstract class UiRouterBase : RouterBase
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
@@ -525,6 +606,7 @@ public abstract class UiRouterBase : RouterBase
@@ -646,11 +730,14 @@ public abstract class UiRouterBase : RouterBase 0)
{
var next = Stack.Peek();
next.OnResume();
next.OnShow();
+ SyncPauseRequest(next, isVisible: true);
}
}
@@ -665,5 +752,102 @@ public abstract class UiRouterBase : RouterBase
+ /// 尝试绑定暂停栈管理器。
+ ///
+ private void TryBindPauseStackManager()
+ {
+ try
+ {
+ _pauseStackManager = this.GetUtility();
+ }
+ catch (InvalidOperationException)
+ {
+ _pauseStackManager = null;
+ }
+ }
+
+ ///
+ /// 根据页面可见性同步暂停请求。
+ ///
+ /// 页面行为。
+ /// 页面是否应视为可见。
+ 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
+ .OrderByDescending(static pair => pair.Key, StringComparer.Ordinal)
+ .Select(static pair => pair.Value)
+ .Where(static page => page.IsAlive && page.IsVisible))
+ {
+ yield return page;
+ }
+ }
+
#endregion
-}
\ No newline at end of file
+}
diff --git a/GFramework.Godot/UI/CanvasItemUiPageBehaviorBase.cs b/GFramework.Godot/UI/CanvasItemUiPageBehaviorBase.cs
index 59dc320b..b7bfafb4 100644
--- a/GFramework.Godot/UI/CanvasItemUiPageBehaviorBase.cs
+++ b/GFramework.Godot/UI/CanvasItemUiPageBehaviorBase.cs
@@ -14,7 +14,6 @@
using GFramework.Game.Abstractions.Enums;
using GFramework.Game.Abstractions.UI;
using GFramework.Godot.Extensions;
-using Godot;
namespace GFramework.Godot.UI;
@@ -36,6 +35,16 @@ public abstract class CanvasItemUiPageBehaviorBase : IUiPageBehavior
///
private readonly IUiPage? _page;
+ ///
+ /// 视图可选提供的交互配置提供者。
+ ///
+ private readonly IUiInteractionProfileProvider? _profileProvider;
+
+ ///
+ /// 视图可选提供的 UI 语义动作处理器。
+ ///
+ private readonly IUiActionHandler? _uiActionHandler;
+
///
/// 视图节点的所有者实例。
///
@@ -51,6 +60,8 @@ public abstract class CanvasItemUiPageBehaviorBase : IUiPageBehavior
Owner = owner;
_key = key;
_page = owner as IUiPage;
+ _profileProvider = owner as IUiInteractionProfileProvider;
+ _uiActionHandler = owner as IUiActionHandler;
}
#region 抽象属性 - 子类必须实现
@@ -115,6 +126,13 @@ public abstract class CanvasItemUiPageBehaviorBase : IUiPageBehavior
///
public bool IsVisible => Owner.Visible;
+ ///
+ /// 获取页面当前的交互配置。
+ /// 若页面未提供自定义配置,则回退到层级默认值。
+ ///
+ public UiInteractionProfile InteractionProfile => _profileProvider?.GetUiInteractionProfile(Layer)
+ ?? UiInteractionProfile.CreateDefault(Layer);
+
#endregion
#region 生命周期管理
@@ -153,6 +171,8 @@ public abstract class CanvasItemUiPageBehaviorBase : IUiPageBehavior
Owner.SetProcess(false);
Owner.SetPhysicsProcess(false);
Owner.SetProcessInput(false);
+ Owner.SetProcessUnhandledInput(false);
+ Owner.SetProcessUnhandledKeyInput(false);
}
///
@@ -166,10 +186,14 @@ public abstract class CanvasItemUiPageBehaviorBase : IUiPageBehavior
_page?.OnResume();
+ ApplyPauseAwareProcessingMode();
+
// 恢复处理
Owner.SetProcess(true);
Owner.SetPhysicsProcess(true);
Owner.SetProcessInput(true);
+ Owner.SetProcessUnhandledInput(true);
+ Owner.SetProcessUnhandledKeyInput(true);
}
///
@@ -189,9 +213,30 @@ public abstract class CanvasItemUiPageBehaviorBase : IUiPageBehavior
public virtual void OnShow()
{
_page?.OnShow();
+ ApplyPauseAwareProcessingMode();
Owner.Show();
OnResume();
}
+ ///
+ /// 尝试处理一个路由仲裁后的 UI 语义动作。
+ ///
+ /// 当前动作。
+ /// 如果视图显式处理了该动作则返回 。
+ public virtual bool TryHandleUiAction(UiInputAction action)
+ {
+ return _uiActionHandler?.TryHandleUiAction(action) ?? false;
+ }
+
+ ///
+ /// 根据交互配置调整节点在暂停态下的处理模式。
+ ///
+ private void ApplyPauseAwareProcessingMode()
+ {
+ Owner.ProcessMode = InteractionProfile.ContinueProcessingWhenPaused
+ ? Node.ProcessModeEnum.Always
+ : Node.ProcessModeEnum.Pausable;
+ }
+
#endregion
-}
\ No newline at end of file
+}
From 053fd4a37179393f0f25ad7905f5d50239fbee55 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Fri, 17 Apr 2026 22:42:37 +0800
Subject: [PATCH 2/6] =?UTF-8?q?feat(ui):=20=E6=B7=BB=E5=8A=A0UI=E8=B7=AF?=
=?UTF-8?q?=E7=94=B1=E5=9F=BA=E7=B1=BB=E5=92=8C=E6=8E=A5=E5=8F=A3=E5=AE=9A?=
=?UTF-8?q?=E4=B9=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现UiRouterBase基类,提供页面栈管理和层级UI管理功能
- 定义IUiRouter接口,规范UI界面导航和切换操作
- 添加UI过渡管道机制,支持UI切换处理器注册和执行
- 实现页面栈操作方法,包括Push、Pop、Replace、Clear等
- 添加层级UI管理功能,支持Overlay、Modal、Toast等浮层显示
- 集成暂停管理功能,实现页面可见性驱动的暂停令牌管理
- 提供UI动作分发机制,支持语义动作捕获和分发
- 实现UI交互配置文件UiInteractionProfile,定义页面交互契约
---
GFramework.Game.Abstractions/UI/IUiRouter.cs | 13 +-
.../UI/UiInteractionProfile.cs | 52 +--
.../UI/UiRouterInteractionTests.cs | 339 ++++++++++++++++++
GFramework.Game/UI/UiInteractionProfiles.cs | 59 +++
GFramework.Game/UI/UiRouterBase.cs | 37 +-
.../UI/CanvasItemUiPageBehaviorBase.cs | 5 +-
6 files changed, 446 insertions(+), 59 deletions(-)
create mode 100644 GFramework.Game.Tests/UI/UiRouterInteractionTests.cs
create mode 100644 GFramework.Game/UI/UiInteractionProfiles.cs
diff --git a/GFramework.Game.Abstractions/UI/IUiRouter.cs b/GFramework.Game.Abstractions/UI/IUiRouter.cs
index 06510560..a84aff7b 100644
--- a/GFramework.Game.Abstractions/UI/IUiRouter.cs
+++ b/GFramework.Game.Abstractions/UI/IUiRouter.cs
@@ -194,10 +194,19 @@ public interface IUiRouter : ISystem
IUiPageBehavior? GetUiActionOwner(UiInputAction action);
///
- /// 尝试把语义动作分发给当前拥有该动作的页面。
+ /// 尝试把语义动作分发给当前拥有该动作捕获权的页面。
///
/// 当前动作。
- /// 如果该动作已被某个页面捕获并消费,则返回 。
+ /// 如果该动作已被某个页面捕获并完成分发,则返回 。
+ bool TryDispatchUiAction(UiInputAction action);
+
+ ///
+ /// 尝试把语义动作分发给当前拥有该动作捕获权的页面。
+ ///
+ /// 当前动作。
+ /// 如果该动作已被某个页面捕获并完成分发,则返回 。
+ [Obsolete(
+ "Use TryDispatchUiAction(UiInputAction action) to emphasize dispatch semantics instead of handler success.")]
bool TryHandleUiAction(UiInputAction action);
///
diff --git a/GFramework.Game.Abstractions/UI/UiInteractionProfile.cs b/GFramework.Game.Abstractions/UI/UiInteractionProfile.cs
index 32069d48..6f2cecb3 100644
--- a/GFramework.Game.Abstractions/UI/UiInteractionProfile.cs
+++ b/GFramework.Game.Abstractions/UI/UiInteractionProfile.cs
@@ -1,18 +1,16 @@
using GFramework.Core.Abstractions.Pause;
-using GFramework.Game.Abstractions.Enums;
namespace GFramework.Game.Abstractions.UI;
///
-/// 描述一个 UI 页面在输入、World 阻断与暂停上的运行时语义。
+/// 描述一个 UI 页面在输入、World 阻断与暂停上的交互契约数据。
///
+///
+/// 该类型仅承载抽象层需要共享的页面交互配置,不包含默认值工厂或动作判定等运行时策略。
+/// 运行时层可在不反向依赖 Abstractions 的前提下,通过专门的 helper 为该 DTO 提供默认值和语义判定。
+///
public sealed class UiInteractionProfile
{
- ///
- /// 获取默认值实例。
- ///
- public static UiInteractionProfile Default { get; } = new();
-
///
/// 声明当前页面要捕获的语义动作集合。
///
@@ -47,44 +45,4 @@ public sealed class UiInteractionProfile
/// 页面向暂停栈登记时使用的原因文本。
///
public string PauseReason { get; init; } = string.Empty;
-
- ///
- /// 判断当前配置是否捕获了指定动作。
- ///
- /// 要查询的语义动作。
- /// 如果当前配置捕获该动作则返回 。
- public bool Captures(UiInputAction action)
- {
- return action switch
- {
- UiInputAction.Cancel => CapturedActions.HasFlag(UiInputActionMask.Cancel),
- UiInputAction.Confirm => CapturedActions.HasFlag(UiInputActionMask.Confirm),
- _ => false
- };
- }
-
- ///
- /// 为指定层级生成默认交互配置。
- ///
- /// UI 层级。
- /// 该层级的默认交互语义。
- public static UiInteractionProfile CreateDefault(UiLayer layer)
- {
- return layer switch
- {
- UiLayer.Modal => new UiInteractionProfile
- {
- CapturedActions = UiInputActionMask.Cancel,
- BlocksWorldPointerInput = true,
- BlocksWorldActionInput = true
- },
- UiLayer.Topmost => new UiInteractionProfile
- {
- CapturedActions = UiInputActionMask.Cancel,
- BlocksWorldPointerInput = true,
- BlocksWorldActionInput = true
- },
- _ => Default
- };
- }
}
diff --git a/GFramework.Game.Tests/UI/UiRouterInteractionTests.cs b/GFramework.Game.Tests/UI/UiRouterInteractionTests.cs
new file mode 100644
index 00000000..205e1d6d
--- /dev/null
+++ b/GFramework.Game.Tests/UI/UiRouterInteractionTests.cs
@@ -0,0 +1,339 @@
+// Copyright (c) 2026 GeWuYou
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System.Reflection;
+using GFramework.Game.UI;
+
+namespace GFramework.Game.Tests.UI;
+
+///
+/// 验证 UI 路由输入语义、层级排序与显示恢复生命周期的回归测试。
+///
+[TestFixture]
+public class UiRouterInteractionTests
+{
+ ///
+ /// 验证模态层和顶层共享同一套阻塞型默认交互配置。
+ ///
+ [Test]
+ public void CreateDefault_ForModalAndTopmost_ReturnsBlockingCancelProfile()
+ {
+ // Arrange
+ var modal = UiInteractionProfiles.CreateDefault(UiLayer.Modal);
+ var topmost = UiInteractionProfiles.CreateDefault(UiLayer.Topmost);
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ Assert.That(modal.CapturedActions, Is.EqualTo(UiInputActionMask.Cancel));
+ Assert.That(modal.BlocksWorldPointerInput, Is.True);
+ Assert.That(modal.BlocksWorldActionInput, Is.True);
+ Assert.That(topmost, Is.SameAs(modal));
+ });
+ }
+
+ ///
+ /// 验证只要动作被页面捕获,路由分发就会返回成功,即使页面没有显式消费该动作。
+ ///
+ [Test]
+ public void TryDispatchUiAction_WhenCapturedButUnhandled_ReturnsTrue()
+ {
+ // Arrange
+ var router = CreateRouter();
+ var page = new TestUiPage("capturing-page", UiLayer.Topmost)
+ {
+ InteractionProfile = new UiInteractionProfile
+ {
+ CapturedActions = UiInputActionMask.Cancel
+ },
+ TryHandleUiActionResult = false
+ };
+
+ router.Show(page, UiLayer.Topmost);
+
+ // Act
+ var dispatched = router.TryDispatchUiAction(UiInputAction.Cancel);
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ Assert.That(dispatched, Is.True);
+ Assert.That(page.TryHandleUiActionCallCount, Is.EqualTo(1));
+ });
+ }
+
+ ///
+ /// 验证层级页面排序使用实例自增序号,而不是依赖固定宽度的字符串顺序。
+ ///
+ [Test]
+ public void GetUiActionOwner_WhenInstanceIdWidthOverflows_UsesNumericOrder()
+ {
+ // Arrange
+ var router = CreateRouter();
+ SetInstanceCounter(router, 999998);
+
+ var olderPage = new TestUiPage("older", UiLayer.Topmost)
+ {
+ InteractionProfile = new UiInteractionProfile
+ {
+ CapturedActions = UiInputActionMask.Cancel
+ }
+ };
+ var newerPage = new TestUiPage("newer", UiLayer.Topmost)
+ {
+ InteractionProfile = new UiInteractionProfile
+ {
+ CapturedActions = UiInputActionMask.Cancel
+ }
+ };
+
+ router.Show(olderPage, UiLayer.Topmost);
+ router.Show(newerPage, UiLayer.Topmost);
+
+ // Act
+ var owner = router.GetUiActionOwner(UiInputAction.Cancel);
+
+ // Assert
+ Assert.That(owner, Is.SameAs(newerPage));
+ }
+
+ ///
+ /// 验证恢复挂起的层级页面时,不会再对依赖 OnShow 触发恢复的页面重复调用 OnResume。
+ ///
+ [Test]
+ public void Resume_WhenPageResumesDuringShow_DoesNotCallResumeTwice()
+ {
+ // Arrange
+ var router = CreateRouter();
+ var page = new TestUiPage("resumable-layer-page", UiLayer.Overlay)
+ {
+ ResumeFromShow = true
+ };
+
+ var handle = router.Show(page, UiLayer.Overlay);
+ router.Hide(handle, UiLayer.Overlay);
+ var resumeCountBeforeResume = page.OnResumeCallCount;
+
+ // Act
+ router.Resume(handle, UiLayer.Overlay);
+
+ // Assert
+ Assert.That(page.OnResumeCallCount, Is.EqualTo(resumeCountBeforeResume + 1));
+ }
+
+ ///
+ /// 验证弹出栈顶页面后,恢复下层页面时不会重复触发恢复逻辑。
+ ///
+ [Test]
+ public async Task PopAsync_WhenPageResumesDuringShow_DoesNotCallResumeTwice()
+ {
+ // Arrange
+ var router = CreateRouter();
+ var underlyingPage = new TestUiPage("underlying-page", UiLayer.Page)
+ {
+ ResumeFromShow = true
+ };
+ var topPage = new TestUiPage("top-page", UiLayer.Page);
+
+ await router.PushAsync(underlyingPage);
+ await router.PushAsync(topPage);
+ var resumeCountBeforePop = underlyingPage.OnResumeCallCount;
+
+ // Act
+ await router.PopAsync(UiPopPolicy.Destroy);
+
+ // Assert
+ Assert.That(underlyingPage.OnResumeCallCount, Is.EqualTo(resumeCountBeforePop + 1));
+ }
+
+ ///
+ /// 创建带有测试根节点的 UI 路由器。
+ ///
+ /// 已绑定测试根节点的路由器实例。
+ private static TestUiRouter CreateRouter()
+ {
+ var router = new TestUiRouter();
+ router.BindRoot(new TestUiRoot());
+ return router;
+ }
+
+ ///
+ /// 把实例计数器调整到指定值,以便覆盖实例标识符宽度溢出的排序回归。
+ ///
+ /// 目标路由器。
+ /// 要写入的计数器值。
+ private static void SetInstanceCounter(UiRouterBase router, int value)
+ {
+ var field = typeof(UiRouterBase).GetField("_instanceCounter", BindingFlags.Instance | BindingFlags.NonPublic);
+ Assert.That(field, Is.Not.Null);
+
+ field!.SetValue(router, value);
+ }
+
+ ///
+ /// 测试用 UI 路由器实现。
+ ///
+ private sealed class TestUiRouter : UiRouterBase
+ {
+ ///
+ /// 注册处理器。
+ ///
+ protected override void RegisterHandlers()
+ {
+ }
+ }
+
+ ///
+ /// 测试用 UI 根节点,占位记录添加/移除操作即可。
+ ///
+ private sealed class TestUiRoot : IUiRoot
+ {
+ ///
+ /// 记录当前挂载的页面集合。
+ ///
+ private readonly List _children = new();
+
+ ///
+ public void AddUiPage(IUiPageBehavior child)
+ {
+ _children.Add(child);
+ }
+
+ ///
+ public void AddUiPage(IUiPageBehavior child, UiLayer layer, int orderInLayer = 0)
+ {
+ _children.Add(child);
+ }
+
+ ///
+ public void RemoveUiPage(IUiPageBehavior child)
+ {
+ _children.Remove(child);
+ }
+ }
+
+ ///
+ /// 可配置的测试页面,用于模拟路由器在不同交互语义下的可观察行为。
+ ///
+ private sealed class TestUiPage : IUiPageBehavior
+ {
+ ///
+ /// 初始化测试页面实例。
+ ///
+ /// 页面键。
+ /// 页面层级。
+ public TestUiPage(string key, UiLayer layer)
+ {
+ Key = key;
+ Layer = layer;
+ InteractionProfile = UiInteractionProfiles.Default;
+ IsAlive = true;
+ }
+
+ ///
+ /// 获取或设置一个值,指示 是否要模拟 `CanvasItemUiPageBehaviorBase` 那样触发恢复逻辑。
+ ///
+ public bool ResumeFromShow { get; init; }
+
+ ///
+ /// 获取或设置页面处理动作时返回的结果。
+ ///
+ public bool TryHandleUiActionResult { get; init; } = true;
+
+ ///
+ /// 记录恢复回调触发次数。
+ ///
+ public int OnResumeCallCount { get; private set; }
+
+ ///
+ /// 记录动作处理方法调用次数。
+ ///
+ public int TryHandleUiActionCallCount { get; private set; }
+
+ ///
+ public UiHandle? Handle { get; set; }
+
+ ///
+ public UiLayer Layer { get; }
+
+ ///
+ public bool IsReentrant { get; init; } = true;
+
+ ///
+ public object View => this;
+
+ ///
+ public bool IsAlive { get; private set; }
+
+ ///
+ public bool IsVisible { get; private set; }
+
+ ///
+ public bool IsModal => Layer == UiLayer.Modal;
+
+ ///
+ public bool BlocksInput { get; init; }
+
+ ///
+ public UiInteractionProfile InteractionProfile { get; init; }
+
+ ///
+ public string Key { get; }
+
+ ///
+ public void OnEnter(IUiPageEnterParam? param)
+ {
+ }
+
+ ///
+ public void OnExit()
+ {
+ IsAlive = false;
+ IsVisible = false;
+ }
+
+ ///
+ public void OnPause()
+ {
+ }
+
+ ///
+ public void OnResume()
+ {
+ OnResumeCallCount++;
+ }
+
+ ///
+ public void OnHide()
+ {
+ IsVisible = false;
+ }
+
+ ///
+ public void OnShow()
+ {
+ IsVisible = true;
+
+ // The Godot page behavior resumes from OnShow(), so the router must not call OnResume() again on top.
+ if (ResumeFromShow)
+ OnResume();
+ }
+
+ ///
+ public bool TryHandleUiAction(UiInputAction action)
+ {
+ TryHandleUiActionCallCount++;
+ return TryHandleUiActionResult;
+ }
+ }
+}
diff --git a/GFramework.Game/UI/UiInteractionProfiles.cs b/GFramework.Game/UI/UiInteractionProfiles.cs
new file mode 100644
index 00000000..224de6b6
--- /dev/null
+++ b/GFramework.Game/UI/UiInteractionProfiles.cs
@@ -0,0 +1,59 @@
+using GFramework.Game.Abstractions.Enums;
+using GFramework.Game.Abstractions.UI;
+
+namespace GFramework.Game.UI;
+
+///
+/// 为 提供运行时默认值与语义判定。
+///
+///
+/// 该 helper 保留在运行时程序集内,避免把默认策略和输入判定逻辑放回 Abstractions。
+/// UI 页面和路由器都应通过这里共享同一套默认语义,避免层级默认值漂移。
+///
+public static class UiInteractionProfiles
+{
+ ///
+ /// 获取不捕获动作、也不阻断 World 输入的默认配置。
+ ///
+ public static UiInteractionProfile Default { get; } = new();
+
+ ///
+ /// 获取会捕获取消动作并阻断 World 输入的阻塞型默认配置。
+ ///
+ public static UiInteractionProfile BlockingCancel { get; } = new()
+ {
+ CapturedActions = UiInputActionMask.Cancel,
+ BlocksWorldPointerInput = true,
+ BlocksWorldActionInput = true
+ };
+
+ ///
+ /// 为指定层级生成默认交互配置。
+ ///
+ /// UI 层级。
+ /// 该层级的默认交互语义。
+ public static UiInteractionProfile CreateDefault(UiLayer layer)
+ {
+ return layer switch
+ {
+ UiLayer.Modal or UiLayer.Topmost => BlockingCancel,
+ _ => Default
+ };
+ }
+
+ ///
+ /// 判断指定配置是否捕获了目标 UI 语义动作。
+ ///
+ /// 目标配置。
+ /// 要查询的动作。
+ /// 如果配置声明捕获了该动作则返回 。
+ public static bool Captures(UiInteractionProfile profile, UiInputAction action)
+ {
+ return action switch
+ {
+ UiInputAction.Cancel => (profile.CapturedActions & UiInputActionMask.Cancel) != 0,
+ UiInputAction.Confirm => (profile.CapturedActions & UiInputActionMask.Confirm) != 0,
+ _ => false
+ };
+ }
+}
diff --git a/GFramework.Game/UI/UiRouterBase.cs b/GFramework.Game/UI/UiRouterBase.cs
index 8bb376e9..34b64132 100644
--- a/GFramework.Game/UI/UiRouterBase.cs
+++ b/GFramework.Game/UI/UiRouterBase.cs
@@ -1,5 +1,3 @@
-using GFramework.Core.Abstractions.Logging;
-using GFramework.Core.Abstractions.Pause;
using GFramework.Core.Extensions;
using GFramework.Game.Abstractions.Enums;
using GFramework.Game.Abstractions.UI;
@@ -361,7 +359,6 @@ public abstract class UiRouterBase : RouterBase page.InteractionProfile.Captures(action));
+ .FirstOrDefault(page => UiInteractionProfiles.Captures(page.InteractionProfile, action));
}
///
@@ -475,7 +472,7 @@ public abstract class UiRouterBase : RouterBase
/// 当前动作。
/// 如果已有页面捕获该动作则返回 。
- public bool TryHandleUiAction(UiInputAction action)
+ public bool TryDispatchUiAction(UiInputAction action)
{
var owner = GetUiActionOwner(action);
if (owner is null)
@@ -488,6 +485,18 @@ public abstract class UiRouterBase : RouterBase
+ /// 尝试将语义动作分发给当前拥有捕获权的页面。
+ ///
+ /// 当前动作。
+ /// 如果已有页面捕获该动作则返回 。
+ [Obsolete(
+ "Use TryDispatchUiAction(UiInputAction action) to emphasize dispatch semantics instead of handler success.")]
+ public bool TryHandleUiAction(UiInputAction action)
+ {
+ return TryDispatchUiAction(action);
+ }
+
///
/// 判断当前可见 UI 是否阻断 World 指针输入。
///
@@ -735,7 +744,6 @@ public abstract class UiRouterBase : RouterBase 0)
{
var next = Stack.Peek();
- next.OnResume();
next.OnShow();
SyncPauseRequest(next, isVisible: true);
}
@@ -764,6 +772,7 @@ public abstract class UiRouterBase : RouterBase pair.Key, StringComparer.Ordinal)
+ // 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))
{
@@ -849,5 +859,18 @@ public abstract class UiRouterBase : RouterBase
+ /// 从实例标识符中提取自增序号,供层内最近显示优先排序使用。
+ ///
+ /// 实例标识符,预期格式为 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
}
diff --git a/GFramework.Godot/UI/CanvasItemUiPageBehaviorBase.cs b/GFramework.Godot/UI/CanvasItemUiPageBehaviorBase.cs
index b7bfafb4..565289e3 100644
--- a/GFramework.Godot/UI/CanvasItemUiPageBehaviorBase.cs
+++ b/GFramework.Godot/UI/CanvasItemUiPageBehaviorBase.cs
@@ -13,7 +13,7 @@
using GFramework.Game.Abstractions.Enums;
using GFramework.Game.Abstractions.UI;
-using GFramework.Godot.Extensions;
+using GFramework.Game.UI;
namespace GFramework.Godot.UI;
@@ -131,7 +131,7 @@ public abstract class CanvasItemUiPageBehaviorBase : IUiPageBehavior
/// 若页面未提供自定义配置,则回退到层级默认值。
///
public UiInteractionProfile InteractionProfile => _profileProvider?.GetUiInteractionProfile(Layer)
- ?? UiInteractionProfile.CreateDefault(Layer);
+ ?? UiInteractionProfiles.CreateDefault(Layer);
#endregion
@@ -213,7 +213,6 @@ public abstract class CanvasItemUiPageBehaviorBase : IUiPageBehavior
public virtual void OnShow()
{
_page?.OnShow();
- ApplyPauseAwareProcessingMode();
Owner.Show();
OnResume();
}
From f011f3158fce5d4f731d498e04fbadf925354145 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Fri, 17 Apr 2026 22:51:34 +0800
Subject: [PATCH 3/6] =?UTF-8?q?refactor(global):=20=E7=BB=9F=E4=B8=80?=
=?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=85=A8=E5=B1=80=E5=91=BD=E5=90=8D=E7=A9=BA?=
=?UTF-8?q?=E9=97=B4=E5=BC=95=E7=94=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在GFramework.Game项目中添加暂停抽象接口的全局引用
- 在GFramework.Game项目中添加日志抽象接口的全局引用
- 从测试项目的UI路由交互测试文件中移除不必要的using声明
- 将测试项目的全局using声明统一到GlobalUsings.cs文件中
- 在GFramework.Godot项目中添加扩展方法的全局引用
- 优化全局命名空间引用的组织结构
---
GFramework.Game.Tests/GlobalUsings.cs | 6 +++++-
GFramework.Game.Tests/UI/UiRouterInteractionTests.cs | 3 ---
GFramework.Game/GlobalUsings.cs | 3 ++-
GFramework.Game/UI/UiRouterBase.cs | 1 +
GFramework.Godot/GlobalUsings.cs | 3 ++-
5 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/GFramework.Game.Tests/GlobalUsings.cs b/GFramework.Game.Tests/GlobalUsings.cs
index 6b9d4d62..1ea2af31 100644
--- a/GFramework.Game.Tests/GlobalUsings.cs
+++ b/GFramework.Game.Tests/GlobalUsings.cs
@@ -16,4 +16,8 @@ global using Moq;
global using System;
global using System.Collections.Generic;
global using System.Linq;
-global using System.Threading.Tasks;
\ No newline at end of file
+global using System.Threading.Tasks;
+global using System.Reflection;
+global using GFramework.Game.Abstractions.Enums;
+global using GFramework.Game.Abstractions.UI;
+global using GFramework.Game.UI;
diff --git a/GFramework.Game.Tests/UI/UiRouterInteractionTests.cs b/GFramework.Game.Tests/UI/UiRouterInteractionTests.cs
index 205e1d6d..160cb9ef 100644
--- a/GFramework.Game.Tests/UI/UiRouterInteractionTests.cs
+++ b/GFramework.Game.Tests/UI/UiRouterInteractionTests.cs
@@ -11,9 +11,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-using System.Reflection;
-using GFramework.Game.UI;
-
namespace GFramework.Game.Tests.UI;
///
diff --git a/GFramework.Game/GlobalUsings.cs b/GFramework.Game/GlobalUsings.cs
index b91413e9..687ce844 100644
--- a/GFramework.Game/GlobalUsings.cs
+++ b/GFramework.Game/GlobalUsings.cs
@@ -20,4 +20,5 @@ global using System.Threading.Tasks;
global using System.Globalization;
global using System.IO;
global using System.Text.Json;
-global using YamlDotNet.RepresentationModel;
\ No newline at end of file
+global using YamlDotNet.RepresentationModel;
+global using GFramework.Core.Abstractions.Logging;
diff --git a/GFramework.Game/UI/UiRouterBase.cs b/GFramework.Game/UI/UiRouterBase.cs
index 34b64132..ede1b51b 100644
--- a/GFramework.Game/UI/UiRouterBase.cs
+++ b/GFramework.Game/UI/UiRouterBase.cs
@@ -1,3 +1,4 @@
+using GFramework.Core.Abstractions.Pause;
using GFramework.Core.Extensions;
using GFramework.Game.Abstractions.Enums;
using GFramework.Game.Abstractions.UI;
diff --git a/GFramework.Godot/GlobalUsings.cs b/GFramework.Godot/GlobalUsings.cs
index 41d45db7..818d6b41 100644
--- a/GFramework.Godot/GlobalUsings.cs
+++ b/GFramework.Godot/GlobalUsings.cs
@@ -16,4 +16,5 @@ global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
-global using Godot;
\ No newline at end of file
+global using Godot;
+global using GFramework.Godot.Extensions;
From eec8dc841281ea2a39592caf4dec3842a8ee01cb Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Fri, 17 Apr 2026 22:58:29 +0800
Subject: [PATCH 4/6] =?UTF-8?q?test(ui):=20=E6=B7=BB=E5=8A=A0=20UI=20?=
=?UTF-8?q?=E8=B7=AF=E7=94=B1=E5=99=A8=E4=BA=A4=E4=BA=92=E8=AF=AD=E4=B9=89?=
=?UTF-8?q?=E5=9B=9E=E5=BD=92=E6=B5=8B=E8=AF=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 验证模态层和顶层共享同一套阻塞型默认交互配置
- 验证页面捕获动作后路由分发返回成功的语义
- 验证层级页面排序使用实例自增序号而非字符串顺序
- 验证恢复挂起页面时避免重复调用 OnResume 的问题
- 验证弹出栈顶页面后恢复下层页面的重复触发问题
- 创建测试用路由器、根节点和可配置页面类
- 实现实例计数器设置功能以覆盖边界条件测试
---
.../UI/UiRouterInteractionTests.cs | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/GFramework.Game.Tests/UI/UiRouterInteractionTests.cs b/GFramework.Game.Tests/UI/UiRouterInteractionTests.cs
index 160cb9ef..618e289d 100644
--- a/GFramework.Game.Tests/UI/UiRouterInteractionTests.cs
+++ b/GFramework.Game.Tests/UI/UiRouterInteractionTests.cs
@@ -161,6 +161,7 @@ public class UiRouterInteractionTests
{
var router = new TestUiRouter();
router.BindRoot(new TestUiRoot());
+ router.InitializeForTests();
return router;
}
@@ -182,6 +183,22 @@ public class UiRouterInteractionTests
///
private sealed class TestUiRouter : UiRouterBase
{
+ ///
+ /// 以测试专用的最小依赖集合执行路由器初始化。
+ ///
+ public void InitializeForTests()
+ {
+ Initialize();
+ }
+
+ ///
+ /// 以测试最小依赖完成初始化,避免把测试绑定到完整的架构 Utility 配置上。
+ ///
+ protected override void OnInit()
+ {
+ RegisterHandlers();
+ }
+
///
/// 注册处理器。
///
From ef8530a379f6694a44f5e33d0896cee1bd8a72dd Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Fri, 17 Apr 2026 23:08:53 +0800
Subject: [PATCH 5/6] =?UTF-8?q?test(UiRouterInteractionTests):=20=E6=9B=B4?=
=?UTF-8?q?=E6=96=B0UI=E8=B7=AF=E7=94=B1=E4=BA=A4=E4=BA=92=E6=B5=8B?=
=?UTF-8?q?=E8=AF=95=E6=96=AD=E8=A8=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 修改modal验证逻辑为验证topmost元素的输入动作掩码、指针输入阻塞和操作输入阻塞属性
- 为_instanceCounter字段查找添加更详细的错误消息以提高调试可读性
- 验证_instanceCounter字段类型是否仍为int类型以确保测试稳定性
- 移除对field变量的null合并操作符调用以简化代码逻辑
---
GFramework.Game.Tests/UI/UiRouterInteractionTests.cs | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/GFramework.Game.Tests/UI/UiRouterInteractionTests.cs b/GFramework.Game.Tests/UI/UiRouterInteractionTests.cs
index 618e289d..dbed8ac1 100644
--- a/GFramework.Game.Tests/UI/UiRouterInteractionTests.cs
+++ b/GFramework.Game.Tests/UI/UiRouterInteractionTests.cs
@@ -35,7 +35,9 @@ public class UiRouterInteractionTests
Assert.That(modal.CapturedActions, Is.EqualTo(UiInputActionMask.Cancel));
Assert.That(modal.BlocksWorldPointerInput, Is.True);
Assert.That(modal.BlocksWorldActionInput, Is.True);
- Assert.That(topmost, Is.SameAs(modal));
+ Assert.That(topmost.CapturedActions, Is.EqualTo(UiInputActionMask.Cancel));
+ Assert.That(topmost.BlocksWorldPointerInput, Is.True);
+ Assert.That(topmost.BlocksWorldActionInput, Is.True);
});
}
@@ -173,9 +175,10 @@ public class UiRouterInteractionTests
private static void SetInstanceCounter(UiRouterBase router, int value)
{
var field = typeof(UiRouterBase).GetField("_instanceCounter", BindingFlags.Instance | BindingFlags.NonPublic);
- Assert.That(field, Is.Not.Null);
+ Assert.That(field, Is.Not.Null, "UiRouterBase._instanceCounter 字段未找到,可能发生了内部重构。");
+ Assert.That(field!.FieldType, Is.EqualTo(typeof(int)), "_instanceCounter 字段类型已变化,请同步调整测试。");
- field!.SetValue(router, value);
+ field.SetValue(router, value);
}
///
From f7117254f82a2965c265416060d3dc0d96b030e4 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Fri, 17 Apr 2026 23:16:04 +0800
Subject: [PATCH 6/6] =?UTF-8?q?fix(tests):=20=E4=BF=AE=E5=A4=8DUI=E8=B7=AF?=
=?UTF-8?q?=E7=94=B1=E4=BA=A4=E4=BA=92=E6=B5=8B=E8=AF=95=E4=B8=AD=E7=9A=84?=
=?UTF-8?q?=E6=96=87=E6=A1=A3=E6=B3=A8=E9=87=8A=E5=92=8C=E4=BB=A3=E7=A0=81?=
=?UTF-8?q?=E5=9D=97=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 为PopAsync测试方法添加了缺失的返回值XML文档注释
- 在Godot页面行为恢复逻辑中添加了正确的代码块括号
- 确保当ResumeFromShow为true时OnResume方法调用的条件判断正确
---
GFramework.Game.Tests/UI/UiRouterInteractionTests.cs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/GFramework.Game.Tests/UI/UiRouterInteractionTests.cs b/GFramework.Game.Tests/UI/UiRouterInteractionTests.cs
index dbed8ac1..0920631d 100644
--- a/GFramework.Game.Tests/UI/UiRouterInteractionTests.cs
+++ b/GFramework.Game.Tests/UI/UiRouterInteractionTests.cs
@@ -133,6 +133,7 @@ public class UiRouterInteractionTests
///
/// 验证弹出栈顶页面后,恢复下层页面时不会重复触发恢复逻辑。
///
+ /// 表示异步测试执行过程的任务。
[Test]
public async Task PopAsync_WhenPageResumesDuringShow_DoesNotCallResumeTwice()
{
@@ -343,7 +344,9 @@ public class UiRouterInteractionTests
// The Godot page behavior resumes from OnShow(), so the router must not call OnResume() again on top.
if (ResumeFromShow)
+ {
OnResume();
+ }
}
///