Add XCUI runtime screen layer and demo textarea

This commit is contained in:
2026-04-05 05:14:16 +08:00
parent 67a28bdd4a
commit ade5be31d6
18 changed files with 2042 additions and 37 deletions

View File

@@ -0,0 +1,326 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
#include <XCEngine/UI/Runtime/UISystem.h>
#include <filesystem>
#include <fstream>
namespace {
namespace fs = std::filesystem;
using XCEngine::UI::UIColor;
using XCEngine::UI::UIDrawList;
using XCEngine::UI::UIInputEvent;
using XCEngine::UI::UIInputEventType;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
using XCEngine::UI::Runtime::IUIScreenDocumentHost;
using XCEngine::UI::Runtime::UIScreenAsset;
using XCEngine::UI::Runtime::UIScreenDocument;
using XCEngine::UI::Runtime::UIScreenFrameInput;
using XCEngine::UI::Runtime::UIScreenFrameResult;
using XCEngine::UI::Runtime::UIScreenLayerId;
using XCEngine::UI::Runtime::UIScreenLayerOptions;
using XCEngine::UI::Runtime::UIScreenLoadResult;
using XCEngine::UI::Runtime::UIScreenPlayer;
using XCEngine::UI::Runtime::UIDocumentScreenHost;
using XCEngine::UI::Runtime::UISystemFrameResult;
using XCEngine::UI::Runtime::UISystem;
class FakeScreenDocumentHost final : public IUIScreenDocumentHost {
public:
struct BuildCall {
std::string displayName = {};
std::size_t inputEventCount = 0;
std::uint64_t frameIndex = 0;
};
UIScreenLoadResult LoadScreen(const UIScreenAsset& asset) override {
++loadCount;
lastLoadedAsset = asset;
UIScreenLoadResult result = {};
if (!asset.IsValid()) {
result.errorMessage = "Invalid screen asset.";
return result;
}
result.succeeded = true;
result.document.sourcePath = asset.documentPath;
result.document.displayName = asset.screenId.empty() ? asset.documentPath : asset.screenId;
result.document.viewDocument.valid = true;
result.document.dependencies.push_back(asset.themePath);
return result;
}
UIScreenFrameResult BuildFrame(
const UIScreenDocument& document,
const UIScreenFrameInput& input) override {
++buildCount;
lastBuiltDocument = document;
lastFrameInput = input;
UIScreenFrameResult result = {};
UIDrawList& drawList = result.drawData.EmplaceDrawList(document.displayName);
drawList.AddFilledRect(input.viewportRect, UIColor(0.2f, 0.3f, 0.4f, 1.0f), 4.0f);
drawList.AddText(
UIPoint(input.viewportRect.x + 8.0f, input.viewportRect.y + 8.0f),
document.displayName,
UIColor(1.0f, 1.0f, 1.0f, 1.0f),
16.0f);
buildCalls.push_back(BuildCall{
document.displayName,
input.events.size(),
input.frameIndex
});
return result;
}
std::size_t loadCount = 0;
std::size_t buildCount = 0;
UIScreenAsset lastLoadedAsset = {};
UIScreenDocument lastBuiltDocument = {};
UIScreenFrameInput lastFrameInput = {};
std::vector<BuildCall> buildCalls = {};
};
UIScreenAsset MakeAsset() {
UIScreenAsset asset = {};
asset.screenId = "MainMenu";
asset.documentPath = "Assets/UI/MainMenu.xcui";
asset.themePath = "Assets/UI/MainMenu.xctheme";
return asset;
}
UIScreenFrameInput MakeFrameInput(std::uint64_t frameIndex = 7) {
UIScreenFrameInput input = {};
input.viewportRect = UIRect(10.0f, 20.0f, 320.0f, 180.0f);
input.deltaTimeSeconds = 1.0 / 60.0;
input.frameIndex = frameIndex;
input.focused = true;
UIInputEvent event = {};
event.type = UIInputEventType::PointerMove;
event.position = UIPoint(42.0f, 64.0f);
input.events.push_back(event);
return input;
}
void WriteTextFile(const fs::path& path, const char* contents) {
fs::create_directories(path.parent_path());
std::ofstream output(path, std::ios::binary | std::ios::trunc);
ASSERT_TRUE(output.is_open());
output << contents;
ASSERT_TRUE(static_cast<bool>(output));
}
TEST(UIRuntimeTest, ScreenPlayerLoadsAssetAndDocumentMetadata) {
FakeScreenDocumentHost host = {};
UIScreenPlayer player(host);
ASSERT_TRUE(player.Load(MakeAsset()));
ASSERT_TRUE(player.IsLoaded());
ASSERT_NE(player.GetAsset(), nullptr);
ASSERT_NE(player.GetDocument(), nullptr);
EXPECT_EQ(player.GetAsset()->documentPath, "Assets/UI/MainMenu.xcui");
EXPECT_EQ(player.GetDocument()->displayName, "MainMenu");
EXPECT_EQ(host.loadCount, 1u);
}
TEST(UIRuntimeTest, ScreenPlayerUpdateBuildsFrameAndTracksStats) {
FakeScreenDocumentHost host = {};
UIScreenPlayer player(host);
ASSERT_TRUE(player.Load(MakeAsset()));
const UIScreenFrameResult& result = player.Update(MakeFrameInput(12));
EXPECT_TRUE(result.errorMessage.empty());
EXPECT_TRUE(result.stats.documentLoaded);
EXPECT_EQ(result.stats.drawListCount, 1u);
EXPECT_EQ(result.stats.commandCount, 2u);
EXPECT_EQ(result.stats.inputEventCount, 1u);
EXPECT_EQ(result.stats.presentedFrameIndex, 12u);
EXPECT_EQ(player.GetPresentedFrameCount(), 1u);
EXPECT_EQ(host.buildCount, 1u);
EXPECT_EQ(host.lastBuiltDocument.displayName, "MainMenu");
EXPECT_EQ(host.lastFrameInput.viewportRect.width, 320.0f);
}
TEST(UIRuntimeTest, ScreenPlayerWithoutLoadedDocumentReturnsNotLoadedFrame) {
FakeScreenDocumentHost host = {};
UIScreenPlayer player(host);
const UIScreenFrameResult& result = player.Update(MakeFrameInput());
EXPECT_FALSE(result.stats.documentLoaded);
EXPECT_TRUE(result.drawData.Empty());
EXPECT_FALSE(result.errorMessage.empty());
EXPECT_EQ(player.GetPresentedFrameCount(), 0u);
EXPECT_EQ(host.buildCount, 0u);
}
TEST(UIRuntimeTest, UISystemTicksAllCreatedPlayers) {
FakeScreenDocumentHost host = {};
UISystem system(host);
UIScreenPlayer& playerA = system.CreatePlayer();
UIScreenPlayer& playerB = system.CreatePlayer();
ASSERT_TRUE(playerA.Load(MakeAsset()));
UIScreenAsset hudAsset = MakeAsset();
hudAsset.screenId = "HUD";
hudAsset.documentPath = "Assets/UI/Hud.xcui";
ASSERT_TRUE(playerB.Load(hudAsset));
system.Tick(MakeFrameInput(21));
EXPECT_EQ(system.GetPlayerCount(), 2u);
EXPECT_EQ(host.loadCount, 2u);
EXPECT_EQ(host.buildCount, 2u);
EXPECT_EQ(playerA.GetLastFrame().stats.presentedFrameIndex, 21u);
EXPECT_EQ(playerB.GetLastFrame().stats.presentedFrameIndex, 21u);
}
TEST(UIRuntimeTest, UISystemUpdateComposesLayersAndRoutesInputToTopInteractiveLayer) {
FakeScreenDocumentHost host = {};
UISystem system(host);
UIScreenPlayer& gameplay = system.CreatePlayer();
ASSERT_TRUE(gameplay.Load(MakeAsset()));
UIScreenAsset hudAsset = MakeAsset();
hudAsset.screenId = "HUD";
hudAsset.documentPath = "Assets/UI/Hud.xcui";
UIScreenLayerOptions hudOptions = {};
hudOptions.debugName = "HUD";
hudOptions.acceptsInput = false;
UIScreenPlayer& hud = system.CreatePlayer(hudOptions);
ASSERT_TRUE(hud.Load(hudAsset));
const UISystemFrameResult& frame = system.Update(MakeFrameInput(33));
ASSERT_EQ(frame.presentedLayerCount, 2u);
EXPECT_EQ(frame.skippedLayerCount, 0u);
ASSERT_EQ(frame.layers.size(), 2u);
EXPECT_EQ(frame.layers[0].asset.screenId, "MainMenu");
EXPECT_EQ(frame.layers[1].asset.screenId, "HUD");
EXPECT_EQ(frame.layers[0].stats.inputEventCount, 1u);
EXPECT_EQ(frame.layers[1].stats.inputEventCount, 0u);
ASSERT_EQ(frame.drawData.GetDrawListCount(), 2u);
EXPECT_EQ(frame.drawData.GetDrawLists()[0].GetDebugName(), "MainMenu");
EXPECT_EQ(frame.drawData.GetDrawLists()[1].GetDebugName(), "HUD");
ASSERT_EQ(host.buildCalls.size(), 2u);
EXPECT_EQ(host.buildCalls[0].displayName, "MainMenu");
EXPECT_EQ(host.buildCalls[0].inputEventCount, 1u);
EXPECT_EQ(host.buildCalls[1].displayName, "HUD");
EXPECT_EQ(host.buildCalls[1].inputEventCount, 0u);
}
TEST(UIRuntimeTest, UISystemModalLayerBlocksLowerLayersAndKeepsOnlyTopFrameVisible) {
FakeScreenDocumentHost host = {};
UISystem system(host);
UIScreenAsset gameplayAsset = MakeAsset();
gameplayAsset.screenId = "GameplayHUD";
gameplayAsset.documentPath = "Assets/UI/GameplayHud.xcui";
const UIScreenLayerId gameplayLayer = system.PushScreen(gameplayAsset);
ASSERT_NE(gameplayLayer, 0u);
UIScreenAsset pauseAsset = MakeAsset();
pauseAsset.screenId = "PauseMenu";
pauseAsset.documentPath = "Assets/UI/PauseMenu.xcui";
UIScreenLayerOptions pauseOptions = {};
pauseOptions.debugName = "PauseMenu";
pauseOptions.blocksLayersBelow = true;
const UIScreenLayerId pauseLayer = system.PushScreen(pauseAsset, pauseOptions);
ASSERT_NE(pauseLayer, 0u);
const UISystemFrameResult& frame = system.Update(MakeFrameInput(48));
EXPECT_EQ(system.GetLayerCount(), 2u);
EXPECT_EQ(frame.presentedLayerCount, 1u);
EXPECT_EQ(frame.skippedLayerCount, 1u);
ASSERT_EQ(frame.layers.size(), 1u);
EXPECT_EQ(frame.layers[0].layerId, pauseLayer);
EXPECT_EQ(frame.layers[0].asset.screenId, "PauseMenu");
EXPECT_TRUE(frame.layers[0].options.blocksLayersBelow);
ASSERT_EQ(frame.drawData.GetDrawListCount(), 1u);
EXPECT_EQ(frame.drawData.GetDrawLists()[0].GetDebugName(), "PauseMenu");
ASSERT_EQ(host.buildCalls.size(), 1u);
EXPECT_EQ(host.buildCalls[0].displayName, "PauseMenu");
EXPECT_EQ(host.buildCalls[0].inputEventCount, 1u);
}
TEST(UIRuntimeTest, UIDocumentScreenHostLoadsRealCompiledDocuments) {
const fs::path root = fs::temp_directory_path() / "xcui_runtime_host_load_test";
fs::remove_all(root);
WriteTextFile(
root / "RuntimeScreen.xcui",
"<View name=\"RuntimeScreen\">\n"
" <Column gap=\"10\">\n"
" <Text text=\"Runtime HUD\" />\n"
" </Column>\n"
"</View>\n");
WriteTextFile(root / "RuntimeTheme.xctheme", "<Theme name=\"RuntimeTheme\" />\n");
UIDocumentScreenHost host = {};
UIScreenAsset asset = {};
asset.screenId = "RuntimeHUD";
asset.documentPath = (root / "RuntimeScreen.xcui").string();
asset.themePath = (root / "RuntimeTheme.xctheme").string();
const UIScreenLoadResult loadResult = host.LoadScreen(asset);
ASSERT_TRUE(loadResult.succeeded);
EXPECT_EQ(loadResult.document.displayName, "RuntimeHUD");
EXPECT_TRUE(loadResult.document.viewDocument.valid);
EXPECT_TRUE(loadResult.document.hasThemeDocument);
EXPECT_FALSE(loadResult.document.dependencies.empty());
fs::remove_all(root);
}
TEST(UIRuntimeTest, UIDocumentScreenHostBuildsConcreteRuntimeFrame) {
const fs::path root = fs::temp_directory_path() / "xcui_runtime_host_frame_test";
fs::remove_all(root);
WriteTextFile(
root / "RuntimeScreen.xcui",
"<View name=\"RuntimeScreen\" padding=\"18\">\n"
" <Column gap=\"10\">\n"
" <Text text=\"Runtime HUD\" />\n"
" <Card title=\"Quest Tracker\" subtitle=\"2 active objectives\">\n"
" <Text text=\"Collect 3 relic shards\" />\n"
" </Card>\n"
" <Row gap=\"12\">\n"
" <Button title=\"Resume\" width=\"stretch\" />\n"
" <Button title=\"Settings\" width=\"stretch\" />\n"
" </Row>\n"
" </Column>\n"
"</View>\n");
UIDocumentScreenHost host = {};
UIScreenPlayer player(host);
UIScreenAsset asset = {};
asset.documentPath = (root / "RuntimeScreen.xcui").string();
ASSERT_TRUE(player.Load(asset));
const UIScreenFrameResult& frame = player.Update(MakeFrameInput(33));
EXPECT_TRUE(frame.errorMessage.empty());
EXPECT_TRUE(frame.stats.documentLoaded);
EXPECT_GT(frame.stats.commandCount, 0u);
EXPECT_GT(frame.stats.nodeCount, 0u);
EXPECT_GT(frame.stats.filledRectCommandCount, 0u);
EXPECT_GT(frame.stats.textCommandCount, 0u);
EXPECT_EQ(frame.stats.presentedFrameIndex, 33u);
fs::remove_all(root);
}
} // namespace