Integrate XCUI runtime context into SceneRuntime

This commit is contained in:
2026-04-05 06:52:17 +08:00
parent edf434aa03
commit d46dcbfa9e
7 changed files with 323 additions and 2 deletions

View File

@@ -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`. - 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. - `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. - `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. - Runtime screen emission now also carries concrete button text in the shared document host path instead of silently dropping button labels.
Current gap: 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. - The runtime widget library is still shallow and missing the editor-grade controls that will later be shared downward.
### 3. Editor Layer ### 3. Editor Layer
@@ -88,6 +89,7 @@ Current gap:
- `new_editor_xcui_hosted_preview_presenter_tests`: `12/12` - `new_editor_xcui_hosted_preview_presenter_tests`: `12/12`
- `XCNewEditor` Debug target builds successfully - `XCNewEditor` Debug target builds successfully
- `core_ui_tests`: `28/28` - `core_ui_tests`: `28/28`
- `scene_tests`: `65/65`
- `core_ui_style_tests`: `5/5` - `core_ui_style_tests`: `5/5`
- `ui_resource_tests`: `11/11` - `ui_resource_tests`: `11/11`
- `editor_tests` targeted bridge smoke: `3/3` - `editor_tests` targeted bridge smoke: `3/3`
@@ -119,11 +121,21 @@ Current gap:
- Engine runtime layer added: - Engine runtime layer added:
- `UIScreenPlayer` - `UIScreenPlayer`
- `UIDocumentScreenHost` - `UIDocumentScreenHost`
- `UISceneRuntimeContext`
- `UIScreenStackController` - `UIScreenStackController`
- `UISystem` - `UISystem`
- layered screen composition and modal blocking semantics - layered screen composition and modal blocking semantics
- Runtime/game integration scaffolding now includes reusable `HUD/menu/modal` stack helpers on top of `UISystem`. - 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. - `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. - Runtime document-host draw emission now preserves button labels for shared screen rendering.
- RHI image path improvements: - RHI image path improvements:
- clipped image UV adjustment - clipped image UV adjustment
@@ -162,7 +174,7 @@ Current gap:
## Next Phase ## 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. 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. 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. 4. Reduce remaining ImGui leakage in hosted preview surfaces and panel contracts now that the compositor seam is in place.

View File

