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`.
- `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.

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/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

View File

@@ -1,12 +1,37 @@
#pragma once
#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 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<UI::Runtime::UISceneRuntimeContext> m_uiRuntime;
Scene* m_scene = nullptr;
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 "Scripting/ScriptEngine.h"
#include <XCEngine/UI/Runtime/UISceneRuntimeContext.h>
namespace XCEngine {
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) {
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

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/ScriptComponent.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 <string>
#include <vector>
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<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 {
public:
explicit RecordingScriptRuntime(std::vector<std::string>* 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