Integrate XCUI shell state and runtime frame seams

This commit is contained in:
2026-04-05 12:50:55 +08:00
parent ec97445071
commit e5e9f348a3
29 changed files with 3183 additions and 102 deletions

View File

@@ -2,6 +2,7 @@
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
#include <XCEngine/UI/Runtime/UISceneRuntimeContext.h>
#include <XCEngine/UI/Runtime/UIScreenStackController.h>
#include <XCEngine/UI/Runtime/UISystem.h>
@@ -9,12 +10,14 @@
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
namespace {
using XCEngine::UI::Runtime::UIScreenAsset;
using XCEngine::UI::Runtime::UIScreenFrameInput;
using XCEngine::UI::Runtime::UIScreenPlayer;
using XCEngine::UI::Runtime::UISceneRuntimeContext;
using XCEngine::UI::Runtime::UIDocumentScreenHost;
using XCEngine::UI::Runtime::UIScreenStackController;
using XCEngine::UI::Runtime::UISystem;
@@ -93,6 +96,69 @@ UIScreenFrameInput BuildInputState(std::uint64_t frameIndex = 1u) {
return input;
}
const XCEngine::UI::Runtime::UISystemPresentedLayer* FindPresentedLayerById(
const XCEngine::UI::Runtime::UISystemFrameResult& frame,
XCEngine::UI::Runtime::UIScreenLayerId layerId) {
for (const XCEngine::UI::Runtime::UISystemPresentedLayer& layer : frame.layers) {
if (layer.layerId == layerId) {
return &layer;
}
}
return nullptr;
}
class RecordingDocumentHost final : public XCEngine::UI::Runtime::IUIScreenDocumentHost {
public:
struct BuildCall {
std::string displayName = {};
UIScreenFrameInput input = {};
};
XCEngine::UI::Runtime::UIScreenLoadResult LoadScreen(const UIScreenAsset& asset) override {
XCEngine::UI::Runtime::UIScreenLoadResult result = {};
result.succeeded = asset.IsValid();
result.document.sourcePath = asset.documentPath;
result.document.displayName = asset.screenId.empty() ? asset.documentPath : asset.screenId;
return result;
}
XCEngine::UI::Runtime::UIScreenFrameResult BuildFrame(
const XCEngine::UI::Runtime::UIScreenDocument& document,
const UIScreenFrameInput& input) override {
m_buildCalls.push_back(BuildCall{ document.displayName, input });
XCEngine::UI::Runtime::UIScreenFrameResult result = {};
result.stats.documentLoaded = true;
result.stats.inputEventCount = input.events.size();
result.stats.presentedFrameIndex = input.frameIndex;
XCEngine::UI::UIDrawList& drawList = result.drawData.EmplaceDrawList(document.displayName);
drawList.AddText(
XCEngine::UI::UIPoint(input.viewportRect.x, input.viewportRect.y),
document.displayName);
result.stats.drawListCount = result.drawData.GetDrawListCount();
result.stats.commandCount = result.drawData.GetTotalCommandCount();
return result;
}
const BuildCall* FindBuildCall(const std::string& displayName) const {
for (const BuildCall& call : m_buildCalls) {
if (call.displayName == displayName) {
return &call;
}
}
return nullptr;
}
std::size_t GetBuildCallCount() const {
return m_buildCalls.size();
}
private:
std::vector<BuildCall> m_buildCalls = {};
};
} // namespace
TEST(UIRuntimeTest, ScreenPlayerBuildsDrawDataFromDocumentTree) {
@@ -115,6 +181,52 @@ TEST(UIRuntimeTest, ScreenPlayerBuildsDrawDataFromDocumentTree) {
EXPECT_EQ(player.GetPresentedFrameCount(), 1u);
}
TEST(UIRuntimeTest, ScreenPlayerConsumeLastFrameReturnsDetachedPacketAndClearsBorrowedState) {
TempFileScope viewFile("xcui_runtime_consume_player", ".xcui", BuildViewMarkup("Runtime Consume"));
UIDocumentScreenHost host = {};
UIScreenPlayer player(host);
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.consume.player")));
const auto& firstFrame = player.Update(BuildInputState(2u));
ASSERT_TRUE(firstFrame.stats.documentLoaded);
EXPECT_EQ(firstFrame.stats.presentedFrameIndex, 2u);
EXPECT_TRUE(DrawDataContainsText(firstFrame.drawData, "Runtime Consume"));
XCEngine::UI::Runtime::UIScreenFrameResult consumedFrame = player.ConsumeLastFrame();
EXPECT_TRUE(consumedFrame.stats.documentLoaded);
EXPECT_EQ(consumedFrame.stats.presentedFrameIndex, 2u);
EXPECT_EQ(consumedFrame.stats.drawListCount, consumedFrame.drawData.GetDrawListCount());
EXPECT_EQ(consumedFrame.stats.commandCount, consumedFrame.drawData.GetTotalCommandCount());
EXPECT_TRUE(DrawDataContainsText(consumedFrame.drawData, "Runtime Consume"));
EXPECT_EQ(player.GetPresentedFrameCount(), 1u);
const auto& clearedFrame = player.GetLastFrame();
EXPECT_FALSE(clearedFrame.stats.documentLoaded);
EXPECT_EQ(clearedFrame.stats.presentedFrameIndex, 0u);
EXPECT_EQ(clearedFrame.drawData.GetDrawListCount(), 0u);
EXPECT_TRUE(clearedFrame.errorMessage.empty());
const auto& secondFrame = player.Update(BuildInputState(3u));
EXPECT_TRUE(secondFrame.stats.documentLoaded);
EXPECT_EQ(secondFrame.stats.presentedFrameIndex, 3u);
EXPECT_TRUE(DrawDataContainsText(secondFrame.drawData, "Runtime Consume"));
EXPECT_EQ(player.GetPresentedFrameCount(), 2u);
EXPECT_EQ(consumedFrame.stats.presentedFrameIndex, 2u);
EXPECT_TRUE(DrawDataContainsText(consumedFrame.drawData, "Runtime Consume"));
const XCEngine::UI::Runtime::UIScreenFrameResult emptyFrame = player.ConsumeLastFrame();
EXPECT_TRUE(emptyFrame.stats.documentLoaded);
EXPECT_EQ(emptyFrame.stats.presentedFrameIndex, 3u);
EXPECT_TRUE(DrawDataContainsText(emptyFrame.drawData, "Runtime Consume"));
const XCEngine::UI::Runtime::UIScreenFrameResult clearedAgain = player.ConsumeLastFrame();
EXPECT_FALSE(clearedAgain.stats.documentLoaded);
EXPECT_EQ(clearedAgain.stats.presentedFrameIndex, 0u);
EXPECT_EQ(clearedAgain.drawData.GetDrawListCount(), 0u);
}
TEST(UIRuntimeTest, UISystemForwardsActiveScreenToPlayer) {
TempFileScope baseView("xcui_runtime_base", ".xcui", BuildViewMarkup("Base Screen"));
TempFileScope overlayView("xcui_runtime_overlay", ".xcui", BuildViewMarkup("Overlay Screen", "Modal Dialog"));
@@ -236,3 +348,224 @@ TEST(UIRuntimeTest, ScreenStackControllerReplaceTopKeepsPreviousScreenWhenReplac
EXPECT_EQ(frame.presentedLayerCount, 1u);
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Pause Menu"));
}
TEST(UIRuntimeTest, RoutesViewportAndFocusOnlyToTopInteractiveVisibleLayer) {
RecordingDocumentHost host = {};
UISystem system(host);
XCEngine::UI::Runtime::UIScreenLayerOptions gameplayOptions = {};
gameplayOptions.debugName = "gameplay";
gameplayOptions.acceptsInput = true;
gameplayOptions.blocksLayersBelow = false;
XCEngine::UI::Runtime::UIScreenLayerOptions overlayOptions = {};
overlayOptions.debugName = "overlay";
overlayOptions.acceptsInput = true;
overlayOptions.blocksLayersBelow = false;
const auto gameplayLayerId = system.PushScreen(
BuildScreenAsset(fs::path("gameplay_view.xcui"), "runtime.gameplay"),
gameplayOptions);
const auto overlayLayerId = system.PushScreen(
BuildScreenAsset(fs::path("overlay_view.xcui"), "runtime.overlay"),
overlayOptions);
ASSERT_NE(gameplayLayerId, 0u);
ASSERT_NE(overlayLayerId, 0u);
UIScreenFrameInput input = BuildInputState(8u);
input.viewportRect = XCEngine::UI::UIRect(15.0f, 25.0f, 1024.0f, 576.0f);
XCEngine::UI::UIInputEvent textEvent = {};
textEvent.type = XCEngine::UI::UIInputEventType::Character;
textEvent.character = 'I';
input.events.push_back(textEvent);
const auto& frame = system.Update(input);
ASSERT_EQ(frame.presentedLayerCount, 2u);
ASSERT_EQ(frame.layers.size(), 2u);
ASSERT_EQ(host.GetBuildCallCount(), 2u);
const auto* gameplayCall = host.FindBuildCall("runtime.gameplay");
const auto* overlayCall = host.FindBuildCall("runtime.overlay");
ASSERT_NE(gameplayCall, nullptr);
ASSERT_NE(overlayCall, nullptr);
EXPECT_EQ(gameplayCall->input.viewportRect.x, input.viewportRect.x);
EXPECT_EQ(gameplayCall->input.viewportRect.y, input.viewportRect.y);
EXPECT_EQ(gameplayCall->input.viewportRect.width, input.viewportRect.width);
EXPECT_EQ(gameplayCall->input.viewportRect.height, input.viewportRect.height);
EXPECT_TRUE(gameplayCall->input.events.empty());
EXPECT_FALSE(gameplayCall->input.focused);
EXPECT_EQ(gameplayCall->input.frameIndex, input.frameIndex);
EXPECT_EQ(overlayCall->input.viewportRect.x, input.viewportRect.x);
EXPECT_EQ(overlayCall->input.viewportRect.y, input.viewportRect.y);
EXPECT_EQ(overlayCall->input.viewportRect.width, input.viewportRect.width);
EXPECT_EQ(overlayCall->input.viewportRect.height, input.viewportRect.height);
EXPECT_EQ(overlayCall->input.events.size(), 1u);
EXPECT_TRUE(overlayCall->input.focused);
EXPECT_EQ(overlayCall->input.frameIndex, input.frameIndex);
const auto* gameplayLayer = FindPresentedLayerById(frame, gameplayLayerId);
const auto* overlayLayer = FindPresentedLayerById(frame, overlayLayerId);
ASSERT_NE(gameplayLayer, nullptr);
ASSERT_NE(overlayLayer, nullptr);
EXPECT_EQ(gameplayLayer->stats.inputEventCount, 0u);
EXPECT_EQ(overlayLayer->stats.inputEventCount, 1u);
}
TEST(UIRuntimeTest, HiddenTopLayerLeavesUnderlyingLayerFocusedAndInteractive) {
RecordingDocumentHost host = {};
UISystem system(host);
XCEngine::UI::Runtime::UIScreenLayerOptions visibleOptions = {};
visibleOptions.debugName = "visible";
visibleOptions.acceptsInput = true;
visibleOptions.blocksLayersBelow = false;
XCEngine::UI::Runtime::UIScreenLayerOptions hiddenOptions = {};
hiddenOptions.debugName = "hidden";
hiddenOptions.visible = false;
hiddenOptions.acceptsInput = true;
hiddenOptions.blocksLayersBelow = false;
const auto visibleLayerId = system.PushScreen(
BuildScreenAsset(fs::path("visible_view.xcui"), "runtime.visible"),
visibleOptions);
const auto hiddenLayerId = system.PushScreen(
BuildScreenAsset(fs::path("hidden_view.xcui"), "runtime.hidden"),
hiddenOptions);
ASSERT_NE(visibleLayerId, 0u);
ASSERT_NE(hiddenLayerId, 0u);
UIScreenFrameInput input = BuildInputState(9u);
input.viewportRect = XCEngine::UI::UIRect(40.0f, 60.0f, 700.0f, 420.0f);
XCEngine::UI::UIInputEvent keyEvent = {};
keyEvent.type = XCEngine::UI::UIInputEventType::KeyDown;
keyEvent.keyCode = 32;
input.events.push_back(keyEvent);
const auto& frame = system.Update(input);
ASSERT_EQ(frame.presentedLayerCount, 1u);
ASSERT_EQ(frame.skippedLayerCount, 1u);
ASSERT_EQ(frame.layers.size(), 1u);
ASSERT_EQ(host.GetBuildCallCount(), 1u);
const auto* visibleCall = host.FindBuildCall("runtime.visible");
ASSERT_NE(visibleCall, nullptr);
EXPECT_EQ(visibleCall->input.viewportRect.x, input.viewportRect.x);
EXPECT_EQ(visibleCall->input.viewportRect.y, input.viewportRect.y);
EXPECT_EQ(visibleCall->input.viewportRect.width, input.viewportRect.width);
EXPECT_EQ(visibleCall->input.viewportRect.height, input.viewportRect.height);
EXPECT_EQ(visibleCall->input.events.size(), 1u);
EXPECT_TRUE(visibleCall->input.focused);
EXPECT_EQ(host.FindBuildCall("runtime.hidden"), nullptr);
const auto* visibleLayer = FindPresentedLayerById(frame, visibleLayerId);
ASSERT_NE(visibleLayer, nullptr);
EXPECT_EQ(visibleLayer->stats.inputEventCount, 1u);
EXPECT_EQ(FindPresentedLayerById(frame, hiddenLayerId), nullptr);
}
TEST(UIRuntimeTest, UISystemConsumeLastFrameReturnsDetachedPresentationPacket) {
RecordingDocumentHost host = {};
UISystem system(host);
XCEngine::UI::Runtime::UIScreenLayerOptions options = {};
options.debugName = "runtime";
const auto layerId = system.PushScreen(
BuildScreenAsset(fs::path("runtime_consume_view.xcui"), "runtime.consume"),
options);
ASSERT_NE(layerId, 0u);
UIScreenFrameInput input = BuildInputState(12u);
input.viewportRect = XCEngine::UI::UIRect(48.0f, 72.0f, 1280.0f, 720.0f);
input.deltaTimeSeconds = 1.0 / 30.0;
XCEngine::UI::UIInputEvent textEvent = {};
textEvent.type = XCEngine::UI::UIInputEventType::Character;
textEvent.character = 'R';
input.events.push_back(textEvent);
const auto& frame = system.Update(input);
ASSERT_EQ(frame.presentedLayerCount, 1u);
EXPECT_EQ(frame.viewportRect.x, input.viewportRect.x);
EXPECT_EQ(frame.viewportRect.y, input.viewportRect.y);
EXPECT_EQ(frame.viewportRect.width, input.viewportRect.width);
EXPECT_EQ(frame.viewportRect.height, input.viewportRect.height);
EXPECT_EQ(frame.submittedInputEventCount, 1u);
EXPECT_DOUBLE_EQ(frame.deltaTimeSeconds, input.deltaTimeSeconds);
EXPECT_TRUE(frame.focused);
XCEngine::UI::Runtime::UISystemFrameResult consumedFrame = system.ConsumeLastFrame();
EXPECT_EQ(consumedFrame.frameIndex, input.frameIndex);
EXPECT_EQ(consumedFrame.presentedLayerCount, 1u);
EXPECT_EQ(consumedFrame.layers.size(), 1u);
EXPECT_EQ(consumedFrame.layers.front().layerId, layerId);
EXPECT_EQ(consumedFrame.viewportRect.x, input.viewportRect.x);
EXPECT_EQ(consumedFrame.viewportRect.y, input.viewportRect.y);
EXPECT_EQ(consumedFrame.viewportRect.width, input.viewportRect.width);
EXPECT_EQ(consumedFrame.viewportRect.height, input.viewportRect.height);
EXPECT_EQ(consumedFrame.submittedInputEventCount, 1u);
EXPECT_DOUBLE_EQ(consumedFrame.deltaTimeSeconds, input.deltaTimeSeconds);
EXPECT_TRUE(consumedFrame.focused);
EXPECT_TRUE(DrawDataContainsText(consumedFrame.drawData, "runtime.consume"));
const auto& clearedFrame = system.GetLastFrame();
EXPECT_EQ(clearedFrame.frameIndex, 0u);
EXPECT_EQ(clearedFrame.presentedLayerCount, 0u);
EXPECT_EQ(clearedFrame.submittedInputEventCount, 0u);
EXPECT_TRUE(clearedFrame.layers.empty());
EXPECT_EQ(clearedFrame.drawData.GetDrawListCount(), 0u);
const XCEngine::UI::Runtime::UISystemFrameResult emptyFrame = system.ConsumeLastFrame();
EXPECT_EQ(emptyFrame.frameIndex, 0u);
EXPECT_TRUE(emptyFrame.layers.empty());
EXPECT_EQ(emptyFrame.drawData.GetDrawListCount(), 0u);
}
TEST(UIRuntimeTest, SceneRuntimeContextConsumeLastFrameForwardsPresentationSnapshot) {
TempFileScope viewFile("xcui_runtime_context", ".xcui", BuildViewMarkup("Runtime Context"));
UISceneRuntimeContext runtimeContext = {};
const auto layerId = runtimeContext.GetStackController().PushMenu(
BuildScreenAsset(viewFile.Path(), "runtime.context"),
"context");
ASSERT_NE(layerId, 0u);
const XCEngine::UI::UIRect viewportRect(24.0f, 32.0f, 960.0f, 540.0f);
runtimeContext.SetViewportRect(viewportRect);
runtimeContext.SetFocused(true);
XCEngine::UI::UIInputEvent textEvent = {};
textEvent.type = XCEngine::UI::UIInputEventType::Character;
textEvent.character = 'C';
runtimeContext.QueueInputEvent(textEvent);
runtimeContext.Update(0.25);
XCEngine::UI::Runtime::UISystemFrameResult consumedFrame = runtimeContext.ConsumeLastFrame();
EXPECT_EQ(consumedFrame.frameIndex, 1u);
EXPECT_EQ(consumedFrame.presentedLayerCount, 1u);
EXPECT_EQ(consumedFrame.layers.size(), 1u);
EXPECT_EQ(consumedFrame.layers.front().layerId, layerId);
EXPECT_EQ(consumedFrame.viewportRect.x, viewportRect.x);
EXPECT_EQ(consumedFrame.viewportRect.y, viewportRect.y);
EXPECT_EQ(consumedFrame.viewportRect.width, viewportRect.width);
EXPECT_EQ(consumedFrame.viewportRect.height, viewportRect.height);
EXPECT_EQ(consumedFrame.submittedInputEventCount, 1u);
EXPECT_DOUBLE_EQ(consumedFrame.deltaTimeSeconds, 0.25);
EXPECT_TRUE(consumedFrame.focused);
EXPECT_TRUE(DrawDataContainsText(consumedFrame.drawData, "Runtime Context"));
const auto& clearedFrame = runtimeContext.GetLastFrame();
EXPECT_EQ(clearedFrame.frameIndex, 0u);
EXPECT_EQ(clearedFrame.presentedLayerCount, 0u);
EXPECT_TRUE(clearedFrame.layers.empty());
EXPECT_EQ(clearedFrame.drawData.GetDrawListCount(), 0u);
runtimeContext.Update(0.5);
const auto& secondFrame = runtimeContext.GetLastFrame();
EXPECT_EQ(secondFrame.frameIndex, 2u);
EXPECT_EQ(secondFrame.submittedInputEventCount, 0u);
EXPECT_DOUBLE_EQ(secondFrame.deltaTimeSeconds, 0.5);
EXPECT_TRUE(secondFrame.focused);
}