mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-13 14:14:29 +08:00
feat(ui): 添加UI路由基类和接口定义
- 实现UiRouterBase基类,提供页面栈管理和层级UI管理功能 - 定义IUiRouter接口,规范UI界面导航和切换操作 - 添加UI过渡管道机制,支持UI切换处理器注册和执行 - 实现页面栈操作方法,包括Push、Pop、Replace、Clear等 - 添加层级UI管理功能,支持Overlay、Modal、Toast等浮层显示 - 集成暂停管理功能,实现页面可见性驱动的暂停令牌管理 - 提供UI动作分发机制,支持语义动作捕获和分发 - 实现UI交互配置文件UiInteractionProfile,定义页面交互契约
This commit is contained in:
parent
2e7fd1fc87
commit
053fd4a371
@ -194,10 +194,19 @@ public interface IUiRouter : ISystem
|
|||||||
IUiPageBehavior? GetUiActionOwner(UiInputAction action);
|
IUiPageBehavior? GetUiActionOwner(UiInputAction action);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 尝试把语义动作分发给当前拥有该动作的页面。
|
/// 尝试把语义动作分发给当前拥有该动作捕获权的页面。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="action">当前动作。</param>
|
/// <param name="action">当前动作。</param>
|
||||||
/// <returns>如果该动作已被某个页面捕获并消费,则返回 <see langword="true" />。</returns>
|
/// <returns>如果该动作已被某个页面捕获并完成分发,则返回 <see langword="true" />。</returns>
|
||||||
|
bool TryDispatchUiAction(UiInputAction action);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试把语义动作分发给当前拥有该动作捕获权的页面。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="action">当前动作。</param>
|
||||||
|
/// <returns>如果该动作已被某个页面捕获并完成分发,则返回 <see langword="true" />。</returns>
|
||||||
|
[Obsolete(
|
||||||
|
"Use TryDispatchUiAction(UiInputAction action) to emphasize dispatch semantics instead of handler success.")]
|
||||||
bool TryHandleUiAction(UiInputAction action);
|
bool TryHandleUiAction(UiInputAction action);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -1,18 +1,16 @@
|
|||||||
using GFramework.Core.Abstractions.Pause;
|
using GFramework.Core.Abstractions.Pause;
|
||||||
using GFramework.Game.Abstractions.Enums;
|
|
||||||
|
|
||||||
namespace GFramework.Game.Abstractions.UI;
|
namespace GFramework.Game.Abstractions.UI;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 描述一个 UI 页面在输入、World 阻断与暂停上的运行时语义。
|
/// 描述一个 UI 页面在输入、World 阻断与暂停上的交互契约数据。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 该类型仅承载抽象层需要共享的页面交互配置,不包含默认值工厂或动作判定等运行时策略。
|
||||||
|
/// 运行时层可在不反向依赖 Abstractions 的前提下,通过专门的 helper 为该 DTO 提供默认值和语义判定。
|
||||||
|
/// </remarks>
|
||||||
public sealed class UiInteractionProfile
|
public sealed class UiInteractionProfile
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// 获取默认值实例。
|
|
||||||
/// </summary>
|
|
||||||
public static UiInteractionProfile Default { get; } = new();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 声明当前页面要捕获的语义动作集合。
|
/// 声明当前页面要捕获的语义动作集合。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -47,44 +45,4 @@ public sealed class UiInteractionProfile
|
|||||||
/// 页面向暂停栈登记时使用的原因文本。
|
/// 页面向暂停栈登记时使用的原因文本。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string PauseReason { get; init; } = string.Empty;
|
public string PauseReason { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 判断当前配置是否捕获了指定动作。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="action">要查询的语义动作。</param>
|
|
||||||
/// <returns>如果当前配置捕获该动作则返回 <see langword="true" />。</returns>
|
|
||||||
public bool Captures(UiInputAction action)
|
|
||||||
{
|
|
||||||
return action switch
|
|
||||||
{
|
|
||||||
UiInputAction.Cancel => CapturedActions.HasFlag(UiInputActionMask.Cancel),
|
|
||||||
UiInputAction.Confirm => CapturedActions.HasFlag(UiInputActionMask.Confirm),
|
|
||||||
_ => false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 为指定层级生成默认交互配置。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="layer">UI 层级。</param>
|
|
||||||
/// <returns>该层级的默认交互语义。</returns>
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
339
GFramework.Game.Tests/UI/UiRouterInteractionTests.cs
Normal file
339
GFramework.Game.Tests/UI/UiRouterInteractionTests.cs
Normal file
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 UI 路由输入语义、层级排序与显示恢复生命周期的回归测试。
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture]
|
||||||
|
public class UiRouterInteractionTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 验证模态层和顶层共享同一套阻塞型默认交互配置。
|
||||||
|
/// </summary>
|
||||||
|
[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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证只要动作被页面捕获,路由分发就会返回成功,即使页面没有显式消费该动作。
|
||||||
|
/// </summary>
|
||||||
|
[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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证层级页面排序使用实例自增序号,而不是依赖固定宽度的字符串顺序。
|
||||||
|
/// </summary>
|
||||||
|
[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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证恢复挂起的层级页面时,不会再对依赖 OnShow 触发恢复的页面重复调用 OnResume。
|
||||||
|
/// </summary>
|
||||||
|
[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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证弹出栈顶页面后,恢复下层页面时不会重复触发恢复逻辑。
|
||||||
|
/// </summary>
|
||||||
|
[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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建带有测试根节点的 UI 路由器。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>已绑定测试根节点的路由器实例。</returns>
|
||||||
|
private static TestUiRouter CreateRouter()
|
||||||
|
{
|
||||||
|
var router = new TestUiRouter();
|
||||||
|
router.BindRoot(new TestUiRoot());
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 把实例计数器调整到指定值,以便覆盖实例标识符宽度溢出的排序回归。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="router">目标路由器。</param>
|
||||||
|
/// <param name="value">要写入的计数器值。</param>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试用 UI 路由器实现。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class TestUiRouter : UiRouterBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 注册处理器。
|
||||||
|
/// </summary>
|
||||||
|
protected override void RegisterHandlers()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试用 UI 根节点,占位记录添加/移除操作即可。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class TestUiRoot : IUiRoot
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 记录当前挂载的页面集合。
|
||||||
|
/// </summary>
|
||||||
|
private readonly List<IUiPageBehavior> _children = new();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void AddUiPage(IUiPageBehavior child)
|
||||||
|
{
|
||||||
|
_children.Add(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void AddUiPage(IUiPageBehavior child, UiLayer layer, int orderInLayer = 0)
|
||||||
|
{
|
||||||
|
_children.Add(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void RemoveUiPage(IUiPageBehavior child)
|
||||||
|
{
|
||||||
|
_children.Remove(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 可配置的测试页面,用于模拟路由器在不同交互语义下的可观察行为。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class TestUiPage : IUiPageBehavior
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化测试页面实例。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">页面键。</param>
|
||||||
|
/// <param name="layer">页面层级。</param>
|
||||||
|
public TestUiPage(string key, UiLayer layer)
|
||||||
|
{
|
||||||
|
Key = key;
|
||||||
|
Layer = layer;
|
||||||
|
InteractionProfile = UiInteractionProfiles.Default;
|
||||||
|
IsAlive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置一个值,指示 <see cref="OnShow" /> 是否要模拟 `CanvasItemUiPageBehaviorBase` 那样触发恢复逻辑。
|
||||||
|
/// </summary>
|
||||||
|
public bool ResumeFromShow { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置页面处理动作时返回的结果。
|
||||||
|
/// </summary>
|
||||||
|
public bool TryHandleUiActionResult { get; init; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 记录恢复回调触发次数。
|
||||||
|
/// </summary>
|
||||||
|
public int OnResumeCallCount { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 记录动作处理方法调用次数。
|
||||||
|
/// </summary>
|
||||||
|
public int TryHandleUiActionCallCount { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public UiHandle? Handle { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public UiLayer Layer { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsReentrant { get; init; } = true;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public object View => this;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsAlive { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsVisible { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsModal => Layer == UiLayer.Modal;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool BlocksInput { get; init; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public UiInteractionProfile InteractionProfile { get; init; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Key { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void OnEnter(IUiPageEnterParam? param)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void OnExit()
|
||||||
|
{
|
||||||
|
IsAlive = false;
|
||||||
|
IsVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void OnPause()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void OnResume()
|
||||||
|
{
|
||||||
|
OnResumeCallCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void OnHide()
|
||||||
|
{
|
||||||
|
IsVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool TryHandleUiAction(UiInputAction action)
|
||||||
|
{
|
||||||
|
TryHandleUiActionCallCount++;
|
||||||
|
return TryHandleUiActionResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
GFramework.Game/UI/UiInteractionProfiles.cs
Normal file
59
GFramework.Game/UI/UiInteractionProfiles.cs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
using GFramework.Game.Abstractions.Enums;
|
||||||
|
using GFramework.Game.Abstractions.UI;
|
||||||
|
|
||||||
|
namespace GFramework.Game.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为 <see cref="UiInteractionProfile" /> 提供运行时默认值与语义判定。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 该 helper 保留在运行时程序集内,避免把默认策略和输入判定逻辑放回 Abstractions。
|
||||||
|
/// UI 页面和路由器都应通过这里共享同一套默认语义,避免层级默认值漂移。
|
||||||
|
/// </remarks>
|
||||||
|
public static class UiInteractionProfiles
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取不捕获动作、也不阻断 World 输入的默认配置。
|
||||||
|
/// </summary>
|
||||||
|
public static UiInteractionProfile Default { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取会捕获取消动作并阻断 World 输入的阻塞型默认配置。
|
||||||
|
/// </summary>
|
||||||
|
public static UiInteractionProfile BlockingCancel { get; } = new()
|
||||||
|
{
|
||||||
|
CapturedActions = UiInputActionMask.Cancel,
|
||||||
|
BlocksWorldPointerInput = true,
|
||||||
|
BlocksWorldActionInput = true
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为指定层级生成默认交互配置。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="layer">UI 层级。</param>
|
||||||
|
/// <returns>该层级的默认交互语义。</returns>
|
||||||
|
public static UiInteractionProfile CreateDefault(UiLayer layer)
|
||||||
|
{
|
||||||
|
return layer switch
|
||||||
|
{
|
||||||
|
UiLayer.Modal or UiLayer.Topmost => BlockingCancel,
|
||||||
|
_ => Default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断指定配置是否捕获了目标 UI 语义动作。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="profile">目标配置。</param>
|
||||||
|
/// <param name="action">要查询的动作。</param>
|
||||||
|
/// <returns>如果配置声明捕获了该动作则返回 <see langword="true" />。</returns>
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,3 @@
|
|||||||
using GFramework.Core.Abstractions.Logging;
|
|
||||||
using GFramework.Core.Abstractions.Pause;
|
|
||||||
using GFramework.Core.Extensions;
|
using GFramework.Core.Extensions;
|
||||||
using GFramework.Game.Abstractions.Enums;
|
using GFramework.Game.Abstractions.Enums;
|
||||||
using GFramework.Game.Abstractions.UI;
|
using GFramework.Game.Abstractions.UI;
|
||||||
@ -361,7 +359,6 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
page.OnShow();
|
page.OnShow();
|
||||||
page.OnResume();
|
|
||||||
SyncPauseRequest(page, isVisible: true);
|
SyncPauseRequest(page, isVisible: true);
|
||||||
Log.Debug("Resume UI: instanceId={0}, layer={1}", handle.InstanceId, layer);
|
Log.Debug("Resume UI: instanceId={0}, layer={1}", handle.InstanceId, layer);
|
||||||
}
|
}
|
||||||
@ -467,7 +464,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
|
|||||||
public IUiPageBehavior? GetUiActionOwner(UiInputAction action)
|
public IUiPageBehavior? GetUiActionOwner(UiInputAction action)
|
||||||
{
|
{
|
||||||
return EnumerateVisiblePagesByPriority()
|
return EnumerateVisiblePagesByPriority()
|
||||||
.FirstOrDefault(page => page.InteractionProfile.Captures(action));
|
.FirstOrDefault(page => UiInteractionProfiles.Captures(page.InteractionProfile, action));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -475,7 +472,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="action">当前动作。</param>
|
/// <param name="action">当前动作。</param>
|
||||||
/// <returns>如果已有页面捕获该动作则返回 <see langword="true" />。</returns>
|
/// <returns>如果已有页面捕获该动作则返回 <see langword="true" />。</returns>
|
||||||
public bool TryHandleUiAction(UiInputAction action)
|
public bool TryDispatchUiAction(UiInputAction action)
|
||||||
{
|
{
|
||||||
var owner = GetUiActionOwner(action);
|
var owner = GetUiActionOwner(action);
|
||||||
if (owner is null)
|
if (owner is null)
|
||||||
@ -488,6 +485,18 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试将语义动作分发给当前拥有捕获权的页面。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="action">当前动作。</param>
|
||||||
|
/// <returns>如果已有页面捕获该动作则返回 <see langword="true" />。</returns>
|
||||||
|
[Obsolete(
|
||||||
|
"Use TryDispatchUiAction(UiInputAction action) to emphasize dispatch semantics instead of handler success.")]
|
||||||
|
public bool TryHandleUiAction(UiInputAction action)
|
||||||
|
{
|
||||||
|
return TryDispatchUiAction(action);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 判断当前可见 UI 是否阻断 World 指针输入。
|
/// 判断当前可见 UI 是否阻断 World 指针输入。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -735,7 +744,6 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
|
|||||||
if (Stack.Count > 0)
|
if (Stack.Count > 0)
|
||||||
{
|
{
|
||||||
var next = Stack.Peek();
|
var next = Stack.Peek();
|
||||||
next.OnResume();
|
|
||||||
next.OnShow();
|
next.OnShow();
|
||||||
SyncPauseRequest(next, isVisible: true);
|
SyncPauseRequest(next, isVisible: true);
|
||||||
}
|
}
|
||||||
@ -764,6 +772,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
|
|||||||
catch (InvalidOperationException)
|
catch (InvalidOperationException)
|
||||||
{
|
{
|
||||||
_pauseStackManager = null;
|
_pauseStackManager = null;
|
||||||
|
Log.Debug("PauseStackManager not available. Pause integration is disabled for the UI router.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -841,7 +850,8 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
|
|||||||
yield break;
|
yield break;
|
||||||
|
|
||||||
foreach (var page in layerDict
|
foreach (var page in layerDict
|
||||||
.OrderByDescending(static pair => 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)
|
.Select(static pair => pair.Value)
|
||||||
.Where(static page => page.IsAlive && page.IsVisible))
|
.Where(static page => page.IsAlive && page.IsVisible))
|
||||||
{
|
{
|
||||||
@ -849,5 +859,18 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从实例标识符中提取自增序号,供层内最近显示优先排序使用。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instanceId">实例标识符,预期格式为 <c>ui_000001</c>。</param>
|
||||||
|
/// <returns>提取到的自增序号;若格式异常则返回 <see cref="int.MinValue" />,使异常值排在最后。</returns>
|
||||||
|
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
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
using GFramework.Game.Abstractions.Enums;
|
using GFramework.Game.Abstractions.Enums;
|
||||||
using GFramework.Game.Abstractions.UI;
|
using GFramework.Game.Abstractions.UI;
|
||||||
using GFramework.Godot.Extensions;
|
using GFramework.Game.UI;
|
||||||
|
|
||||||
namespace GFramework.Godot.UI;
|
namespace GFramework.Godot.UI;
|
||||||
|
|
||||||
@ -131,7 +131,7 @@ public abstract class CanvasItemUiPageBehaviorBase<T> : IUiPageBehavior
|
|||||||
/// 若页面未提供自定义配置,则回退到层级默认值。
|
/// 若页面未提供自定义配置,则回退到层级默认值。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public UiInteractionProfile InteractionProfile => _profileProvider?.GetUiInteractionProfile(Layer)
|
public UiInteractionProfile InteractionProfile => _profileProvider?.GetUiInteractionProfile(Layer)
|
||||||
?? UiInteractionProfile.CreateDefault(Layer);
|
?? UiInteractionProfiles.CreateDefault(Layer);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@ -213,7 +213,6 @@ public abstract class CanvasItemUiPageBehaviorBase<T> : IUiPageBehavior
|
|||||||
public virtual void OnShow()
|
public virtual void OnShow()
|
||||||
{
|
{
|
||||||
_page?.OnShow();
|
_page?.OnShow();
|
||||||
ApplyPauseAwareProcessingMode();
|
|
||||||
Owner.Show();
|
Owner.Show();
|
||||||
OnResume();
|
OnResume();
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user