diff --git a/docs/plan/XCUI_Phase_Status_2026-04-05.md b/docs/plan/XCUI_Phase_Status_2026-04-05.md index 85e1ac78..991c9f16 100644 --- a/docs/plan/XCUI_Phase_Status_2026-04-05.md +++ b/docs/plan/XCUI_Phase_Status_2026-04-05.md @@ -47,11 +47,12 @@ Current gap: - Engine-side runtime ownership is no longer zero: `UIScreenPlayer`, `UIDocumentScreenHost`, and `UISystem` now define a shared runtime contract for loading a screen document, ticking it with input, and collecting `UI::UIDrawData`. - `UISystem` now supports layered screen composition semantics: stacked screen players, top-interactive input routing, and modal layers that block lower screens. - `UIScreenStackController::ReplaceTop` now preserves the previous top screen if the replacement screen fails to load, so runtime menu flows do not silently drop their active layer on bad assets. +- `SceneRuntime` now owns a dedicated `UISceneRuntimeContext`, so game/runtime code has a first-class place to configure viewport/focus, queue `UIInputEvent`s, drive `UISystem` each `Update`, and inspect the latest UI frame result. - Runtime screen emission now also carries concrete button text in the shared document host path instead of silently dropping button labels. Current gap: -- No production game-loop integration has been wired yet from scene/runtime systems into `UISystem`. +- Runtime UI is now wired into `SceneRuntime`, but render submission is still limited to producing `UIDrawData`; there is no game-view/runtime presenter path that automatically draws those frames yet. - The runtime widget library is still shallow and missing the editor-grade controls that will later be shared downward. ### 3. Editor Layer @@ -88,6 +89,7 @@ Current gap: - `new_editor_xcui_hosted_preview_presenter_tests`: `12/12` - `XCNewEditor` Debug target builds successfully - `core_ui_tests`: `28/28` +- `scene_tests`: `65/65` - `core_ui_style_tests`: `5/5` - `ui_resource_tests`: `11/11` - `editor_tests` targeted bridge smoke: `3/3` @@ -119,11 +121,21 @@ Current gap: - Engine runtime layer added: - `UIScreenPlayer` - `UIDocumentScreenHost` + - `UISceneRuntimeContext` - `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`. - `UIScreenStackController` replacement now rolls back safely on failure instead of popping the active top layer first. +- `SceneRuntime` now exposes XCUI runtime ownership directly: + - `GetUISystem()` + - `GetUIScreenStackController()` + - `GetLastUIFrame()` + - `SetUIViewportRect(...)` + - `SetUIFocused(...)` + - `QueueUIInputEvent(...)` + - `ClearQueuedUIInputEvents()` + - automatic `UISystem` ticking during `SceneRuntime::Update(...)` - Runtime document-host draw emission now preserves button labels for shared screen rendering. - RHI image path improvements: - clipped image UV adjustment @@ -162,7 +174,7 @@ Current gap: ## Next Phase -1. Expand runtime/game-layer ownership from the current document host + layered `UISystem` into reusable menu/HUD stack patterns and engine runtime integration. +1. Expand runtime/game-layer ownership from the current `SceneRuntime` UI context into scene-declared HUD/menu bootstrapping, draw submission, and higher-level runtime UI policies. 2. Promote the current editor-facing widget prototypes out of authored `LayoutLab` content and into reusable XCUI widget/runtime modules, then continue with toolbar/menu and more native shell-owned chrome. 3. Add a native XCUI host compositor on the existing window-level compositor seam so `new_editor` can present without going through ImGui-owned draw data. 4. Reduce remaining ImGui leakage in hosted preview surfaces and panel contracts now that the compositor seam is in place. diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 9f637582..0315755d 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -525,9 +525,11 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenTypes.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenDocumentHost.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenPlayer.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UISceneRuntimeContext.h ${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/UISceneRuntimeContext.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Runtime/UIScreenStackController.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Runtime/UISystem.cpp diff --git a/engine/include/XCEngine/Scene/SceneRuntime.h b/engine/include/XCEngine/Scene/SceneRuntime.h index d96aa745..a5c04740 100644 --- a/engine/include/XCEngine/Scene/SceneRuntime.h +++ b/engine/include/XCEngine/Scene/SceneRuntime.h @@ -1,12 +1,37 @@ #pragma once #include +#include + +#include + +namespace XCEngine { +namespace UI { +namespace Runtime { + +class UISceneRuntimeContext; +class UISystem; +class UIScreenStackController; +struct UISystemFrameResult; + +} // namespace Runtime +} // namespace UI +} // namespace XCEngine namespace XCEngine { namespace Components { class SceneRuntime { public: + SceneRuntime(); + ~SceneRuntime(); + + SceneRuntime(SceneRuntime&& other) noexcept; + SceneRuntime& operator=(SceneRuntime&& other) noexcept; + + SceneRuntime(const SceneRuntime&) = delete; + SceneRuntime& operator=(const SceneRuntime&) = delete; + void Start(Scene* scene); void Stop(); @@ -14,10 +39,19 @@ public: void Update(float deltaTime); void LateUpdate(float deltaTime); + UI::Runtime::UISystem& GetUISystem(); + UI::Runtime::UIScreenStackController& GetUIScreenStackController(); + const UI::Runtime::UISystemFrameResult& GetLastUIFrame() const; + void SetUIViewportRect(const UI::UIRect& viewportRect); + void SetUIFocused(bool focused); + void QueueUIInputEvent(const UI::UIInputEvent& event); + void ClearQueuedUIInputEvents(); + bool IsRunning() const { return m_running; } Scene* GetScene() const { return m_scene; } private: + std::unique_ptr m_uiRuntime; Scene* m_scene = nullptr; bool m_running = false; }; diff --git a/engine/include/XCEngine/UI/Runtime/UISceneRuntimeContext.h b/engine/include/XCEngine/UI/Runtime/UISceneRuntimeContext.h new file mode 100644 index 00000000..f81499ca --- /dev/null +++ b/engine/include/XCEngine/UI/Runtime/UISceneRuntimeContext.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +namespace XCEngine { +namespace UI { +namespace Runtime { + +class UISceneRuntimeContext { +public: + UISceneRuntimeContext(); + + UISystem& GetSystem(); + const UISystem& GetSystem() const; + + UIScreenStackController& GetStackController(); + const UIScreenStackController& GetStackController() const; + + const UISystemFrameResult& GetLastFrame() const; + + void Reset(); + void SetViewportRect(const UIRect& viewportRect); + void SetFocused(bool focused); + void QueueInputEvent(const UIInputEvent& event); + void ClearQueuedInputEvents(); + void Update(double deltaTimeSeconds); + +private: + UIDocumentScreenHost m_documentHost = {}; + UISystem m_system; + UIScreenStackController m_stackController; + UIScreenFrameInput m_pendingFrameInput = {}; + std::uint64_t m_nextFrameIndex = 1u; +}; + +} // namespace Runtime +} // namespace UI +} // namespace XCEngine diff --git a/engine/src/Scene/SceneRuntime.cpp b/engine/src/Scene/SceneRuntime.cpp index cb26c731..774da1cf 100644 --- a/engine/src/Scene/SceneRuntime.cpp +++ b/engine/src/Scene/SceneRuntime.cpp @@ -1,10 +1,21 @@ #include "Scene/SceneRuntime.h" #include "Scripting/ScriptEngine.h" +#include namespace XCEngine { namespace Components { +SceneRuntime::SceneRuntime() + : m_uiRuntime(std::make_unique()) { +} + +SceneRuntime::~SceneRuntime() = default; + +SceneRuntime::SceneRuntime(SceneRuntime&& other) noexcept = default; + +SceneRuntime& SceneRuntime::operator=(SceneRuntime&& other) noexcept = default; + void SceneRuntime::Start(Scene* scene) { if (m_running && m_scene == scene) { return; @@ -18,16 +29,19 @@ void SceneRuntime::Start(Scene* scene) { m_scene = scene; m_running = true; + m_uiRuntime->Reset(); Scripting::ScriptEngine::Get().OnRuntimeStart(scene); } void SceneRuntime::Stop() { if (!m_running) { + m_uiRuntime->Reset(); m_scene = nullptr; return; } Scripting::ScriptEngine::Get().OnRuntimeStop(); + m_uiRuntime->Reset(); m_running = false; m_scene = nullptr; } @@ -49,6 +63,7 @@ void SceneRuntime::Update(float deltaTime) { Scripting::ScriptEngine::Get().OnUpdate(deltaTime); m_scene->Update(deltaTime); + m_uiRuntime->Update(deltaTime); } void SceneRuntime::LateUpdate(float deltaTime) { @@ -60,5 +75,33 @@ void SceneRuntime::LateUpdate(float deltaTime) { m_scene->LateUpdate(deltaTime); } +UI::Runtime::UISystem& SceneRuntime::GetUISystem() { + return m_uiRuntime->GetSystem(); +} + +UI::Runtime::UIScreenStackController& SceneRuntime::GetUIScreenStackController() { + return m_uiRuntime->GetStackController(); +} + +const UI::Runtime::UISystemFrameResult& SceneRuntime::GetLastUIFrame() const { + return m_uiRuntime->GetLastFrame(); +} + +void SceneRuntime::SetUIViewportRect(const UI::UIRect& viewportRect) { + m_uiRuntime->SetViewportRect(viewportRect); +} + +void SceneRuntime::SetUIFocused(bool focused) { + m_uiRuntime->SetFocused(focused); +} + +void SceneRuntime::QueueUIInputEvent(const UI::UIInputEvent& event) { + m_uiRuntime->QueueInputEvent(event); +} + +void SceneRuntime::ClearQueuedUIInputEvents() { + m_uiRuntime->ClearQueuedInputEvents(); +} + } // namespace Components } // namespace XCEngine diff --git a/engine/src/UI/Runtime/UISceneRuntimeContext.cpp b/engine/src/UI/Runtime/UISceneRuntimeContext.cpp new file mode 100644 index 00000000..43765903 --- /dev/null +++ b/engine/src/UI/Runtime/UISceneRuntimeContext.cpp @@ -0,0 +1,64 @@ +#include + +namespace XCEngine { +namespace UI { +namespace Runtime { + +UISceneRuntimeContext::UISceneRuntimeContext() + : m_system(m_documentHost) + , m_stackController(m_system) { +} + +UISystem& UISceneRuntimeContext::GetSystem() { + return m_system; +} + +const UISystem& UISceneRuntimeContext::GetSystem() const { + return m_system; +} + +UIScreenStackController& UISceneRuntimeContext::GetStackController() { + return m_stackController; +} + +const UIScreenStackController& UISceneRuntimeContext::GetStackController() const { + return m_stackController; +} + +const UISystemFrameResult& UISceneRuntimeContext::GetLastFrame() const { + return m_system.GetLastFrame(); +} + +void UISceneRuntimeContext::Reset() { + m_stackController.Clear(); + m_system.DestroyAllPlayers(); + m_pendingFrameInput = {}; + m_nextFrameIndex = 1u; +} + +void UISceneRuntimeContext::SetViewportRect(const UIRect& viewportRect) { + m_pendingFrameInput.viewportRect = viewportRect; +} + +void UISceneRuntimeContext::SetFocused(bool focused) { + m_pendingFrameInput.focused = focused; +} + +void UISceneRuntimeContext::QueueInputEvent(const UIInputEvent& event) { + m_pendingFrameInput.events.push_back(event); +} + +void UISceneRuntimeContext::ClearQueuedInputEvents() { + m_pendingFrameInput.events.clear(); +} + +void UISceneRuntimeContext::Update(double deltaTimeSeconds) { + m_pendingFrameInput.deltaTimeSeconds = deltaTimeSeconds; + m_pendingFrameInput.frameIndex = m_nextFrameIndex++; + m_system.Update(m_pendingFrameInput); + m_pendingFrameInput.events.clear(); +} + +} // namespace Runtime +} // namespace UI +} // namespace XCEngine diff --git a/tests/Scene/test_scene_runtime.cpp b/tests/Scene/test_scene_runtime.cpp index 1198df9b..c7ebc657 100644 --- a/tests/Scene/test_scene_runtime.cpp +++ b/tests/Scene/test_scene_runtime.cpp @@ -4,13 +4,22 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include #include #include using namespace XCEngine::Components; using namespace XCEngine::Scripting; +using XCEngine::UI::Runtime::UIScreenAsset; + +namespace fs = std::filesystem; namespace { @@ -67,6 +76,61 @@ private: std::vector* m_events = nullptr; }; +class TempFileScope { +public: + TempFileScope(std::string stem, std::string extension, std::string contents) { + const auto uniqueId = std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count()); + m_path = fs::temp_directory_path() / (std::move(stem) + "_" + uniqueId + std::move(extension)); + std::ofstream output(m_path, std::ios::binary | std::ios::trunc); + output << contents; + } + + ~TempFileScope() { + std::error_code ec; + fs::remove(m_path, ec); + } + + const fs::path& Path() const { + return m_path; + } + +private: + fs::path m_path = {}; +}; + +std::string BuildViewMarkup(const char* heroTitle) { + return + "\n" + " \n" + " \n" + " \n" + " \n" + "\n"; +} + +UIScreenAsset BuildScreenAsset(const fs::path& viewPath, const char* screenId) { + UIScreenAsset screen = {}; + screen.screenId = screenId; + screen.documentPath = viewPath.string(); + return screen; +} + +bool DrawDataContainsText( + const XCEngine::UI::UIDrawData& drawData, + const std::string& text) { + for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) { + for (const XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) { + if (command.type == XCEngine::UI::UIDrawCommandType::Text && + command.text == text) { + return true; + } + } + } + + return false; +} + class RecordingScriptRuntime : public IScriptRuntime { public: explicit RecordingScriptRuntime(std::vector* events) @@ -304,4 +368,66 @@ TEST_F(SceneRuntimeTest, StartingNewSceneStopsPreviousRuntimeFirst) { runtime.Stop(); } +TEST_F(SceneRuntimeTest, UpdateTicksUiRuntimeAndClearsQueuedInputEvents) { + Scene* runtimeScene = CreateScene("RuntimeScene"); + runtime.Start(runtimeScene); + runtime.SetUIViewportRect(XCEngine::UI::UIRect(0.0f, 0.0f, 800.0f, 480.0f)); + runtime.SetUIFocused(true); + + TempFileScope menuView("xcui_scene_runtime_menu", ".xcui", BuildViewMarkup("Runtime Menu")); + const auto layerId = runtime.GetUIScreenStackController().PushMenu( + BuildScreenAsset(menuView.Path(), "runtime.menu"), + "menu"); + ASSERT_NE(layerId, 0u); + + XCEngine::UI::UIInputEvent textEvent = {}; + textEvent.type = XCEngine::UI::UIInputEventType::Character; + textEvent.character = 'A'; + runtime.QueueUIInputEvent(textEvent); + + runtime.Update(0.016f); + + const auto& firstFrame = runtime.GetLastUIFrame(); + ASSERT_EQ(firstFrame.presentedLayerCount, 1u); + ASSERT_EQ(firstFrame.layers.size(), 1u); + EXPECT_EQ(firstFrame.frameIndex, 1u); + EXPECT_EQ(firstFrame.layers.front().stats.inputEventCount, 1u); + EXPECT_TRUE(DrawDataContainsText(firstFrame.drawData, "Runtime Menu")); + + runtime.Update(0.016f); + + const auto& secondFrame = runtime.GetLastUIFrame(); + ASSERT_EQ(secondFrame.presentedLayerCount, 1u); + ASSERT_EQ(secondFrame.layers.size(), 1u); + EXPECT_EQ(secondFrame.frameIndex, 2u); + EXPECT_EQ(secondFrame.layers.front().stats.inputEventCount, 0u); +} + +TEST_F(SceneRuntimeTest, StopClearsUiRuntimeState) { + Scene* runtimeScene = CreateScene("RuntimeScene"); + runtime.Start(runtimeScene); + runtime.SetUIViewportRect(XCEngine::UI::UIRect(0.0f, 0.0f, 800.0f, 480.0f)); + runtime.SetUIFocused(true); + + TempFileScope menuView("xcui_scene_runtime_pause", ".xcui", BuildViewMarkup("Pause Menu")); + const auto layerId = runtime.GetUIScreenStackController().PushMenu( + BuildScreenAsset(menuView.Path(), "runtime.pause"), + "pause"); + ASSERT_NE(layerId, 0u); + + runtime.Update(0.016f); + ASSERT_EQ(runtime.GetUISystem().GetLayerCount(), 1u); + ASSERT_EQ(runtime.GetUIScreenStackController().GetEntryCount(), 1u); + ASSERT_EQ(runtime.GetLastUIFrame().presentedLayerCount, 1u); + + runtime.Stop(); + + EXPECT_FALSE(runtime.IsRunning()); + EXPECT_EQ(runtime.GetScene(), nullptr); + EXPECT_EQ(runtime.GetUISystem().GetLayerCount(), 0u); + EXPECT_EQ(runtime.GetUIScreenStackController().GetEntryCount(), 0u); + EXPECT_EQ(runtime.GetLastUIFrame().presentedLayerCount, 0u); + EXPECT_TRUE(runtime.GetLastUIFrame().layers.empty()); +} + } // namespace