@@ -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/UIScreenTypes.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenDocumentHost.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/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}/include/XCEngine/UI/Runtime/UISystem.h
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Runtime/UIScreenDocumentHost.cpp ${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/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/UIScreenStackController.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Runtime/UISystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Runtime/UISystem.cpp

View File

@@ -1,12 +1,37 @@
#pragma once #pragma once
#include <XCEngine/Scene/Scene.h> #include <XCEngine/Scene/Scene.h>
#include <XCEngine/UI/Types.h>
#include <memory>
namespace XCEngine {
namespace UI {
namespace Runtime {
class UISceneRuntimeContext;
class UISystem;
class UIScreenStackController;
struct UISystemFrameResult;
} // namespace Runtime
} // namespace UI
} // namespace XCEngine
namespace XCEngine { namespace XCEngine {
namespace Components { namespace Components {
class SceneRuntime { class SceneRuntime {
public: 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 Start(Scene* scene);
void Stop(); void Stop();
@@ -14,10 +39,19 @@ public:
void Update(float deltaTime); void Update(float deltaTime);
void LateUpdate(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; } bool IsRunning() const { return m_running; }
Scene* GetScene() const { return m_scene; } Scene* GetScene() const { return m_scene; }
private: private:
std::unique_ptr<UI::Runtime::UISceneRuntimeContext> m_uiRuntime;
Scene* m_scene = nullptr; Scene* m_scene = nullptr;
bool m_running = false; bool m_running = false;
}; };

View File

@@ -0,0 +1,40 @@
#pragma once
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
#include <XCEngine/UI/Runtime/UIScreenStackController.h>
#include <XCEngine/UI/Runtime/UISystem.h>
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

View File

@@ -1,10 +1,21 @@
#include "Scene/SceneRuntime.h" #include "Scene/SceneRuntime.h"
#include "Scripting/ScriptEngine.h" #include "Scripting/ScriptEngine.h"
#include <XCEngine/UI/Runtime/UISceneRuntimeContext.h>
namespace XCEngine { namespace XCEngine {
namespace Components { namespace Components {
SceneRuntime::SceneRuntime()
: m_uiRuntime(std::make_unique<UI::Runtime::UISceneRuntimeContext>()) {
}
SceneRuntime::~SceneRuntime() = default;
SceneRuntime::SceneRuntime(SceneRuntime&& other) noexcept = default;
SceneRuntime& SceneRuntime::operator=(SceneRuntime&& other) noexcept = default;
void SceneRuntime::Start(Scene* scene) { void SceneRuntime::Start(Scene* scene) {
if (m_running && m_scene == scene) { if (m_running && m_scene == scene) {
return; return;
@@ -18,16 +29,19 @@ void SceneRuntime::Start(Scene* scene) {
m_scene = scene; m_scene = scene;
m_running = true; m_running = true;
m_uiRuntime->Reset();
Scripting::ScriptEngine::Get().OnRuntimeStart(scene); Scripting::ScriptEngine::Get().OnRuntimeStart(scene);
} }
void SceneRuntime::Stop() { void SceneRuntime::Stop() {
if (!m_running) { if (!m_running) {
m_uiRuntime->Reset();
m_scene = nullptr; m_scene = nullptr;
return; return;
} }
Scripting::ScriptEngine::Get().OnRuntimeStop(); Scripting::ScriptEngine::Get().OnRuntimeStop();
m_uiRuntime->Reset();
m_running = false; m_running = false;
m_scene = nullptr; m_scene = nullptr;
} }
@@ -49,6 +63,7 @@ void SceneRuntime::Update(float deltaTime) {
Scripting::ScriptEngine::Get().OnUpdate(deltaTime); Scripting::ScriptEngine::Get().OnUpdate(deltaTime);
m_scene->Update(deltaTime); m_scene->Update(deltaTime);
m_uiRuntime->Update(deltaTime);
} }
void SceneRuntime::LateUpdate(float deltaTime) { void SceneRuntime::LateUpdate(float deltaTime) {
@@ -60,5 +75,33 @@ void SceneRuntime::LateUpdate(float deltaTime) {
m_scene->LateUpdate(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 Components
} // namespace XCEngine } // namespace XCEngine

View File

@@ -0,0 +1,64 @@
#include <XCEngine/UI/Runtime/UISceneRuntimeContext.h>
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

View File

@@ -4,13 +4,22 @@
#include <XCEngine/Scripting/IScriptRuntime.h> #include <XCEngine/Scripting/IScriptRuntime.h>
#include <XCEngine/Scripting/ScriptComponent.h> #include <XCEngine/Scripting/ScriptComponent.h>
#include <XCEngine/Scripting/ScriptEngine.h> #include <XCEngine/Scripting/ScriptEngine.h>
#include <XCEngine/UI/Runtime/UIScreenStackController.h>
#include <XCEngine/UI/Runtime/UIScreenTypes.h>
#include <XCEngine/UI/Runtime/UISystem.h>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
using namespace XCEngine::Components; using namespace XCEngine::Components;
using namespace XCEngine::Scripting; using namespace XCEngine::Scripting;
using XCEngine::UI::Runtime::UIScreenAsset;
namespace fs = std::filesystem;
namespace { namespace {
@@ -67,6 +76,61 @@ private:
std::vector<std::string>* m_events = nullptr; std::vector<std::string>* 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
"<View name=\"Runtime Screen\">\n"
" <Column id=\"root\" padding=\"18\" gap=\"10\">\n"
" <Card id=\"hero\" title=\"" + std::string(heroTitle) + "\" subtitle=\"Shared XCUI runtime layer\" />\n"
" <Text id=\"status\" text=\"Ready for play\" />\n"
" </Column>\n"
"</View>\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 { class RecordingScriptRuntime : public IScriptRuntime {
public: public:
explicit RecordingScriptRuntime(std::vector<std::string>* events) explicit RecordingScriptRuntime(std::vector<std::string>* events)
@@ -304,4 +368,66 @@ TEST_F(SceneRuntimeTest, StartingNewSceneStopsPreviousRuntimeFirst) {
runtime.Stop(); 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 } // namespace