#include #include #include #include #include #include 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 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(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", "\n" " \n" " \n" " \n" "\n"); WriteTextFile(root / "RuntimeTheme.xctheme", "\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", "\n" " \n" " \n" " \n" " \n" " \n" " \n" "