From 0c24c7c6111a597c360012bc2c4ec457066ca1e2 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 5 Apr 2026 06:05:54 +0800 Subject: [PATCH] Add XCUI runtime screen stack helper --- docs/plan/XCUI_Phase_Status_2026-04-05.md | 4 +- engine/CMakeLists.txt | 4 + .../UI/Runtime/UIScreenStackController.h | 54 +++++++ .../UI/Runtime/UIScreenStackController.cpp | 148 ++++++++++++++++++ tests/Core/UI/test_ui_runtime.cpp | 64 ++++++++ 5 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 engine/include/XCEngine/UI/Runtime/UIScreenStackController.h create mode 100644 engine/src/UI/Runtime/UIScreenStackController.cpp diff --git a/docs/plan/XCUI_Phase_Status_2026-04-05.md b/docs/plan/XCUI_Phase_Status_2026-04-05.md index 2060ab30..b03275f2 100644 --- a/docs/plan/XCUI_Phase_Status_2026-04-05.md +++ b/docs/plan/XCUI_Phase_Status_2026-04-05.md @@ -73,7 +73,7 @@ Current gap: - `new_editor_xcui_rhi_command_compiler_tests`: `6/6` - `new_editor_xcui_rhi_render_backend_tests`: `5/5` - `XCNewEditor` Debug target builds successfully -- `core_ui_tests`: `14/14` +- `core_ui_tests`: `16/16` - `core_ui_style_tests`: `5/5` - `ui_resource_tests`: `11/11` - `editor_tests` targeted bridge smoke: `3/3` @@ -102,8 +102,10 @@ Current gap: - Engine runtime layer added: - `UIScreenPlayer` - `UIDocumentScreenHost` + - `UIScreenStackController` - `UISystem` - layered screen composition and modal blocking semantics +- Runtime/game integration scaffolding now includes reusable `HUD/menu/modal` stack helpers on top of `UISystem`. - Runtime document-host draw emission now preserves button labels for shared screen rendering. - RHI image path improvements: - clipped image UV adjustment diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 03ce77b2..c39626b4 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -522,6 +522,7 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UISystem.h ${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Runtime/UIScreenDocumentHost.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Runtime/UIScreenPlayer.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Runtime/UIScreenStackController.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Runtime/UISystem.cpp # Input @@ -587,6 +588,9 @@ target_link_libraries(XCEngine PUBLIC ) if(MSVC) + target_compile_options(XCEngine PRIVATE + /FS + $<$:/Z7>) target_link_libraries(XCEngine PUBLIC delayimp) target_link_options(XCEngine INTERFACE "/DELAYLOAD:assimp-vc143-mt.dll") set_target_properties(XCEngine PROPERTIES diff --git a/engine/include/XCEngine/UI/Runtime/UIScreenStackController.h b/engine/include/XCEngine/UI/Runtime/UIScreenStackController.h new file mode 100644 index 00000000..fac49f9d --- /dev/null +++ b/engine/include/XCEngine/UI/Runtime/UIScreenStackController.h @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include +#include +#include + +namespace XCEngine { +namespace UI { +namespace Runtime { + +struct UIScreenStackEntry { + UIScreenLayerId layerId = 0; + UIScreenAsset asset = {}; + UIScreenLayerOptions options = {}; +}; + +class UIScreenStackController { +public: + explicit UIScreenStackController(UISystem& system); + + UIScreenLayerId PushScreen( + const UIScreenAsset& asset, + const UIScreenLayerOptions& options = UIScreenLayerOptions()); + UIScreenLayerId PushMenu( + const UIScreenAsset& asset, + const std::string& debugName = {}); + UIScreenLayerId PushModal( + const UIScreenAsset& asset, + const std::string& debugName = {}); + UIScreenLayerId PushHud( + const UIScreenAsset& asset, + const std::string& debugName = {}); + + bool ReplaceTop( + const UIScreenAsset& asset, + const UIScreenLayerOptions& options = UIScreenLayerOptions()); + bool Pop(); + bool Remove(UIScreenLayerId layerId); + void Clear(); + + std::size_t GetEntryCount() const; + const UIScreenStackEntry* GetTop() const; + const std::vector& GetEntries() const; + +private: + UISystem* m_system = nullptr; + std::vector m_entries = {}; +}; + +} // namespace Runtime +} // namespace UI +} // namespace XCEngine diff --git a/engine/src/UI/Runtime/UIScreenStackController.cpp b/engine/src/UI/Runtime/UIScreenStackController.cpp new file mode 100644 index 00000000..abae789b --- /dev/null +++ b/engine/src/UI/Runtime/UIScreenStackController.cpp @@ -0,0 +1,148 @@ +#include + +#include + +namespace XCEngine { +namespace UI { +namespace Runtime { + +namespace { + +std::string ResolveDebugName( + const UIScreenAsset& asset, + const std::string& fallbackName, + const std::string& explicitName) { + if (!explicitName.empty()) { + return explicitName; + } + if (!asset.screenId.empty()) { + return asset.screenId; + } + return fallbackName; +} + +} // namespace + +UIScreenStackController::UIScreenStackController(UISystem& system) + : m_system(&system) { +} + +UIScreenLayerId UIScreenStackController::PushScreen( + const UIScreenAsset& asset, + const UIScreenLayerOptions& options) { + const UIScreenLayerId layerId = m_system != nullptr + ? m_system->PushScreen(asset, options) + : 0; + if (layerId == 0) { + return 0; + } + + UIScreenStackEntry entry = {}; + entry.layerId = layerId; + entry.asset = asset; + entry.options = options; + m_entries.push_back(std::move(entry)); + return layerId; +} + +UIScreenLayerId UIScreenStackController::PushMenu( + const UIScreenAsset& asset, + const std::string& debugName) { + UIScreenLayerOptions options = {}; + options.debugName = ResolveDebugName(asset, "Menu", debugName); + options.acceptsInput = true; + options.blocksLayersBelow = true; + return PushScreen(asset, options); +} + +UIScreenLayerId UIScreenStackController::PushModal( + const UIScreenAsset& asset, + const std::string& debugName) { + UIScreenLayerOptions options = {}; + options.debugName = ResolveDebugName(asset, "Modal", debugName); + options.acceptsInput = true; + options.blocksLayersBelow = true; + return PushScreen(asset, options); +} + +UIScreenLayerId UIScreenStackController::PushHud( + const UIScreenAsset& asset, + const std::string& debugName) { + UIScreenLayerOptions options = {}; + options.debugName = ResolveDebugName(asset, "HUD", debugName); + options.acceptsInput = false; + options.blocksLayersBelow = false; + return PushScreen(asset, options); +} + +bool UIScreenStackController::ReplaceTop( + const UIScreenAsset& asset, + const UIScreenLayerOptions& options) { + if (m_entries.empty()) { + return PushScreen(asset, options) != 0; + } + + if (!Pop()) { + return false; + } + + return PushScreen(asset, options) != 0; +} + +bool UIScreenStackController::Pop() { + if (m_system == nullptr || m_entries.empty()) { + return false; + } + + const UIScreenLayerId layerId = m_entries.back().layerId; + if (!m_system->RemoveLayer(layerId)) { + return false; + } + + m_entries.pop_back(); + return true; +} + +bool UIScreenStackController::Remove(UIScreenLayerId layerId) { + if (m_system == nullptr) { + return false; + } + + for (std::size_t index = 0; index < m_entries.size(); ++index) { + if (m_entries[index].layerId != layerId) { + continue; + } + + if (!m_system->RemoveLayer(layerId)) { + return false; + } + + m_entries.erase(m_entries.begin() + static_cast(index)); + return true; + } + + return false; +} + +void UIScreenStackController::Clear() { + if (m_system != nullptr) { + m_system->DestroyAllPlayers(); + } + m_entries.clear(); +} + +std::size_t UIScreenStackController::GetEntryCount() const { + return m_entries.size(); +} + +const UIScreenStackEntry* UIScreenStackController::GetTop() const { + return m_entries.empty() ? nullptr : &m_entries.back(); +} + +const std::vector& UIScreenStackController::GetEntries() const { + return m_entries; +} + +} // namespace Runtime +} // namespace UI +} // namespace XCEngine diff --git a/tests/Core/UI/test_ui_runtime.cpp b/tests/Core/UI/test_ui_runtime.cpp index 457e320a..5ff0507d 100644 --- a/tests/Core/UI/test_ui_runtime.cpp +++ b/tests/Core/UI/test_ui_runtime.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -15,6 +16,7 @@ using XCEngine::UI::Runtime::UIScreenAsset; using XCEngine::UI::Runtime::UIScreenFrameInput; using XCEngine::UI::Runtime::UIScreenPlayer; using XCEngine::UI::Runtime::UIDocumentScreenHost; +using XCEngine::UI::Runtime::UIScreenStackController; using XCEngine::UI::Runtime::UISystem; namespace fs = std::filesystem; @@ -141,3 +143,65 @@ TEST(UIRuntimeTest, UISystemForwardsActiveScreenToPlayer) { EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Modal Dialog")); EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Base Screen")); } + +TEST(UIRuntimeTest, ScreenStackControllerAppliesHudAndMenuLayerPolicies) { + TempFileScope hudView("xcui_runtime_hud", ".xcui", BuildViewMarkup("HUD Screen")); + TempFileScope menuView("xcui_runtime_menu", ".xcui", BuildViewMarkup("Pause Menu", "Paused")); + + UIDocumentScreenHost host = {}; + UISystem system(host); + UIScreenStackController stack(system); + + const auto hudLayer = stack.PushHud(BuildScreenAsset(hudView.Path(), "runtime.hud"), "hud"); + const auto menuLayer = stack.PushMenu(BuildScreenAsset(menuView.Path(), "runtime.menu"), "menu"); + ASSERT_NE(hudLayer, 0u); + ASSERT_NE(menuLayer, 0u); + + ASSERT_EQ(stack.GetEntryCount(), 2u); + ASSERT_NE(stack.GetTop(), nullptr); + EXPECT_EQ(stack.GetTop()->layerId, menuLayer); + + const auto* hudOptions = system.FindLayerOptions(hudLayer); + const auto* menuOptions = system.FindLayerOptions(menuLayer); + ASSERT_NE(hudOptions, nullptr); + ASSERT_NE(menuOptions, nullptr); + EXPECT_FALSE(hudOptions->acceptsInput); + EXPECT_FALSE(hudOptions->blocksLayersBelow); + EXPECT_TRUE(menuOptions->acceptsInput); + EXPECT_TRUE(menuOptions->blocksLayersBelow); + + const auto& frame = system.Update(BuildInputState(4u)); + EXPECT_EQ(frame.presentedLayerCount, 1u); + EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Pause Menu")); + EXPECT_FALSE(DrawDataContainsText(frame.drawData, "HUD Screen")); +} + +TEST(UIRuntimeTest, ScreenStackControllerReplaceTopSwapsMenuContent) { + TempFileScope pauseView("xcui_runtime_pause", ".xcui", BuildViewMarkup("Pause Menu")); + TempFileScope settingsView("xcui_runtime_settings", ".xcui", BuildViewMarkup("Settings Menu")); + + UIDocumentScreenHost host = {}; + UISystem system(host); + UIScreenStackController stack(system); + + const auto pauseLayer = stack.PushMenu(BuildScreenAsset(pauseView.Path(), "runtime.pause"), "pause"); + ASSERT_NE(pauseLayer, 0u); + + XCEngine::UI::Runtime::UIScreenLayerOptions replacementOptions = {}; + replacementOptions.debugName = "settings"; + replacementOptions.acceptsInput = true; + replacementOptions.blocksLayersBelow = true; + ASSERT_TRUE(stack.ReplaceTop( + BuildScreenAsset(settingsView.Path(), "runtime.settings"), + replacementOptions)); + + ASSERT_EQ(stack.GetEntryCount(), 1u); + ASSERT_NE(stack.GetTop(), nullptr); + EXPECT_EQ(stack.GetTop()->asset.screenId, "runtime.settings"); + EXPECT_NE(stack.GetTop()->layerId, pauseLayer); + + const auto& frame = system.Update(BuildInputState(5u)); + EXPECT_EQ(frame.presentedLayerCount, 1u); + EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Settings Menu")); + EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Pause Menu")); +}