This commit is contained in:
Rowland 2026-07-27 10:36:59 +08:00
parent 3ba7b090d1
commit 92431296fa
17 changed files with 1390 additions and 106 deletions

View File

@ -524,12 +524,13 @@ int main(int argc, char* argv[]) {
[&animationAssets](MetaCore::MetaCoreAssetGuid guid) { return animationAssets.LoadController(guid); },
[&animationAssets](MetaCore::MetaCoreAssetGuid guid) { return animationAssets.LoadAvatar(guid); },
[&animationAssets](MetaCore::MetaCoreAssetGuid guid) { return animationAssets.LoadTimeline(guid); }, &physicsWorld);
MetaCore::MetaCoreRuntimeUiSystem runtimeUi;
MetaCore::MetaCoreScriptRuntime scriptRuntime(
scene, scriptRegistry,
[&output, &errors](std::uint32_t level, std::string_view category, std::string_view message) {
std::ostream& stream = level >= 2U ? errors : output;
stream << "MetaCorePlayer Script[" << category << "]: " << message << '\n';
}, &runtimeInput, &physicsWorld, &animationRuntime
}, &runtimeInput, &physicsWorld, &animationRuntime, &runtimeUi
);
scriptRuntime.Start();
animationRuntime.SetEventCallback([&scriptRuntime](const MetaCore::MetaCoreAnimationRuntimeEvent& event) {
@ -572,7 +573,6 @@ int main(int argc, char* argv[]) {
const auto loadedUiManifest = runtimeProjectDocument.UiManifestPath.empty()
? std::optional<MetaCore::MetaCoreRuntimeUiManifest>{}
: MetaCore::MetaCoreReadRuntimeUiManifest(uiManifestPath, typeRegistry);
MetaCore::MetaCoreRuntimeUiSystem runtimeUi;
if (loadedUiManifest.has_value()) {
if (!runtimeUi.Initialize(window, projectRoot, projectRoot / "Ui", *loadedUiManifest)) {
errors << "MetaCorePlayer: failed to initialize runtime UI\n";
@ -641,10 +641,22 @@ int main(int argc, char* argv[]) {
MetaCoreScriptValueV1 value{}; value.Type = MetaCoreScriptAbiValue_String;
std::snprintf(value.Text, sizeof(value.Text), "%s", text.c_str()); return value;
};
scriptRuntime.Publish("MetaCore.UI", {
const auto intValue = [](std::int64_t number) {
MetaCoreScriptValueV1 value{}; value.Type = MetaCoreScriptAbiValue_Int;
value.IntValue = number; return value;
};
const std::vector<MetaCoreScriptValueV1> payload{
textValue(event.DocumentId), textValue(event.ElementId), textValue(event.Action),
textValue(event.Type), textValue(event.Value)
});
textValue(event.Type), textValue(event.Value), textValue(event.OldValue),
intValue(static_cast<std::int64_t>(event.InstanceId)),
textValue(event.DocumentAssetGuid.ToString()),
intValue(static_cast<std::int64_t>(event.EventType)),
intValue(static_cast<std::int64_t>(event.InputSource))
};
scriptRuntime.Publish("MetaCore.UI", payload);
if (event.SceneObjectId != 0) {
scriptRuntime.PublishToObject(event.SceneObjectId, "MetaCore.UI", payload);
}
});
runtimeInteraction.SetEventCallback([&scriptRuntime](const MetaCore::MetaCorePointerEvent& event) {
MetaCoreScriptValueV1 type{}; type.Type = MetaCoreScriptAbiValue_Int; type.IntValue = static_cast<std::int64_t>(event.Type);

View File

@ -374,6 +374,7 @@ target_compile_options(MetaCoreHeaderTool PRIVATE ${METACORE_COMMON_WARNINGS})
if(METACORE_ENABLE_RUNTIME_DATA)
add_executable(MetaCoreRuntimeConfigTool
tools/MetaCoreRuntimeConfigTool/main.cpp
Source/MetaCoreRender/Private/MetaCoreGlibcCompat.cpp
)
target_compile_options(MetaCoreRuntimeConfigTool PRIVATE ${METACORE_COMMON_WARNINGS})
@ -635,7 +636,7 @@ target_include_directories(MetaCoreScripting
PUBLIC Source/MetaCoreScripting/Public
PRIVATE third_party
)
target_link_libraries(MetaCoreScripting PUBLIC MetaCoreFoundation MetaCoreScene MetaCoreRuntimeInput MetaCorePhysics MetaCoreAnimation glm::glm)
target_link_libraries(MetaCoreScripting PUBLIC MetaCoreFoundation MetaCoreScene MetaCoreRuntimeInput MetaCorePhysics MetaCoreAnimation MetaCoreRuntimeUi glm::glm)
if(UNIX AND NOT APPLE)
target_link_libraries(MetaCoreScripting PRIVATE dl)
endif()
@ -663,11 +664,17 @@ else()
message(WARNING "Crashpad was not found; release packaging works but local Minidump capture is disabled.")
endif()
add_executable(MetaCoreBuildTool tools/MetaCoreBuildTool/main.cpp)
add_executable(MetaCoreBuildTool
tools/MetaCoreBuildTool/main.cpp
Source/MetaCoreRender/Private/MetaCoreGlibcCompat.cpp
)
target_link_libraries(MetaCoreBuildTool PRIVATE MetaCoreDelivery)
target_compile_options(MetaCoreBuildTool PRIVATE ${METACORE_COMMON_WARNINGS})
add_executable(MetaCoreDiagnosticsTool tools/MetaCoreDiagnosticsTool/main.cpp)
add_executable(MetaCoreDiagnosticsTool
tools/MetaCoreDiagnosticsTool/main.cpp
Source/MetaCoreRender/Private/MetaCoreGlibcCompat.cpp
)
target_link_libraries(MetaCoreDiagnosticsTool PRIVATE MetaCoreDelivery)
target_compile_options(MetaCoreDiagnosticsTool PRIVATE ${METACORE_COMMON_WARNINGS})
@ -937,6 +944,7 @@ metacore_stage_ui_blit_material(MetaCoreLauncher)
add_library(MetaCoreRuntimeUi STATIC
Source/MetaCoreRuntimeUi/Public/MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h
Source/MetaCoreRuntimeUi/Private/MetaCoreUiCompiler.cpp
Source/MetaCoreRuntimeUi/Private/MetaCoreRuntimeUiSystem.cpp
)
target_include_directories(MetaCoreRuntimeUi
@ -950,6 +958,7 @@ target_link_libraries(MetaCoreRuntimeUi
MetaCoreRender
MetaCoreRuntimeData
MetaCoreRuntimeInput
MetaCoreScene
RmlUi::RmlUi
PNG::PNG
unofficial::brotli::brotlidec
@ -992,12 +1001,18 @@ metacore_stage_material_templates(MetaCorePlayer)
if(METACORE_BUILD_TESTS)
enable_testing()
add_executable(MetaCoreDeliveryTests tests/MetaCoreDeliveryTests.cpp)
add_executable(MetaCoreDeliveryTests
tests/MetaCoreDeliveryTests.cpp
Source/MetaCoreRender/Private/MetaCoreGlibcCompat.cpp
)
target_link_libraries(MetaCoreDeliveryTests PRIVATE MetaCoreDelivery)
target_compile_options(MetaCoreDeliveryTests PRIVATE ${METACORE_COMMON_WARNINGS})
add_test(NAME MetaCoreDeliveryTests COMMAND MetaCoreDeliveryTests)
add_executable(MetaCoreScriptingTests tests/MetaCoreScriptingTests.cpp)
add_executable(MetaCoreScriptingTests
tests/MetaCoreScriptingTests.cpp
Source/MetaCoreRender/Private/MetaCoreGlibcCompat.cpp
)
target_link_libraries(MetaCoreScriptingTests PRIVATE MetaCoreScripting)
target_compile_options(MetaCoreScriptingTests PRIVATE ${METACORE_COMMON_WARNINGS})
target_compile_definitions(MetaCoreScriptingTests PRIVATE METACORE_SOURCE_ROOT="${CMAKE_SOURCE_DIR}")
@ -1035,6 +1050,14 @@ if(METACORE_BUILD_TESTS)
target_compile_options(MetaCoreRenderTests PRIVATE ${METACORE_COMMON_WARNINGS})
add_test(NAME MetaCoreRenderTests COMMAND MetaCoreRenderTests)
add_executable(MetaCoreRuntimeUiTests
tests/MetaCoreRuntimeUiTests.cpp
Source/MetaCoreRender/Private/MetaCoreGlibcCompat.cpp
)
target_link_libraries(MetaCoreRuntimeUiTests PRIVATE MetaCoreRuntimeUi MetaCoreScene)
target_compile_options(MetaCoreRuntimeUiTests PRIVATE ${METACORE_COMMON_WARNINGS})
add_test(NAME MetaCoreRuntimeUiTests COMMAND MetaCoreRuntimeUiTests)
add_executable(MetaCoreRenderBenchmark tests/MetaCoreRenderBenchmark.cpp)
target_link_libraries(MetaCoreRenderBenchmark PRIVATE MetaCoreRendering)
target_compile_options(MetaCoreRenderBenchmark PRIVATE ${METACORE_COMMON_WARNINGS})

View File

@ -8889,7 +8889,7 @@ bool MetaCoreBuiltinCookService::CookScene(const std::filesystem::path& relative
}
bool MetaCoreBuiltinCookService::CookRuntimeUiAssets() {
if (AssetDatabaseService_ == nullptr || ReflectionRegistry_ == nullptr || !AssetDatabaseService_->HasProject()) {
if (AssetDatabaseService_ == nullptr || PackageService_ == nullptr || ReflectionRegistry_ == nullptr || !AssetDatabaseService_->HasProject()) {
return false;
}
@ -8900,38 +8900,104 @@ bool MetaCoreBuiltinCookService::CookRuntimeUiAssets() {
const std::filesystem::path targetUiRoot = project.BuildPath / "Ui";
const std::filesystem::path targetManifestPath = targetRuntimeRoot / "Ui.mcruntime";
if (!std::filesystem::is_directory(sourceUiRoot) || !std::filesystem::exists(sourceManifestPath)) {
return false;
std::error_code errorCode;
std::filesystem::create_directories(targetRuntimeRoot, errorCode);
std::filesystem::create_directories(targetUiRoot, errorCode);
if (errorCode) return false;
const MetaCoreTypeRegistry& registry = ReflectionRegistry_->GetTypeRegistry();
const auto writeText = [](const std::filesystem::path& path, const std::string& text) {
std::ofstream output(path, std::ios::binary | std::ios::trunc);
return output.is_open() && static_cast<bool>(output.write(text.data(), static_cast<std::streamsize>(text.size())));
};
std::optional<MetaCoreSceneDocument> startupScene;
if (!project.StartupScenePath.empty()) {
const std::filesystem::path scenePath = project.RootPath / project.StartupScenePath;
if (std::filesystem::is_regular_file(scenePath, errorCode)) {
if (scenePath.extension() == ".json") {
startupScene = MetaCoreSceneSerializer::LoadSceneFromJson(scenePath, registry);
} else if (const auto package = PackageService_->ReadPackage(scenePath); package.has_value()) {
startupScene = MetaCoreReadTypedPackagePayload<MetaCoreSceneDocument>(*package, registry, "MetaCoreSceneDocument");
}
}
errorCode.clear();
}
const auto manifest = MetaCoreReadRuntimeUiManifest(sourceManifestPath, ReflectionRegistry_->GetTypeRegistry());
if (!manifest.has_value()) {
return false;
}
for (const MetaCoreRuntimeUiDocumentEntry& entry : manifest->Documents) {
const std::filesystem::path absoluteRmlPath = sourceUiRoot / entry.RmlPath;
const std::filesystem::path normalizedRelative = entry.RmlPath.lexically_normal();
if (entry.DocumentId.empty() ||
normalizedRelative.empty() ||
normalizedRelative.is_absolute() ||
*normalizedRelative.begin() == ".." ||
absoluteRmlPath.extension() != ".rml" ||
!std::filesystem::exists(absoluteRmlPath)) {
return false;
MetaCoreRuntimeUiManifest cookedManifest;
cookedManifest.SchemaVersion = 2;
cookedManifest.GeneratorVersion = METACORE_UI_GENERATOR_VERSION;
std::unordered_set<MetaCoreAssetGuid, MetaCoreAssetGuidHasher> compiledAssets;
if (startupScene.has_value()) {
for (const MetaCoreGameObjectData& object : startupScene->GameObjects) {
if (!object.UiRenderer.has_value() || !object.UiRenderer->Visible ||
!object.UiRenderer->UiDocumentAssetGuid.IsValid()) continue;
const MetaCoreAssetGuid guid = object.UiRenderer->UiDocumentAssetGuid;
const auto record = AssetDatabaseService_->FindAssetByGuid(guid);
if (!record.has_value() || record->Type != "ui_document") return false;
if (compiledAssets.insert(guid).second) {
std::optional<MetaCoreUiDocument> document;
const std::filesystem::path sourcePath = project.RootPath / record->RelativePath;
if (sourcePath.extension() == ".json") {
document = MetaCoreSceneSerializer::LoadUiFromJson(sourcePath, registry);
} else if (const auto package = PackageService_->ReadPackage(sourcePath); package.has_value()) {
document = MetaCoreReadTypedPackagePayload<MetaCoreUiDocument>(*package, registry, "MetaCoreUiDocument");
}
if (!document.has_value()) return false;
const MetaCoreCompiledUiDocument compiled = MetaCoreCompileUiDocument(*document);
if (!compiled.Succeeded) return false;
const std::filesystem::path outputDirectory = targetUiRoot / "Compiled" / guid.ToString();
std::filesystem::create_directories(outputDirectory / "Assets", errorCode);
if (errorCode ||
!writeText(outputDirectory / "document.rml", compiled.Rml) ||
!writeText(outputDirectory / "document.rcss", compiled.Rcss)) return false;
for (const MetaCoreAssetGuid dependency : compiled.Dependencies) {
const auto dependencyRecord = AssetDatabaseService_->FindAssetByGuid(dependency);
if (!dependencyRecord.has_value()) return false;
if (dependencyRecord->Type != "texture") continue;
std::filesystem::path dependencySource = !dependencyRecord->SourcePath.empty()
? dependencyRecord->SourcePath : dependencyRecord->RelativePath;
if (dependencySource.is_relative()) dependencySource = project.RootPath / dependencySource;
errorCode.clear();
std::filesystem::copy_file(dependencySource, outputDirectory / "Assets" / (dependency.ToString() + ".png"),
std::filesystem::copy_options::overwrite_existing, errorCode);
if (errorCode) return false;
}
}
MetaCoreRuntimeUiDocumentEntry entry;
entry.DocumentId = "scene-" + std::to_string(object.Id) + "-ui";
entry.RmlPath = std::filesystem::path("Compiled") / guid.ToString() / "document.rml";
entry.Layer = object.UiRenderer->Layer;
entry.Visible = object.UiRenderer->Visible;
entry.InputEnabled = object.UiRenderer->InputEnabled;
entry.UiDocumentAssetGuid = guid;
entry.UiDocumentAssetGuidString = guid.ToString();
entry.InstanceId = object.Id;
entry.SceneObjectId = object.Id;
entry.RenderMode = static_cast<std::uint32_t>(object.UiRenderer->RenderMode);
entry.TargetWidth = 0;
entry.TargetHeight = 0;
cookedManifest.Documents.push_back(std::move(entry));
}
}
if (!MetaCoreCopyDirectoryTree(sourceUiRoot, targetUiRoot)) {
return false;
if (cookedManifest.Documents.empty()) {
const auto legacyManifest = MetaCoreReadRuntimeUiManifest(sourceManifestPath, registry);
if (!legacyManifest.has_value() || !std::filesystem::is_directory(sourceUiRoot)) return false;
for (const MetaCoreRuntimeUiDocumentEntry& entry : legacyManifest->Documents) {
const std::filesystem::path normalizedRelative = entry.RmlPath.lexically_normal();
if (entry.DocumentId.empty() || normalizedRelative.empty() || normalizedRelative.is_absolute() ||
*normalizedRelative.begin() == ".." || normalizedRelative.extension() != ".rml" ||
!std::filesystem::exists(sourceUiRoot / normalizedRelative)) return false;
}
if (!MetaCoreCopyDirectoryTree(sourceUiRoot, targetUiRoot)) return false;
cookedManifest = *legacyManifest;
}
std::error_code errorCode;
std::filesystem::create_directories(targetRuntimeRoot, errorCode);
if (errorCode) {
return false;
}
std::filesystem::copy_file(sourceManifestPath, targetManifestPath, std::filesystem::copy_options::overwrite_existing, errorCode);
return !errorCode;
std::sort(cookedManifest.Documents.begin(), cookedManifest.Documents.end(), [](const auto& lhs, const auto& rhs) {
return lhs.Layer != rhs.Layer ? lhs.Layer < rhs.Layer : lhs.InstanceId < rhs.InstanceId;
});
return MetaCoreWriteRuntimeUiManifest(targetManifestPath, cookedManifest, registry);
}
std::filesystem::path MetaCoreBuiltinCookService::GetCookedPathForAsset(const MetaCoreAssetGuid& assetGuid) const {
@ -10866,7 +10932,7 @@ public:
const MetaCoreLogLevel mapped = level >= 2U ? MetaCoreLogLevel::Error : level == 1U ? MetaCoreLogLevel::Warning : MetaCoreLogLevel::Info;
editorContext.AddConsoleMessage(mapped, std::string(category), std::string(message));
}, Input_ ? &Input_->GetRuntime() : nullptr, Physics_ ? Physics_->GetOrCreate(editorContext) : nullptr,
editorContext.GetActiveAnimationRuntime()
editorContext.GetActiveAnimationRuntime(), Input_ ? &Input_->GetRuntimeUi() : nullptr
);
Runtime_->Start();
editorContext.SetActiveScriptRuntime(Runtime_.get());
@ -10904,8 +10970,22 @@ public:
MetaCoreScriptValueV1 value{}; value.Type = MetaCoreScriptAbiValue_String;
std::snprintf(value.Text, sizeof(value.Text), "%s", text.c_str()); return value;
};
Runtime_->Publish("MetaCore.UI", {textValue(event.DocumentId), textValue(event.ElementId),
textValue(event.Action), textValue(event.Type), textValue(event.Value)});
const auto intValue = [](std::int64_t number) {
MetaCoreScriptValueV1 value{}; value.Type = MetaCoreScriptAbiValue_Int;
value.IntValue = number; return value;
};
const std::vector<MetaCoreScriptValueV1> payload{
textValue(event.DocumentId), textValue(event.ElementId), textValue(event.Action),
textValue(event.Type), textValue(event.Value), textValue(event.OldValue),
intValue(static_cast<std::int64_t>(event.InstanceId)),
textValue(event.DocumentAssetGuid.ToString()),
intValue(static_cast<std::int64_t>(event.EventType)),
intValue(static_cast<std::int64_t>(event.InputSource))
};
Runtime_->Publish("MetaCore.UI", payload);
if (event.SceneObjectId != 0) {
Runtime_->PublishToObject(event.SceneObjectId, "MetaCore.UI", payload);
}
});
}
}

View File

@ -18,6 +18,7 @@
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h"
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h"
#include "MetaCoreRuntimeInput/MetaCoreInputMap.h"
#include "MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h"
#include "MetaCorePlatform/MetaCoreWindow.h"
#include "MetaCorePhysics/MetaCorePhysics.h"
#include "MetaCoreAnimation/MetaCoreAnimation.h"
@ -346,6 +347,12 @@ void DrawMaterialTextureSlotSummary(
return "Image";
case MetaCoreUiNodeType::Button:
return "Button";
case MetaCoreUiNodeType::Input:
return "Input";
case MetaCoreUiNodeType::List:
return "List";
case MetaCoreUiNodeType::Scroll:
return "Scroll";
case MetaCoreUiNodeType::Panel:
default:
return "Panel";
@ -4678,6 +4685,11 @@ private:
if (ImGui::Button("+ 按钮")) AddNativeNode(MetaCoreUiNodeType::Button);
ImGui::SameLine();
if (ImGui::Button("+ 图片")) AddNativeNode(MetaCoreUiNodeType::Image);
if (ImGui::Button("+ 输入")) AddNativeNode(MetaCoreUiNodeType::Input);
ImGui::SameLine();
if (ImGui::Button("+ 列表")) AddNativeNode(MetaCoreUiNodeType::List);
ImGui::SameLine();
if (ImGui::Button("+ 滚动")) AddNativeNode(MetaCoreUiNodeType::Scroll);
ImGui::Separator();
const bool hasSelection = !SelectedNodeId_.empty() && MetaCoreFindUiNode(Document_, SelectedNodeId_) != nullptr;
ImGui::BeginDisabled(!hasSelection);
@ -4957,6 +4969,22 @@ private:
return;
}
if (node.Type == MetaCoreUiNodeType::Input) {
node.Interactable = true;
node.Text.clear();
node.Placeholder = "请输入";
node.Style.BackgroundColor = glm::vec3(0.10F, 0.12F, 0.15F);
node.RectTransform.Size = glm::vec3(std::max(220.0F, node.RectTransform.Size.x), std::max(40.0F, node.RectTransform.Size.y), 0.0F);
return;
}
if (node.Type == MetaCoreUiNodeType::List || node.Type == MetaCoreUiNodeType::Scroll) {
node.Interactable = true;
node.Style.BackgroundColor = glm::vec3(0.08F, 0.10F, 0.13F);
node.RectTransform.ClipChildren = true;
node.RectTransform.Size = glm::vec3(std::max(280.0F, node.RectTransform.Size.x), std::max(200.0F, node.RectTransform.Size.y), 0.0F);
return;
}
node.Interactable = false;
node.Action.clear();
if (node.Type == MetaCoreUiNodeType::Text) {
@ -5148,6 +5176,10 @@ private:
case MetaCoreUiNodeType::Button:
ApplyNativeButtonStylePreset(node, 0);
break;
case MetaCoreUiNodeType::Input:
case MetaCoreUiNodeType::List:
case MetaCoreUiNodeType::Scroll:
break;
}
}
@ -5190,6 +5222,11 @@ private:
drawPreset("幽灵按钮", 3, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativeButtonStylePreset(target, index); });
drawPreset("工具按钮", 4, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativeButtonStylePreset(target, index); });
break;
case MetaCoreUiNodeType::Input:
case MetaCoreUiNodeType::List:
case MetaCoreUiNodeType::Scroll:
ImGui::TextDisabled("该控件使用主题默认样式。");
break;
}
ImGui::EndCombo();
return changed;
@ -5260,6 +5297,19 @@ private:
Dirty_ |= DrawNativeString("文档名称", Document_.Name, 128);
if (ImGui::DragInt("参考宽度", &Document_.ReferenceWidth, 1.0F, 320, 7680)) Dirty_ = true;
if (ImGui::DragInt("参考高度", &Document_.ReferenceHeight, 1.0F, 240, 4320)) Dirty_ = true;
const char* scaleModeLabels[] = {"固定像素", "随分辨率缩放", "固定物理尺寸"};
int scaleMode = static_cast<int>(Document_.ScaleMode);
if (ImGui::Combo("Canvas 缩放", &scaleMode, scaleModeLabels, IM_ARRAYSIZE(scaleModeLabels))) {
Document_.ScaleMode = static_cast<MetaCoreUiCanvasScaleMode>(std::clamp(scaleMode, 0, 2));
Dirty_ = true;
}
Dirty_ |= ImGui::SliderFloat("宽高匹配", &Document_.MatchWidthOrHeight, 0.0F, 1.0F);
Dirty_ |= ImGui::DragFloat("参考 DPI", &Document_.ReferenceDpi, 1.0F, 48.0F, 600.0F);
Dirty_ |= ImGui::DragFloat("最小缩放", &Document_.MinScale, 0.05F, 0.1F, 8.0F);
Dirty_ |= ImGui::DragFloat("最大缩放", &Document_.MaxScale, 0.05F, Document_.MinScale, 8.0F);
Dirty_ |= ImGui::Checkbox("使用安全区", &Document_.UseSafeArea);
Dirty_ |= DrawNativeString("默认 Locale", Document_.DefaultLocale, 64);
Dirty_ |= DrawNativeString("Fallback Locale", Document_.FallbackLocale, 64);
Dirty_ |= ImGui::ColorEdit3("UI 背景颜色", &Document_.BackgroundColor.x);
if (ImGui::SliderFloat("UI 背景透明度", &Document_.BackgroundAlpha, 0.0F, 1.0F, "%.2f")) {
Document_.BackgroundAlpha = std::clamp(Document_.BackgroundAlpha, 0.0F, 1.0F);
@ -5287,11 +5337,11 @@ private:
return;
}
Dirty_ |= DrawNativeString("名称", node->Name, 128);
const char* nodeTypeLabels[] = {"面板", "文本", "图片", "按钮"};
const char* nodeTypeLabels[] = {"面板", "文本", "图片", "按钮", "输入", "列表", "滚动"};
int nodeTypeIndex = static_cast<int>(node->Type);
if (ImGui::Combo("类型", &nodeTypeIndex, nodeTypeLabels, IM_ARRAYSIZE(nodeTypeLabels))) {
const MetaCoreUiNodeType previousType = node->Type;
node->Type = static_cast<MetaCoreUiNodeType>(std::clamp(nodeTypeIndex, 0, 3));
node->Type = static_cast<MetaCoreUiNodeType>(std::clamp(nodeTypeIndex, 0, 6));
ApplyNativeNodeTypeDefaults(*node, previousType);
ApplyNativeDefaultStylePreset(*node);
Dirty_ = true;
@ -5316,17 +5366,41 @@ private:
node->RectTransform.Size.y = std::max(1.0F, size[1]);
Dirty_ = true;
}
float anchorMin[2]{node->RectTransform.AnchorMin.x, node->RectTransform.AnchorMin.y};
float anchorMax[2]{node->RectTransform.AnchorMax.x, node->RectTransform.AnchorMax.y};
float pivot[2]{node->RectTransform.Pivot.x, node->RectTransform.Pivot.y};
if (ImGui::DragFloat2("Anchor Min", anchorMin, 0.01F, 0.0F, 1.0F)) {
node->RectTransform.AnchorMin = {std::clamp(anchorMin[0], 0.0F, 1.0F), std::clamp(anchorMin[1], 0.0F, 1.0F), 0.0F};
Dirty_ = true;
}
if (ImGui::DragFloat2("Anchor Max", anchorMax, 0.01F, 0.0F, 1.0F)) {
node->RectTransform.AnchorMax = {std::max(node->RectTransform.AnchorMin.x, std::clamp(anchorMax[0], 0.0F, 1.0F)),
std::max(node->RectTransform.AnchorMin.y, std::clamp(anchorMax[1], 0.0F, 1.0F)), 0.0F};
Dirty_ = true;
}
if (ImGui::DragFloat2("Pivot", pivot, 0.01F, 0.0F, 1.0F)) {
node->RectTransform.Pivot = {std::clamp(pivot[0], 0.0F, 1.0F), std::clamp(pivot[1], 0.0F, 1.0F), 0.0F};
Dirty_ = true;
}
Dirty_ |= ImGui::DragInt("导航顺序", &node->NavigationOrder, 1.0F, -1, 100000);
Dirty_ |= ImGui::SliderFloat("透明度", &node->Style.Opacity, 0.0F, 1.0F);
Dirty_ |= ImGui::DragFloat("圆角", &node->Style.BorderRadius, 0.5F, 0.0F, 256.0F);
Dirty_ |= DrawNativeString("主题 Token", node->Style.ThemeToken, 128);
Dirty_ |= DrawNativeString("本地化 Key", node->LocalizationKey, 128);
const bool isPanel = node->Type == MetaCoreUiNodeType::Panel;
const bool isText = node->Type == MetaCoreUiNodeType::Text;
const bool isImage = node->Type == MetaCoreUiNodeType::Image;
const bool isButton = node->Type == MetaCoreUiNodeType::Button;
const bool isInput = node->Type == MetaCoreUiNodeType::Input;
const bool isList = node->Type == MetaCoreUiNodeType::List;
const bool isScroll = node->Type == MetaCoreUiNodeType::Scroll;
if (isPanel || isButton || isImage) {
Dirty_ |= ImGui::ColorEdit3(isImage ? "背景占位" : "背景", &node->Style.BackgroundColor.x);
}
if (isText || isButton) {
if (isText || isButton || isInput) {
Dirty_ |= DrawNativeString("文本", node->Text, 256);
Dirty_ |= ImGui::ColorEdit3("文字颜色", &node->Style.TextColor.x);
Dirty_ |= ImGui::DragFloat("字号", &node->Style.FontSize, 0.5F, 1.0F, 200.0F);
@ -5361,6 +5435,14 @@ private:
}
}
if (isInput) {
Dirty_ |= DrawNativeString("占位文本", node->Placeholder, 256);
Dirty_ |= ImGui::Checkbox("只读", &node->ReadOnly);
Dirty_ |= ImGui::DragInt("最大长度", &node->MaxLength, 1.0F, 0, 4096);
}
if (isList) Dirty_ |= ImGui::DragInt("选中索引", &node->SelectedIndex, 1.0F, -1, 100000);
if (isList || isScroll) Dirty_ |= ImGui::Checkbox("裁剪子节点", &node->RectTransform.ClipChildren);
ImGui::Separator();
if (isButton) {
bool interactable = node->Interactable;
@ -5370,9 +5452,39 @@ private:
}
Dirty_ |= DrawNativeString("按钮 Action", node->Action, 128);
}
if (isText || isButton) {
Dirty_ |= DrawNativeString("数据绑定", node->DataBinding, 128);
Dirty_ |= DrawNativeString("绑定格式", node->DataFormat, 128);
if (isText || isButton || isInput || isList) {
if (node->Bindings.empty() && ImGui::Button("添加类型化绑定")) {
node->Bindings.push_back({"script.value",
isInput ? MetaCoreUiBindingTarget::Value : isList ? MetaCoreUiBindingTarget::Items : MetaCoreUiBindingTarget::Text,
isInput ? MetaCoreUiBindingMode::TwoWay : MetaCoreUiBindingMode::OneWay, {}, {}, {}});
Dirty_ = true;
}
for (std::size_t index = 0; index < node->Bindings.size(); ++index) {
ImGui::PushID(static_cast<int>(index));
ImGui::SeparatorText("绑定");
auto& binding = node->Bindings[index];
Dirty_ |= DrawNativeString("Source Path", binding.SourcePath, 192);
const char* targetLabels[] = {"Text", "Value", "Visible", "Enabled", "Items", "SelectedIndex"};
int target = static_cast<int>(binding.Target);
if (ImGui::Combo("Target", &target, targetLabels, IM_ARRAYSIZE(targetLabels))) {
binding.Target = static_cast<MetaCoreUiBindingTarget>(std::clamp(target, 0, 5));
Dirty_ = true;
}
bool twoWay = binding.Mode == MetaCoreUiBindingMode::TwoWay;
if (ImGui::Checkbox("双向", &twoWay)) {
binding.Mode = twoWay ? MetaCoreUiBindingMode::TwoWay : MetaCoreUiBindingMode::OneWay;
Dirty_ = true;
}
Dirty_ |= DrawNativeString("Format", binding.Format, 96);
Dirty_ |= DrawNativeString("Fallback", binding.Fallback, 128);
if (ImGui::SmallButton("删除绑定")) {
node->Bindings.erase(node->Bindings.begin() + static_cast<std::ptrdiff_t>(index));
Dirty_ = true;
ImGui::PopID();
break;
}
ImGui::PopID();
}
}
if (ImGui::Button("删除节点")) {
DeleteNativeNode(node->Id);
@ -5430,7 +5542,7 @@ private:
void AddNativeNode(MetaCoreUiNodeType type) {
MetaCoreUiNodeDocument node;
node.Type = type;
node.Id = MetaCoreMakeUniqueUiNodeId(Document_, type == MetaCoreUiNodeType::Button ? "Button" : type == MetaCoreUiNodeType::Text ? "Text" : type == MetaCoreUiNodeType::Image ? "Image" : "Panel");
node.Id = MetaCoreMakeUniqueUiNodeId(Document_, MetaCoreUiNodeTypeLabel(type));
node.Name = node.Id;
node.RectTransform.Position = glm::vec3(80.0F + Document_.Nodes.size() * 12.0F, 80.0F + Document_.Nodes.size() * 12.0F, 0.0F);
node.RectTransform.Size = type == MetaCoreUiNodeType::Panel ? glm::vec3(320.0F, 180.0F, 0.0F) : glm::vec3(180.0F, 44.0F, 0.0F);
@ -5438,7 +5550,8 @@ private:
ApplyNativeDefaultStylePreset(node);
if (!SelectedNodeId_.empty()) {
const MetaCoreUiNodeDocument* selectedNode = MetaCoreFindUiNode(Document_, SelectedNodeId_);
if (selectedNode != nullptr && selectedNode->Type == MetaCoreUiNodeType::Panel) {
if (selectedNode != nullptr && (selectedNode->Type == MetaCoreUiNodeType::Panel ||
selectedNode->Type == MetaCoreUiNodeType::List || selectedNode->Type == MetaCoreUiNodeType::Scroll)) {
node.ParentId = SelectedNodeId_;
}
}
@ -5703,8 +5816,28 @@ private:
rcssPath.replace_extension(".rcss");
const MetaCoreNativeUiImageSourceMap imageSources =
PrepareNativeImageRuntimeSources(editorContext, assetDatabaseService, generatedDirectory);
const bool wroteRml = MetaCoreWriteTextFile(project.UiPath / rmlPath, MetaCoreCompileUiDocumentToRml(Document_, rcssPath.filename(), imageSources));
const bool wroteRcss = MetaCoreWriteTextFile(project.UiPath / rcssPath, MetaCoreBuildNativeUiRcssDocument(Document_));
const MetaCoreCompiledUiDocument compiled = MetaCoreCompileUiDocument(Document_);
if (!compiled.Succeeded) {
for (const MetaCoreUiDiagnostic& diagnostic : compiled.Diagnostics)
editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "UI", diagnostic.NodeId + ": " + diagnostic.Message);
return;
}
std::string rml = compiled.Rml;
const std::string generatedStylesheet = "document.rcss";
const std::string actualStylesheet = rcssPath.filename().generic_string();
if (const std::size_t position = rml.find(generatedStylesheet); position != std::string::npos)
rml.replace(position, generatedStylesheet.size(), actualStylesheet);
for (const auto& [guid, imagePath] : imageSources) {
const std::string generatedPath = "Assets/" + guid.ToString() + ".png";
std::size_t position = 0;
while ((position = rml.find(generatedPath, position)) != std::string::npos) {
const std::string actualPath = imagePath.generic_string();
rml.replace(position, generatedPath.size(), actualPath);
position += actualPath.size();
}
}
const bool wroteRml = MetaCoreWriteTextFile(project.UiPath / rmlPath, rml);
const bool wroteRcss = MetaCoreWriteTextFile(project.UiPath / rcssPath, compiled.Rcss);
if (!wroteRml || !wroteRcss) {
editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "UI", "导出运行时 RML 失败");
return;
@ -10211,12 +10344,10 @@ public:
moduleRegistry.RegisterMenuProvider(std::make_unique<MetaCoreDefaultMenuProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreHierarchyPanelProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreScenePanelProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreRuntimeUiPanelProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreInputMapPanelProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreAnimationToolsPanelProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCorePhysicsSettingsPanelProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreRenderingSettingsPanelProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreRmlUiVisualEditorPanelProvider>());
moduleRegistry.RegisterService<MetaCoreIUiDesignerService>(std::make_shared<MetaCoreNativeUiDesignerService>());
// The central Scene / Game viewport is not a dockable provider, but it keeps
// its existing visibility state separate from the scene settings panel.

View File

@ -2,6 +2,7 @@
#include <cstring>
#include <fstream>
#include <limits>
namespace MetaCore {
@ -14,7 +15,14 @@ namespace {
}
input.seekg(0, std::ios::end);
const auto size = static_cast<std::size_t>(input.tellg());
const std::streampos end = input.tellg();
if (end < std::streampos{0}) {
return std::nullopt;
}
const auto size = static_cast<std::size_t>(end);
if (size > static_cast<std::size_t>(std::numeric_limits<std::streamsize>::max())) {
return std::nullopt;
}
input.seekg(0, std::ios::beg);
std::vector<std::byte> bytes(size);

View File

@ -1,6 +1,7 @@
#pragma once
#include "MetaCoreFoundation/MetaCoreId.h"
#include "MetaCoreFoundation/MetaCoreAssetGuid.h"
#include "MetaCoreFoundation/MetaCoreReflection.h"
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h"
@ -148,6 +149,29 @@ struct MetaCoreRuntimeUiDocumentEntry {
MC_PROPERTY()
bool InputEnabled = true;
// Schema v2 fields. RmlPath remains readable for legacy manifests only.
// Runtime convenience value. The string below is the serialized ABI field so
// RuntimeData documents remain readable without registering Foundation types.
MetaCoreAssetGuid UiDocumentAssetGuid{};
MC_PROPERTY()
std::string UiDocumentAssetGuidString{};
MC_PROPERTY()
std::uint64_t InstanceId = 0;
MC_PROPERTY()
std::uint64_t SceneObjectId = 0;
MC_PROPERTY()
std::uint32_t RenderMode = 0;
MC_PROPERTY()
std::int32_t TargetWidth = 0;
MC_PROPERTY()
std::int32_t TargetHeight = 0;
};
MC_STRUCT()
@ -156,6 +180,12 @@ struct MetaCoreRuntimeUiManifest {
MC_PROPERTY()
std::vector<MetaCoreRuntimeUiDocumentEntry> Documents{};
MC_PROPERTY()
std::uint32_t SchemaVersion = 2;
MC_PROPERTY()
std::uint32_t GeneratorVersion = 1;
};
[[nodiscard]] bool MetaCoreWriteRuntimeDataSourcesDocument(

View File

@ -55,21 +55,58 @@ namespace {
return !relative.empty() && !relative.is_absolute() && *relative.begin() != "..";
}
[[nodiscard]] std::string FormatValue(const MetaCoreRuntimeDataValue& value, const std::string& format) {
[[nodiscard]] std::string EscapeRmlText(std::string_view value) {
std::string result;
result.reserve(value.size());
for (char character : value) {
switch (character) {
case '&': result += "&amp;"; break;
case '<': result += "&lt;"; break;
case '>': result += "&gt;"; break;
case '"': result += "&quot;"; break;
default: result += character; break;
}
}
return result;
}
[[nodiscard]] std::string FormatUiValue(const MetaCoreRuntimeUiValue& value, const std::string& format) {
std::ostringstream stream;
if (!format.empty()) stream << std::fixed << std::setprecision(std::max(0, std::atoi(format.c_str())));
switch (value.Type) {
case MetaCoreRuntimeValueType::Bool: return value.BoolValue ? "true" : "false";
case MetaCoreRuntimeValueType::Int64: stream << value.Int64Value; break;
case MetaCoreRuntimeValueType::Double: stream << value.DoubleValue; break;
case MetaCoreRuntimeValueType::String: return value.StringValue;
case MetaCoreRuntimeValueType::Vec3:
stream << value.Vec3Value.x << ", " << value.Vec3Value.y << ", " << value.Vec3Value.z;
break;
case MetaCoreRuntimeUiValueType::Bool: return value.BoolValue ? "true" : "false";
case MetaCoreRuntimeUiValueType::Int64: stream << value.Int64Value; break;
case MetaCoreRuntimeUiValueType::Double: stream << value.DoubleValue; break;
case MetaCoreRuntimeUiValueType::String: return value.StringValue;
case MetaCoreRuntimeUiValueType::Vec3: stream << value.Vec3Value.x << ", " << value.Vec3Value.y << ", " << value.Vec3Value.z; break;
case MetaCoreRuntimeUiValueType::AssetRef: return value.AssetValue.ToString();
case MetaCoreRuntimeUiValueType::GameObjectRef: stream << value.ObjectValue; break;
}
return stream.str();
}
[[nodiscard]] MetaCoreRuntimeUiValue ToUiValue(const MetaCoreRuntimeDataValue& value) {
MetaCoreRuntimeUiValue result;
result.Type = static_cast<MetaCoreRuntimeUiValueType>(value.Type);
result.BoolValue = value.BoolValue;
result.Int64Value = value.Int64Value;
result.DoubleValue = value.DoubleValue;
result.StringValue = value.StringValue;
result.Vec3Value = value.Vec3Value;
return result;
}
[[nodiscard]] MetaCoreRuntimeUiEventType EventTypeFromRml(std::string_view type) {
if (type == "mouseover") return MetaCoreRuntimeUiEventType::PointerEnter;
if (type == "mouseout") return MetaCoreRuntimeUiEventType::PointerLeave;
if (type == "submit") return MetaCoreRuntimeUiEventType::Submit;
if (type == "change") return MetaCoreRuntimeUiEventType::ValueChanged;
if (type == "scroll") return MetaCoreRuntimeUiEventType::Scroll;
if (type == "focus") return MetaCoreRuntimeUiEventType::Focus;
if (type == "blur") return MetaCoreRuntimeUiEventType::Blur;
return MetaCoreRuntimeUiEventType::Click;
}
[[nodiscard]] Rml::Input::KeyIdentifier MetaCoreMapGlfwKeyToRml(int key) {
if (key >= GLFW_KEY_0 && key <= GLFW_KEY_9) {
return static_cast<Rml::Input::KeyIdentifier>(Rml::Input::KI_0 + (key - GLFW_KEY_0));
@ -541,13 +578,44 @@ public:
Rml::Element* target = event.GetTargetElement();
if (target == nullptr) return;
const std::string action = target->GetAttribute<Rml::String>("data-metacore-action", "");
if (action.empty()) return;
MetaCoreRuntimeUiEvent uiEvent;
const auto state = std::find_if(Owner_.Documents_.begin(), Owner_.Documents_.end(), [&](const DocumentState& value) {
return value.Entry.DocumentId == DocumentId_;
});
if (state != Owner_.Documents_.end()) {
uiEvent.InstanceId = state->Entry.InstanceId;
uiEvent.DocumentAssetGuid = state->Entry.UiDocumentAssetGuid;
uiEvent.SceneObjectId = state->Entry.SceneObjectId;
}
uiEvent.DocumentId = DocumentId_;
uiEvent.ElementId = target->GetId();
uiEvent.Action = action;
uiEvent.Type = event.GetType();
uiEvent.Value = target->GetAttribute<Rml::String>("value", "");
uiEvent.EventType = EventTypeFromRml(uiEvent.Type);
uiEvent.InputSource = Owner_.CurrentInputSource_;
uiEvent.Consumed = !event.IsPropagating();
const std::string writePath = target->GetAttribute<Rml::String>("data-metacore-bind-value", "");
const std::string writeMode = target->GetAttribute<Rml::String>("data-metacore-bind-mode-value", "");
if (uiEvent.EventType == MetaCoreRuntimeUiEventType::ValueChanged &&
writeMode == "two-way" && !writePath.empty()) {
uiEvent.Action = writePath;
if (!writePath.starts_with("script.") && !writePath.starts_with("scene.")) {
uiEvent.EventType = MetaCoreRuntimeUiEventType::WriteRequest;
}
const auto previous = Owner_.Values_.find(writePath);
if (previous != Owner_.Values_.end()) uiEvent.OldValue = FormatUiValue(previous->second, {});
MetaCoreRuntimeUiValue value;
value.Type = MetaCoreRuntimeUiValueType::String;
value.StringValue = uiEvent.Value;
Owner_.Values_[writePath] = std::move(value);
}
if (uiEvent.Action.empty() &&
uiEvent.EventType != MetaCoreRuntimeUiEventType::Focus &&
uiEvent.EventType != MetaCoreRuntimeUiEventType::Blur &&
uiEvent.EventType != MetaCoreRuntimeUiEventType::PointerEnter &&
uiEvent.EventType != MetaCoreRuntimeUiEventType::PointerLeave &&
uiEvent.EventType != MetaCoreRuntimeUiEventType::Scroll) return;
Owner_.Events_.push_back(uiEvent);
if (Owner_.Callback_) Owner_.Callback_(uiEvent);
}
@ -565,10 +633,15 @@ public:
Rml::Context* Context_ = nullptr;
std::vector<DocumentState> Documents_;
std::vector<std::unique_ptr<EventListener>> Listeners_;
std::unordered_map<std::string, MetaCoreRuntimeDataValue> Values_;
std::unordered_map<std::string, MetaCoreRuntimeUiValue> Values_;
std::unordered_map<std::string, std::vector<MetaCoreRuntimeUiRow>> Lists_;
std::vector<MetaCoreRuntimeUiEvent> Events_;
std::vector<std::string> Errors_;
std::vector<MetaCoreUiDiagnostic> Diagnostics_;
EventCallback Callback_;
MetaCoreRuntimeUiInputSource CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Programmatic;
std::string Locale_{"en-US"};
std::string Theme_{};
bool Initialized_ = false;
[[nodiscard]] bool HasInputEnabledDocument() const {
@ -577,20 +650,69 @@ public:
});
}
void ApplyBindings() {
void ApplyBindings(const std::string* onlyPath = nullptr) {
for (DocumentState& state : Documents_) {
if (state.Document == nullptr) continue;
Rml::ElementList elements;
state.Document->QuerySelectorAll(elements, "[data-metacore-bind]");
for (Rml::Element* element : elements) {
const std::string dataPointId = element->GetAttribute<Rml::String>("data-metacore-bind", "");
const auto value = Values_.find(dataPointId);
for (const std::string target : {"text", "value", "visible", "enabled", "selected-index"}) {
Rml::ElementList elements;
state.Document->QuerySelectorAll(elements, "[data-metacore-bind-" + target + "]");
for (Rml::Element* element : elements) {
const std::string path = element->GetAttribute<Rml::String>("data-metacore-bind-" + target, "");
if (onlyPath != nullptr && path != *onlyPath) continue;
const auto value = Values_.find(path);
if (value == Values_.end()) {
const std::string fallback = element->GetAttribute<Rml::String>("data-metacore-fallback-" + target, "");
if (fallback.empty()) continue;
if (target == "text") element->SetInnerRML(EscapeRmlText(fallback));
else element->SetAttribute(target == "enabled" ? "disabled" : target, fallback);
continue;
}
const std::string format = element->GetAttribute<Rml::String>("data-metacore-format-" + target, "");
const std::string text = FormatUiValue(value->second, format);
if (target == "text") element->SetInnerRML(EscapeRmlText(text));
else if (target == "value") element->SetAttribute("value", text);
else if (target == "visible") {
if (value->second.BoolValue) element->RemoveAttribute("hidden"); else element->SetAttribute("hidden", "true");
} else if (target == "enabled") {
if (value->second.BoolValue) element->RemoveAttribute("disabled"); else element->SetAttribute("disabled", "true");
} else element->SetAttribute("data-metacore-selected-index", text);
}
}
Rml::ElementList legacyElements;
state.Document->QuerySelectorAll(legacyElements, "[data-metacore-bind]");
for (Rml::Element* element : legacyElements) {
const std::string path = element->GetAttribute<Rml::String>("data-metacore-bind", "");
if (onlyPath != nullptr && path != *onlyPath) continue;
const auto value = Values_.find(path);
if (value == Values_.end()) continue;
const std::string format = element->GetAttribute<Rml::String>("data-metacore-format", "");
element->SetInnerRML(FormatValue(value->second, format));
element->SetInnerRML(EscapeRmlText(FormatUiValue(value->second,
element->GetAttribute<Rml::String>("data-metacore-format", ""))));
}
}
}
bool LoadEntry(const MetaCoreRuntimeUiDocumentEntry& entry) {
if (Context_ == nullptr) return false;
std::filesystem::path relativePath = entry.RmlPath;
if (relativePath.empty() && entry.UiDocumentAssetGuid.IsValid())
relativePath = std::filesystem::path("Compiled") / entry.UiDocumentAssetGuid.ToString() / "document.rml";
const std::filesystem::path absolutePath = UiRoot_ / relativePath;
if (entry.DocumentId.empty() || relativePath.empty() || !IsPathInsideRoot(UiRoot_, absolutePath)) {
Errors_.push_back("Invalid UI manifest entry: " + entry.DocumentId);
return false;
}
Rml::ElementDocument* document = Context_->LoadDocument(absolutePath.string());
if (document == nullptr) {
Errors_.push_back("Failed to load RML: " + relativePath.generic_string());
return false;
}
if (entry.Visible) document->Show(); else document->Hide();
Listeners_.push_back(std::make_unique<EventListener>(*this, entry.DocumentId));
for (const char* type : {"mouseover", "mouseout", "click", "submit", "change", "scroll", "focus", "blur"})
document->AddEventListener(type, Listeners_.back().get());
Documents_.push_back({entry, document});
return true;
}
};
MetaCoreRuntimeUiSystem::MetaCoreRuntimeUiSystem() : Impl_(std::make_unique<Impl>()) {}
@ -599,6 +721,11 @@ MetaCoreRuntimeUiSystem::~MetaCoreRuntimeUiSystem() { Shutdown(); }
bool MetaCoreRuntimeUiSystem::Initialize(MetaCoreWindow& window, const std::filesystem::path&, const std::filesystem::path& uiRoot, const MetaCoreRuntimeUiManifest& manifest) {
Shutdown();
if (uiRoot.empty() || !std::filesystem::is_directory(uiRoot)) return false;
if (manifest.SchemaVersion > METACORE_UI_SCHEMA_VERSION ||
manifest.GeneratorVersion > METACORE_UI_GENERATOR_VERSION) {
Impl_->Errors_.push_back("Runtime UI manifest version is newer than this engine");
return false;
}
Impl_->Window_ = &window;
Impl_->UiRoot_ = std::filesystem::weakly_canonical(uiRoot);
Impl_->RenderInterface_.SetUiRoot(Impl_->UiRoot_);
@ -617,23 +744,13 @@ bool MetaCoreRuntimeUiSystem::Initialize(MetaCoreWindow& window, const std::file
std::vector<MetaCoreRuntimeUiDocumentEntry> entries = manifest.Documents;
std::sort(entries.begin(), entries.end(), [](const auto& lhs, const auto& rhs) { return lhs.Layer < rhs.Layer; });
for (const MetaCoreRuntimeUiDocumentEntry& entry : entries) {
const std::filesystem::path absolutePath = Impl_->UiRoot_ / entry.RmlPath;
if (entry.DocumentId.empty() || entry.RmlPath.empty() || !IsPathInsideRoot(Impl_->UiRoot_, absolutePath)) {
Impl_->Errors_.push_back("Invalid UI manifest entry: " + entry.DocumentId);
continue;
}
Rml::ElementDocument* document = Impl_->Context_->LoadDocument(absolutePath.string());
if (document == nullptr) {
Impl_->Errors_.push_back("Failed to load RML: " + entry.RmlPath.generic_string());
continue;
}
if (entry.Visible) document->Show(); else document->Hide();
Impl_->Listeners_.push_back(std::make_unique<Impl::EventListener>(*Impl_, entry.DocumentId));
document->AddEventListener("click", Impl_->Listeners_.back().get());
document->AddEventListener("submit", Impl_->Listeners_.back().get());
document->AddEventListener("change", Impl_->Listeners_.back().get());
Impl_->Documents_.push_back({entry, document});
std::uint64_t generatedInstanceId = 1;
for (MetaCoreRuntimeUiDocumentEntry& entry : entries) {
if (!entry.UiDocumentAssetGuid.IsValid() && !entry.UiDocumentAssetGuidString.empty())
if (const auto parsed = MetaCoreAssetGuid::Parse(entry.UiDocumentAssetGuidString); parsed.has_value())
entry.UiDocumentAssetGuid = *parsed;
if (entry.InstanceId == 0) entry.InstanceId = generatedInstanceId++;
(void)Impl_->LoadEntry(entry);
}
Impl_->Initialized_ = true;
return true;
@ -660,6 +777,8 @@ void MetaCoreRuntimeUiSystem::Shutdown() {
Impl_->FileInterface_.reset();
Impl_->SystemInterface_.reset();
Impl_->Values_.clear();
Impl_->Lists_.clear();
Impl_->Diagnostics_.clear();
Impl_->Initialized_ = false;
}
@ -677,6 +796,7 @@ MetaCoreInputConsumption MetaCoreRuntimeUiSystem::ProcessInput() {
if (!Impl_->Initialized_ || Impl_->Context_ == nullptr || Impl_->Window_ == nullptr || !Impl_->HasInputEnabledDocument()) return consumed;
MetaCoreInput& input = Impl_->Window_->GetInput();
const glm::vec2 cursor = input.GetCursorPosition();
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Mouse;
consumed.Mouse = !Impl_->Context_->ProcessMouseMove(static_cast<int>(cursor.x), static_cast<int>(cursor.y), 0);
for (int button = 0; button < 3; ++button) {
const auto mouseButton = static_cast<MetaCoreMouseButton>(button);
@ -685,21 +805,99 @@ MetaCoreInputConsumption MetaCoreRuntimeUiSystem::ProcessInput() {
}
if (std::abs(input.GetMouseWheelDelta()) > 0.001F) consumed.Mouse = !Impl_->Context_->ProcessMouseWheel(input.GetMouseWheelDelta(), 0) || consumed.Mouse;
for (const MetaCoreKeyEvent& event : input.GetKeyEvents()) {
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Keyboard;
const Rml::Input::KeyIdentifier key = MetaCoreMapGlfwKeyToRml(event.Key);
const int modifiers = MetaCoreMapGlfwModifiersToRml(event.Modifiers);
const bool propagated = event.Pressed ? Impl_->Context_->ProcessKeyDown(key, modifiers) : Impl_->Context_->ProcessKeyUp(key, modifiers);
consumed.Keyboard = !propagated || consumed.Keyboard;
}
for (const char32_t codePoint : input.GetTextInput()) consumed.Keyboard = !Impl_->Context_->ProcessTextInput(static_cast<Rml::Character>(codePoint)) || consumed.Keyboard;
for (const char32_t codePoint : input.GetTextInput()) {
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Keyboard;
consumed.Keyboard = !Impl_->Context_->ProcessTextInput(static_cast<Rml::Character>(codePoint)) || consumed.Keyboard;
}
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Programmatic;
return consumed;
}
void MetaCoreRuntimeUiSystem::ApplyDataUpdates(const std::vector<MetaCoreRuntimeDataUpdate>& updates) {
if (!Impl_->Initialized_) return;
for (const MetaCoreRuntimeDataUpdate& update : updates) Impl_->Values_[update.DataPointId] = update.Value;
for (const MetaCoreRuntimeDataUpdate& update : updates) {
const std::string path = update.DataPointId.starts_with("runtime.") ? update.DataPointId : "runtime." + update.DataPointId;
Impl_->Values_[path] = ToUiValue(update.Value);
Impl_->Values_[update.DataPointId] = ToUiValue(update.Value);
}
Impl_->ApplyBindings();
}
bool MetaCoreRuntimeUiSystem::SetValue(const std::string& sourcePath, const MetaCoreRuntimeUiValue& value) {
if (!Impl_->Initialized_ || sourcePath.empty()) return false;
Impl_->Values_[sourcePath] = value;
Impl_->ApplyBindings(&sourcePath);
return true;
}
std::optional<MetaCoreRuntimeUiValue> MetaCoreRuntimeUiSystem::GetValue(const std::string& sourcePath) const {
const auto found = Impl_->Values_.find(sourcePath);
return found == Impl_->Values_.end() ? std::nullopt : std::optional<MetaCoreRuntimeUiValue>(found->second);
}
bool MetaCoreRuntimeUiSystem::SetListRows(const std::string& sourcePath, std::vector<MetaCoreRuntimeUiRow> rows) {
if (!Impl_->Initialized_ || sourcePath.empty()) return false;
Impl_->Lists_[sourcePath] = std::move(rows);
for (auto& state : Impl_->Documents_) {
if (state.Document == nullptr) continue;
Rml::ElementList elements;
state.Document->QuerySelectorAll(elements, "[data-metacore-bind-items]");
for (Rml::Element* element : elements) {
if (element->GetAttribute<Rml::String>("data-metacore-bind-items", "") != sourcePath) continue;
std::ostringstream markup;
std::size_t index = 0;
for (const auto& row : Impl_->Lists_[sourcePath]) {
markup << "<div class=\"mcui-list-row\" data-index=\"" << index++ << "\">";
std::vector<std::string> keys;
keys.reserve(row.size());
for (const auto& [key, _] : row) keys.push_back(key);
std::sort(keys.begin(), keys.end());
for (const std::string& key : keys)
markup << "<span data-field=\"" << EscapeRmlText(key) << "\">"
<< EscapeRmlText(FormatUiValue(row.at(key), {})) << "</span>";
markup << "</div>";
}
element->SetInnerRML(markup.str());
}
}
return true;
}
bool MetaCoreRuntimeUiSystem::RequestFocus(std::uint64_t instanceId, const std::string& elementId) {
for (auto& state : Impl_->Documents_) {
if (state.Entry.InstanceId != instanceId || state.Document == nullptr) continue;
if (Rml::Element* element = state.Document->GetElementById(elementId); element != nullptr) {
element->Focus();
return true;
}
}
return false;
}
bool MetaCoreRuntimeUiSystem::CreateInstance(const MetaCoreRuntimeUiDocumentEntry& entry) {
if (!Impl_->Initialized_ || entry.InstanceId == 0 ||
std::any_of(Impl_->Documents_.begin(), Impl_->Documents_.end(), [&](const auto& value) {
return value.Entry.InstanceId == entry.InstanceId;
})) return false;
return Impl_->LoadEntry(entry);
}
bool MetaCoreRuntimeUiSystem::DestroyInstance(std::uint64_t instanceId) {
const auto found = std::find_if(Impl_->Documents_.begin(), Impl_->Documents_.end(), [&](const auto& value) {
return value.Entry.InstanceId == instanceId;
});
if (found == Impl_->Documents_.end()) return false;
if (found->Document != nullptr) found->Document->Close();
Impl_->Documents_.erase(found);
return true;
}
bool MetaCoreRuntimeUiSystem::SetDocumentVisible(const std::string& documentId, bool visible) {
for (Impl::DocumentState& state : Impl_->Documents_) {
if (state.Entry.DocumentId != documentId || state.Document == nullptr) continue;
@ -710,10 +908,25 @@ bool MetaCoreRuntimeUiSystem::SetDocumentVisible(const std::string& documentId,
return false;
}
void MetaCoreRuntimeUiSystem::SetLocale(std::string locale) {
if (locale.empty()) return;
Impl_->Locale_ = std::move(locale);
for (auto& state : Impl_->Documents_) if (state.Document != nullptr) state.Document->SetAttribute("data-metacore-locale", Impl_->Locale_);
}
void MetaCoreRuntimeUiSystem::SetTheme(std::string theme) {
Impl_->Theme_ = std::move(theme);
for (auto& state : Impl_->Documents_) if (state.Document != nullptr) state.Document->SetAttribute("data-metacore-theme", Impl_->Theme_);
}
const std::string& MetaCoreRuntimeUiSystem::GetLocale() const { return Impl_->Locale_; }
const std::string& MetaCoreRuntimeUiSystem::GetTheme() const { return Impl_->Theme_; }
std::vector<MetaCoreRuntimeUiEvent> MetaCoreRuntimeUiSystem::ConsumeEvents() { return std::exchange(Impl_->Events_, {}); }
void MetaCoreRuntimeUiSystem::SetEventCallback(EventCallback callback) { Impl_->Callback_ = std::move(callback); }
bool MetaCoreRuntimeUiSystem::IsInitialized() const { return Impl_->Initialized_; }
std::size_t MetaCoreRuntimeUiSystem::GetLoadedDocumentCount() const { return Impl_->Documents_.size(); }
const std::vector<std::string>& MetaCoreRuntimeUiSystem::GetErrors() const { return Impl_->Errors_; }
std::vector<MetaCoreUiDiagnostic> MetaCoreRuntimeUiSystem::GetDiagnostics() const { return Impl_->Diagnostics_; }
} // namespace MetaCore

View File

@ -0,0 +1,255 @@
#include "MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h"
#include <algorithm>
#include <cmath>
#include <functional>
#include <iomanip>
#include <sstream>
#include <unordered_map>
#include <unordered_set>
namespace MetaCore {
namespace {
std::string Escape(std::string_view value) {
std::string result;
result.reserve(value.size());
for (char c : value) {
switch (c) {
case '&': result += "&amp;"; break;
case '<': result += "&lt;"; break;
case '>': result += "&gt;"; break;
case '"': result += "&quot;"; break;
case '\'': result += "&#39;"; break;
default: result += c; break;
}
}
return result;
}
const char* NodeClass(MetaCoreUiNodeType type) {
switch (type) {
case MetaCoreUiNodeType::Panel: return "panel";
case MetaCoreUiNodeType::Text: return "text";
case MetaCoreUiNodeType::Image: return "image";
case MetaCoreUiNodeType::Button: return "button";
case MetaCoreUiNodeType::Input: return "input";
case MetaCoreUiNodeType::List: return "list";
case MetaCoreUiNodeType::Scroll: return "scroll";
}
return "invalid";
}
const char* BindingTargetName(MetaCoreUiBindingTarget target) {
switch (target) {
case MetaCoreUiBindingTarget::Text: return "text";
case MetaCoreUiBindingTarget::Value: return "value";
case MetaCoreUiBindingTarget::Visible: return "visible";
case MetaCoreUiBindingTarget::Enabled: return "enabled";
case MetaCoreUiBindingTarget::Items: return "items";
case MetaCoreUiBindingTarget::SelectedIndex: return "selected-index";
}
return "text";
}
std::string Color(const glm::vec3& value, float alpha = 1.0F) {
std::ostringstream out;
out << "rgba(" << static_cast<int>(std::clamp(value.x, 0.0F, 1.0F) * 255.0F) << ','
<< static_cast<int>(std::clamp(value.y, 0.0F, 1.0F) * 255.0F) << ','
<< static_cast<int>(std::clamp(value.z, 0.0F, 1.0F) * 255.0F) << ','
<< std::fixed << std::setprecision(3) << std::clamp(alpha, 0.0F, 1.0F) << ')';
return out.str();
}
bool IsBindingPathValid(std::string_view path) {
return path.starts_with("runtime.") || path.starts_with("script.") || path.starts_with("scene.");
}
} // namespace
MetaCoreUiDocument MetaCoreMigrateUiDocument(MetaCoreUiDocument document) {
if (document.SchemaVersion >= METACORE_UI_SCHEMA_VERSION) {
return document;
}
for (MetaCoreUiNodeDocument& node : document.Nodes) {
if (!node.DataBinding.empty() && node.Bindings.empty()) {
node.Bindings.push_back({
node.DataBinding,
node.Type == MetaCoreUiNodeType::Input ? MetaCoreUiBindingTarget::Value : MetaCoreUiBindingTarget::Text,
node.Type == MetaCoreUiNodeType::Input ? MetaCoreUiBindingMode::TwoWay : MetaCoreUiBindingMode::OneWay,
node.DataFormat,
{},
{}
});
}
}
document.SchemaVersion = METACORE_UI_SCHEMA_VERSION;
document.GeneratorVersion = METACORE_UI_GENERATOR_VERSION;
return document;
}
std::vector<MetaCoreUiDiagnostic> MetaCoreValidateUiDocument(const MetaCoreUiDocument& document) {
std::vector<MetaCoreUiDiagnostic> diagnostics;
auto issue = [&](std::string node, std::string binding, std::string message) {
diagnostics.push_back({document.Name, std::move(node), std::move(binding), std::move(message), true});
};
if (document.SchemaVersion > METACORE_UI_SCHEMA_VERSION) issue({}, {}, "UI document schema is newer than this engine");
if (document.GeneratorVersion > METACORE_UI_GENERATOR_VERSION) issue({}, {}, "UI document generator is newer than this engine");
if (document.ReferenceWidth <= 0 || document.ReferenceHeight <= 0) issue({}, {}, "Reference resolution must be positive");
if (!std::isfinite(document.MatchWidthOrHeight) || document.MatchWidthOrHeight < 0.0F || document.MatchWidthOrHeight > 1.0F)
issue({}, {}, "MatchWidthOrHeight must be in [0, 1]");
if (document.ReferenceDpi <= 0.0F || document.MinScale <= 0.0F || document.MaxScale < document.MinScale)
issue({}, {}, "Canvas DPI or scale limits are invalid");
std::unordered_map<std::string, const MetaCoreUiNodeDocument*> nodes;
for (const MetaCoreUiNodeDocument& node : document.Nodes) {
if (node.Id.empty()) issue({}, {}, "Every UI node requires a stable ID");
else if (!nodes.emplace(node.Id, &node).second) issue(node.Id, {}, "Duplicate UI node ID");
if (node.RectTransform.MinSize.x < 0.0F || node.RectTransform.MinSize.y < 0.0F ||
node.RectTransform.MaxSize.x < node.RectTransform.MinSize.x ||
node.RectTransform.MaxSize.y < node.RectTransform.MinSize.y)
issue(node.Id, {}, "Node minimum/maximum size is invalid");
if (node.Type == MetaCoreUiNodeType::Input && node.MaxLength < 0) issue(node.Id, {}, "Input MaxLength cannot be negative");
for (const MetaCoreUiBindingDocument& binding : node.Bindings) {
if (!IsBindingPathValid(binding.SourcePath)) issue(node.Id, binding.SourcePath, "Binding path must use runtime., script. or scene.");
if (binding.Mode == MetaCoreUiBindingMode::TwoWay &&
binding.Target != MetaCoreUiBindingTarget::Value &&
binding.Target != MetaCoreUiBindingTarget::SelectedIndex)
issue(node.Id, binding.SourcePath, "Two-way binding is only valid for Value or SelectedIndex");
}
}
for (const std::string& root : document.RootNodeIds) {
if (!nodes.contains(root)) issue(root, {}, "Root node does not exist");
else if (!nodes.at(root)->ParentId.empty()) issue(root, {}, "Root node has a parent");
}
for (const MetaCoreUiNodeDocument& node : document.Nodes) {
if (!node.ParentId.empty() && !nodes.contains(node.ParentId)) issue(node.Id, {}, "Parent node does not exist");
std::unordered_set<std::string> children;
for (const std::string& child : node.Children) {
if (!children.insert(child).second) issue(node.Id, {}, "Child ID is duplicated");
if (!nodes.contains(child)) issue(node.Id, {}, "Child node does not exist: " + child);
else if (nodes.at(child)->ParentId != node.Id) issue(child, {}, "Parent/child relationship is inconsistent");
}
if (!node.ItemTemplateNodeId.empty() && !nodes.contains(node.ItemTemplateNodeId))
issue(node.Id, {}, "List item template node does not exist");
}
enum class Visit { Visiting, Done };
std::unordered_map<std::string, Visit> visited;
std::function<void(const std::string&)> walk = [&](const std::string& id) {
const auto state = visited.find(id);
if (state != visited.end()) {
if (state->second == Visit::Visiting) issue(id, {}, "UI hierarchy contains a cycle");
return;
}
visited[id] = Visit::Visiting;
const auto found = nodes.find(id);
if (found != nodes.end()) for (const std::string& child : found->second->Children) walk(child);
visited[id] = Visit::Done;
};
for (const std::string& root : document.RootNodeIds) walk(root);
for (const auto& [id, _] : nodes) if (!visited.contains(id)) issue(id, {}, "Node is unreachable from RootNodeIds");
return diagnostics;
}
MetaCoreCompiledUiDocument MetaCoreCompileUiDocument(const MetaCoreUiDocument& source) {
MetaCoreCompiledUiDocument result;
const MetaCoreUiDocument document = MetaCoreMigrateUiDocument(source);
result.Diagnostics = MetaCoreValidateUiDocument(document);
if (std::any_of(result.Diagnostics.begin(), result.Diagnostics.end(), [](const auto& value) { return value.IsError; })) return result;
std::unordered_map<std::string, const MetaCoreUiNodeDocument*> nodes;
for (const auto& node : document.Nodes) {
nodes[node.Id] = &node;
if (node.Style.ImageAssetGuid.IsValid()) result.Dependencies.push_back(node.Style.ImageAssetGuid);
}
for (MetaCoreAssetGuid guid : {document.ThemeAssetGuid, document.FontFamilyAssetGuid, document.LocalizationAssetGuid})
if (guid.IsValid()) result.Dependencies.push_back(guid);
std::sort(result.Dependencies.begin(), result.Dependencies.end(), [](const auto& a, const auto& b) { return a.ToString() < b.ToString(); });
result.Dependencies.erase(std::unique(result.Dependencies.begin(), result.Dependencies.end()), result.Dependencies.end());
std::ostringstream rml;
rml << "<rml><head><link type=\"text/rcss\" href=\"document.rcss\"/></head><body"
<< " data-metacore-schema=\"" << METACORE_UI_SCHEMA_VERSION << "\""
<< " data-metacore-scale-mode=\"" << static_cast<int>(document.ScaleMode) << "\""
<< " data-metacore-reference=\"" << document.ReferenceWidth << 'x' << document.ReferenceHeight << "\""
<< " data-metacore-match=\"" << document.MatchWidthOrHeight << "\""
<< " data-metacore-reference-dpi=\"" << document.ReferenceDpi << "\""
<< " data-metacore-safe-area=\"" << (document.UseSafeArea ? "true" : "false") << "\">\n";
std::function<void(const std::string&, int)> emit = [&](const std::string& id, int depth) {
const MetaCoreUiNodeDocument& node = *nodes.at(id);
const std::string indent(static_cast<std::size_t>(depth), ' ');
const char* tag = "div";
if (node.Type == MetaCoreUiNodeType::Button) tag = "button";
else if (node.Type == MetaCoreUiNodeType::Input) tag = "input";
else if (node.Type == MetaCoreUiNodeType::Image) tag = "img";
rml << indent << '<' << tag << " id=\"" << Escape(node.Id) << "\" class=\"mcui mcui-" << NodeClass(node.Type);
if (!node.Style.ThemeToken.empty()) rml << " theme-" << Escape(node.Style.ThemeToken);
rml << "\" data-metacore-kind=\"" << NodeClass(node.Type) << "\"";
if (!node.Action.empty()) rml << " data-metacore-action=\"" << Escape(node.Action) << "\"";
if (node.NavigationOrder >= 0) rml << " tabindex=\"" << node.NavigationOrder << "\"";
if (!node.LocalizationKey.empty()) rml << " data-metacore-loc=\"" << Escape(node.LocalizationKey) << "\"";
if (!node.ShowAnimation.empty()) rml << " data-metacore-show-animation=\"" << Escape(node.ShowAnimation) << "\"";
if (!node.HideAnimation.empty()) rml << " data-metacore-hide-animation=\"" << Escape(node.HideAnimation) << "\"";
for (const auto& binding : node.Bindings) {
rml << " data-metacore-bind-" << BindingTargetName(binding.Target) << "=\"" << Escape(binding.SourcePath) << "\""
<< " data-metacore-bind-mode-" << BindingTargetName(binding.Target) << "=\""
<< (binding.Mode == MetaCoreUiBindingMode::TwoWay ? "two-way" : "one-way") << "\"";
if (!binding.Format.empty()) rml << " data-metacore-format-" << BindingTargetName(binding.Target) << "=\"" << Escape(binding.Format) << "\"";
if (!binding.Fallback.empty()) rml << " data-metacore-fallback-" << BindingTargetName(binding.Target) << "=\"" << Escape(binding.Fallback) << "\"";
}
if (node.Bindings.empty() && !node.DataBinding.empty()) {
rml << " data-metacore-bind-text=\"" << Escape(node.DataBinding) << "\" data-metacore-format-text=\"" << Escape(node.DataFormat) << "\"";
}
if (!node.Visible) rml << " hidden=\"true\"";
if (!node.Interactable && (node.Type == MetaCoreUiNodeType::Button || node.Type == MetaCoreUiNodeType::Input)) rml << " disabled=\"true\"";
if (node.Type == MetaCoreUiNodeType::Input) {
rml << " value=\"" << Escape(node.Text) << "\" placeholder=\"" << Escape(node.Placeholder) << "\"";
if (node.ReadOnly) rml << " readonly=\"true\"";
if (node.MaxLength > 0) rml << " maxlength=\"" << node.MaxLength << "\"";
rml << " type=\"" << (node.InputValueType == MetaCoreUiInputValueType::Text ? "text" : "number") << "\"/>\n";
return;
}
if (node.Type == MetaCoreUiNodeType::Image && node.Style.ImageAssetGuid.IsValid())
rml << " src=\"Assets/" << node.Style.ImageAssetGuid.ToString() << ".png\"";
if (node.Type == MetaCoreUiNodeType::List) rml << " role=\"listbox\" data-metacore-selected-index=\"" << node.SelectedIndex << "\"";
rml << '>';
if (node.Type == MetaCoreUiNodeType::Text || node.Type == MetaCoreUiNodeType::Button) rml << Escape(node.Text);
if (!node.Children.empty()) rml << '\n';
for (const std::string& child : node.Children) emit(child, depth + 2);
if (!node.Children.empty()) rml << indent;
rml << "</" << tag << ">\n";
};
for (const std::string& root : document.RootNodeIds) emit(root, 2);
rml << "</body></rml>\n";
std::ostringstream css;
css << "body { margin:0; width:100%; height:100%; background-color:" << Color(document.BackgroundColor, document.BackgroundAlpha) << "; }\n"
<< ".mcui { position:absolute; box-sizing:border-box; }\n"
<< ".mcui-button:focus, .mcui-input:focus, .mcui-list:focus { outline:2px #4da3ff; }\n"
<< ".mcui-button:disabled, .mcui-input:disabled { opacity:0.45; }\n"
<< ".mcui-scroll { overflow:auto; }\n.mcui-list { overflow:auto; }\n";
for (const auto& node : document.Nodes) {
const auto& rt = node.RectTransform;
const float leftOffset = rt.Position.x - rt.Pivot.x * rt.Size.x;
const float topOffset = rt.Position.y - rt.Pivot.y * rt.Size.y;
const float anchorWidth = (rt.AnchorMax.x - rt.AnchorMin.x) * 100.0F;
const float anchorHeight = (rt.AnchorMax.y - rt.AnchorMin.y) * 100.0F;
css << '#' << node.Id << " { left:calc(" << rt.AnchorMin.x * 100.0F << "% + " << leftOffset
<< "px); top:calc(" << rt.AnchorMin.y * 100.0F << "% + " << topOffset << "px); width:calc("
<< anchorWidth << "% + " << rt.Size.x << "px); height:calc(" << anchorHeight << "% + " << rt.Size.y
<< "px); min-width:" << rt.MinSize.x << "px; min-height:" << rt.MinSize.y << "px; max-width:"
<< rt.MaxSize.x << "px; max-height:" << rt.MaxSize.y << "px; color:" << Color(node.Style.TextColor)
<< "; background-color:" << Color(node.Style.BackgroundColor, node.Style.Opacity) << "; font-size:"
<< node.Style.FontSize << "px; padding:" << node.Style.Padding.y << "px " << node.Style.Padding.x
<< "px; border-radius:" << node.Style.BorderRadius << "px; overflow:"
<< (rt.ClipChildren ? "hidden" : (node.Type == MetaCoreUiNodeType::Scroll || node.Type == MetaCoreUiNodeType::List ? "auto" : "visible"))
<< "; }\n";
}
result.Rml = rml.str();
result.Rcss = css.str();
result.Succeeded = true;
return result;
}
} // namespace MetaCore

View File

@ -3,11 +3,14 @@
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h"
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h"
#include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h"
#include "MetaCoreScene/MetaCoreSceneDocument.h"
#include <filesystem>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
namespace MetaCore {
@ -15,14 +18,87 @@ namespace MetaCore {
class MetaCoreWindow;
class MetaCoreEditorViewportRenderer;
inline constexpr std::uint32_t METACORE_UI_SCHEMA_VERSION = 2U;
inline constexpr std::uint32_t METACORE_UI_GENERATOR_VERSION = 1U;
enum class MetaCoreRuntimeUiEventType : std::uint32_t {
PointerEnter = 0,
PointerLeave,
Click,
Submit,
ValueChanged,
SelectionChanged,
Scroll,
Focus,
Blur,
WriteRequest
};
enum class MetaCoreRuntimeUiInputSource : std::uint32_t {
Programmatic = 0,
Mouse,
Keyboard,
WorldPointer
};
enum class MetaCoreRuntimeUiValueType : std::uint32_t {
Bool = 0,
Int64,
Double,
String,
Vec3,
AssetRef,
GameObjectRef
};
struct MetaCoreRuntimeUiValue {
MetaCoreRuntimeUiValueType Type = MetaCoreRuntimeUiValueType::String;
bool BoolValue = false;
std::int64_t Int64Value = 0;
double DoubleValue = 0.0;
std::string StringValue{};
glm::vec3 Vec3Value{0.0F};
MetaCoreAssetGuid AssetValue{};
std::uint64_t ObjectValue = 0;
};
using MetaCoreRuntimeUiRow = std::unordered_map<std::string, MetaCoreRuntimeUiValue>;
struct MetaCoreRuntimeUiEvent {
std::uint64_t InstanceId = 0;
MetaCoreAssetGuid DocumentAssetGuid{};
std::uint64_t SceneObjectId = 0;
std::string DocumentId{};
std::string ElementId{};
std::string Action{};
std::string Type{};
std::string Value{};
std::string OldValue{};
MetaCoreRuntimeUiEventType EventType = MetaCoreRuntimeUiEventType::Click;
MetaCoreRuntimeUiInputSource InputSource = MetaCoreRuntimeUiInputSource::Programmatic;
bool Consumed = false;
};
struct MetaCoreUiDiagnostic {
std::string Document{};
std::string NodeId{};
std::string BindingPath{};
std::string Message{};
bool IsError = true;
};
struct MetaCoreCompiledUiDocument {
std::string Rml{};
std::string Rcss{};
std::vector<MetaCoreAssetGuid> Dependencies{};
std::vector<MetaCoreUiDiagnostic> Diagnostics{};
bool Succeeded = false;
};
[[nodiscard]] std::vector<MetaCoreUiDiagnostic> MetaCoreValidateUiDocument(const MetaCoreUiDocument& document);
[[nodiscard]] MetaCoreCompiledUiDocument MetaCoreCompileUiDocument(const MetaCoreUiDocument& document);
[[nodiscard]] MetaCoreUiDocument MetaCoreMigrateUiDocument(MetaCoreUiDocument document);
class MetaCoreRuntimeUiSystem {
public:
using EventCallback = std::function<void(const MetaCoreRuntimeUiEvent&)>;
@ -43,13 +119,24 @@ public:
void Update(float deltaSeconds);
[[nodiscard]] MetaCoreInputConsumption ProcessInput();
void ApplyDataUpdates(const std::vector<MetaCoreRuntimeDataUpdate>& updates);
[[nodiscard]] bool SetValue(const std::string& sourcePath, const MetaCoreRuntimeUiValue& value);
[[nodiscard]] std::optional<MetaCoreRuntimeUiValue> GetValue(const std::string& sourcePath) const;
[[nodiscard]] bool SetListRows(const std::string& sourcePath, std::vector<MetaCoreRuntimeUiRow> rows);
[[nodiscard]] bool RequestFocus(std::uint64_t instanceId, const std::string& elementId);
[[nodiscard]] bool CreateInstance(const MetaCoreRuntimeUiDocumentEntry& entry);
[[nodiscard]] bool DestroyInstance(std::uint64_t instanceId);
[[nodiscard]] bool SetDocumentVisible(const std::string& documentId, bool visible);
void SetLocale(std::string locale);
void SetTheme(std::string theme);
[[nodiscard]] const std::string& GetLocale() const;
[[nodiscard]] const std::string& GetTheme() const;
[[nodiscard]] std::vector<MetaCoreRuntimeUiEvent> ConsumeEvents();
void SetEventCallback(EventCallback callback);
[[nodiscard]] bool IsInitialized() const;
[[nodiscard]] std::size_t GetLoadedDocumentCount() const;
[[nodiscard]] const std::vector<std::string>& GetErrors() const;
[[nodiscard]] std::vector<MetaCoreUiDiagnostic> GetDiagnostics() const;
private:
class Impl;

View File

@ -860,6 +860,9 @@ static json RectTransformToJson(const MetaCoreUiRectTransformDocument& trans) {
j["Pivot"] = Vec3ToJson(trans.Pivot);
j["Position"] = Vec3ToJson(trans.Position);
j["Size"] = Vec3ToJson(trans.Size);
j["MinSize"] = Vec3ToJson(trans.MinSize);
j["MaxSize"] = Vec3ToJson(trans.MaxSize);
j["ClipChildren"] = trans.ClipChildren;
return j;
}
@ -870,6 +873,9 @@ static MetaCoreUiRectTransformDocument JsonToRectTransform(const json& j) {
if (j.contains("Pivot")) trans.Pivot = JsonToVec3(j["Pivot"]);
if (j.contains("Position")) trans.Position = JsonToVec3(j["Position"]);
if (j.contains("Size")) trans.Size = JsonToVec3(j["Size"]);
if (j.contains("MinSize")) trans.MinSize = JsonToVec3(j["MinSize"]);
if (j.contains("MaxSize")) trans.MaxSize = JsonToVec3(j["MaxSize"]);
if (j.contains("ClipChildren")) trans.ClipChildren = j["ClipChildren"].get<bool>();
return trans;
}
@ -884,6 +890,9 @@ static json UiStyleToJson(const MetaCoreUiStyleDocument& style) {
j["VerticalAlignment"] = static_cast<int>(style.VerticalAlignment);
j["ImageAssetGuid"] = GuidToString(style.ImageAssetGuid);
j["PreserveAspect"] = style.PreserveAspect;
j["Opacity"] = style.Opacity;
j["BorderRadius"] = style.BorderRadius;
j["ThemeToken"] = style.ThemeToken;
return j;
}
@ -898,9 +907,34 @@ static MetaCoreUiStyleDocument JsonToUiStyle(const json& j) {
if (j.contains("VerticalAlignment")) style.VerticalAlignment = static_cast<MetaCoreUiVerticalAlignment>(j["VerticalAlignment"].get<int>());
if (j.contains("ImageAssetGuid")) style.ImageAssetGuid = StringToGuid(j["ImageAssetGuid"].get<std::string>());
if (j.contains("PreserveAspect")) style.PreserveAspect = j["PreserveAspect"].get<bool>();
if (j.contains("Opacity")) style.Opacity = j["Opacity"].get<float>();
if (j.contains("BorderRadius")) style.BorderRadius = j["BorderRadius"].get<float>();
if (j.contains("ThemeToken")) style.ThemeToken = j["ThemeToken"].get<std::string>();
return style;
}
static json UiBindingToJson(const MetaCoreUiBindingDocument& binding) {
return json{
{"SourcePath", binding.SourcePath},
{"Target", static_cast<int>(binding.Target)},
{"Mode", static_cast<int>(binding.Mode)},
{"Format", binding.Format},
{"Converter", binding.Converter},
{"Fallback", binding.Fallback}
};
}
static MetaCoreUiBindingDocument JsonToUiBinding(const json& j) {
MetaCoreUiBindingDocument binding;
if (j.contains("SourcePath")) binding.SourcePath = j["SourcePath"].get<std::string>();
if (j.contains("Target")) binding.Target = static_cast<MetaCoreUiBindingTarget>(j["Target"].get<int>());
if (j.contains("Mode")) binding.Mode = static_cast<MetaCoreUiBindingMode>(j["Mode"].get<int>());
if (j.contains("Format")) binding.Format = j["Format"].get<std::string>();
if (j.contains("Converter")) binding.Converter = j["Converter"].get<std::string>();
if (j.contains("Fallback")) binding.Fallback = j["Fallback"].get<std::string>();
return binding;
}
static json UiNodeToJson(const MetaCoreUiNodeDocument& node) {
json j = json::object();
j["Id"] = node.Id;
@ -916,6 +950,18 @@ static json UiNodeToJson(const MetaCoreUiNodeDocument& node) {
j["Action"] = node.Action;
j["DataBinding"] = node.DataBinding;
j["DataFormat"] = node.DataFormat;
j["Bindings"] = json::array();
for (const auto& binding : node.Bindings) j["Bindings"].push_back(UiBindingToJson(binding));
j["NavigationOrder"] = node.NavigationOrder;
j["Placeholder"] = node.Placeholder;
j["InputValueType"] = static_cast<int>(node.InputValueType);
j["ReadOnly"] = node.ReadOnly;
j["MaxLength"] = node.MaxLength;
j["SelectedIndex"] = node.SelectedIndex;
j["ItemTemplateNodeId"] = node.ItemTemplateNodeId;
j["LocalizationKey"] = node.LocalizationKey;
j["ShowAnimation"] = node.ShowAnimation;
j["HideAnimation"] = node.HideAnimation;
return j;
}
@ -934,6 +980,18 @@ static MetaCoreUiNodeDocument JsonToUiNode(const json& j) {
if (j.contains("Action")) node.Action = j["Action"].get<std::string>();
if (j.contains("DataBinding")) node.DataBinding = j["DataBinding"].get<std::string>();
if (j.contains("DataFormat")) node.DataFormat = j["DataFormat"].get<std::string>();
if (j.contains("Bindings") && j["Bindings"].is_array())
for (const auto& binding : j["Bindings"]) node.Bindings.push_back(JsonToUiBinding(binding));
if (j.contains("NavigationOrder")) node.NavigationOrder = j["NavigationOrder"].get<std::int32_t>();
if (j.contains("Placeholder")) node.Placeholder = j["Placeholder"].get<std::string>();
if (j.contains("InputValueType")) node.InputValueType = static_cast<MetaCoreUiInputValueType>(j["InputValueType"].get<int>());
if (j.contains("ReadOnly")) node.ReadOnly = j["ReadOnly"].get<bool>();
if (j.contains("MaxLength")) node.MaxLength = j["MaxLength"].get<std::int32_t>();
if (j.contains("SelectedIndex")) node.SelectedIndex = j["SelectedIndex"].get<std::int32_t>();
if (j.contains("ItemTemplateNodeId")) node.ItemTemplateNodeId = j["ItemTemplateNodeId"].get<std::string>();
if (j.contains("LocalizationKey")) node.LocalizationKey = j["LocalizationKey"].get<std::string>();
if (j.contains("ShowAnimation")) node.ShowAnimation = j["ShowAnimation"].get<std::string>();
if (j.contains("HideAnimation")) node.HideAnimation = j["HideAnimation"].get<std::string>();
return node;
}
@ -945,9 +1003,22 @@ bool MetaCoreSceneSerializer::SaveUiToJson(
(void)registry;
try {
json uiJson;
uiJson["SchemaVersion"] = uiDocument.SchemaVersion;
uiJson["GeneratorVersion"] = uiDocument.GeneratorVersion;
uiJson["Name"] = uiDocument.Name;
uiJson["ReferenceWidth"] = uiDocument.ReferenceWidth;
uiJson["ReferenceHeight"] = uiDocument.ReferenceHeight;
uiJson["ScaleMode"] = static_cast<int>(uiDocument.ScaleMode);
uiJson["MatchWidthOrHeight"] = uiDocument.MatchWidthOrHeight;
uiJson["ReferenceDpi"] = uiDocument.ReferenceDpi;
uiJson["MinScale"] = uiDocument.MinScale;
uiJson["MaxScale"] = uiDocument.MaxScale;
uiJson["UseSafeArea"] = uiDocument.UseSafeArea;
uiJson["ThemeAssetGuid"] = GuidToString(uiDocument.ThemeAssetGuid);
uiJson["FontFamilyAssetGuid"] = GuidToString(uiDocument.FontFamilyAssetGuid);
uiJson["LocalizationAssetGuid"] = GuidToString(uiDocument.LocalizationAssetGuid);
uiJson["DefaultLocale"] = uiDocument.DefaultLocale;
uiJson["FallbackLocale"] = uiDocument.FallbackLocale;
uiJson["BackgroundColor"] = Vec3ToJson(uiDocument.BackgroundColor);
uiJson["BackgroundAlpha"] = uiDocument.BackgroundAlpha;
uiJson["RootNodeIds"] = uiDocument.RootNodeIds;
@ -983,9 +1054,22 @@ std::optional<MetaCoreUiDocument> MetaCoreSceneSerializer::LoadUiFromJson(
file >> uiJson;
MetaCoreUiDocument doc;
if (uiJson.contains("SchemaVersion")) doc.SchemaVersion = uiJson["SchemaVersion"].get<std::uint32_t>(); else doc.SchemaVersion = 1;
if (uiJson.contains("GeneratorVersion")) doc.GeneratorVersion = uiJson["GeneratorVersion"].get<std::uint32_t>();
if (uiJson.contains("Name")) doc.Name = uiJson["Name"].get<std::string>();
if (uiJson.contains("ReferenceWidth")) doc.ReferenceWidth = uiJson["ReferenceWidth"].get<std::int32_t>();
if (uiJson.contains("ReferenceHeight")) doc.ReferenceHeight = uiJson["ReferenceHeight"].get<std::int32_t>();
if (uiJson.contains("ScaleMode")) doc.ScaleMode = static_cast<MetaCoreUiCanvasScaleMode>(uiJson["ScaleMode"].get<int>());
if (uiJson.contains("MatchWidthOrHeight")) doc.MatchWidthOrHeight = uiJson["MatchWidthOrHeight"].get<float>();
if (uiJson.contains("ReferenceDpi")) doc.ReferenceDpi = uiJson["ReferenceDpi"].get<float>();
if (uiJson.contains("MinScale")) doc.MinScale = uiJson["MinScale"].get<float>();
if (uiJson.contains("MaxScale")) doc.MaxScale = uiJson["MaxScale"].get<float>();
if (uiJson.contains("UseSafeArea")) doc.UseSafeArea = uiJson["UseSafeArea"].get<bool>();
if (uiJson.contains("ThemeAssetGuid")) doc.ThemeAssetGuid = StringToGuid(uiJson["ThemeAssetGuid"].get<std::string>());
if (uiJson.contains("FontFamilyAssetGuid")) doc.FontFamilyAssetGuid = StringToGuid(uiJson["FontFamilyAssetGuid"].get<std::string>());
if (uiJson.contains("LocalizationAssetGuid")) doc.LocalizationAssetGuid = StringToGuid(uiJson["LocalizationAssetGuid"].get<std::string>());
if (uiJson.contains("DefaultLocale")) doc.DefaultLocale = uiJson["DefaultLocale"].get<std::string>();
if (uiJson.contains("FallbackLocale")) doc.FallbackLocale = uiJson["FallbackLocale"].get<std::string>();
if (uiJson.contains("BackgroundColor")) doc.BackgroundColor = JsonToVec3(uiJson["BackgroundColor"]);
if (uiJson.contains("BackgroundAlpha")) doc.BackgroundAlpha = uiJson["BackgroundAlpha"].get<float>();
if (uiJson.contains("RootNodeIds")) doc.RootNodeIds = uiJson["RootNodeIds"].get<std::vector<std::string>>();

View File

@ -98,7 +98,40 @@ enum class MetaCoreUiNodeType {
Panel = 0,
Text,
Image,
Button
Button,
Input,
List,
Scroll
};
MC_ENUM()
enum class MetaCoreUiCanvasScaleMode {
ConstantPixelSize = 0,
ScaleWithScreenSize,
ConstantPhysicalSize
};
MC_ENUM()
enum class MetaCoreUiBindingMode {
OneWay = 0,
TwoWay
};
MC_ENUM()
enum class MetaCoreUiBindingTarget {
Text = 0,
Value,
Visible,
Enabled,
Items,
SelectedIndex
};
MC_ENUM()
enum class MetaCoreUiInputValueType {
Text = 0,
Integer,
Number
};
MC_ENUM()
@ -135,6 +168,15 @@ struct MetaCoreUiRectTransformDocument {
MC_PROPERTY()
glm::vec3 Size{100.0F, 100.0F, 0.0F};
MC_PROPERTY()
glm::vec3 MinSize{0.0F, 0.0F, 0.0F};
MC_PROPERTY()
glm::vec3 MaxSize{100000.0F, 100000.0F, 0.0F};
MC_PROPERTY()
bool ClipChildren = false;
};
MC_STRUCT()
@ -167,6 +209,38 @@ struct MetaCoreUiStyleDocument {
MC_PROPERTY()
bool PreserveAspect = false;
MC_PROPERTY()
float Opacity = 1.0F;
MC_PROPERTY()
float BorderRadius = 0.0F;
MC_PROPERTY()
std::string ThemeToken{};
};
MC_STRUCT()
struct MetaCoreUiBindingDocument {
MC_GENERATED_BODY()
MC_PROPERTY()
std::string SourcePath{};
MC_PROPERTY()
MetaCoreUiBindingTarget Target = MetaCoreUiBindingTarget::Text;
MC_PROPERTY()
MetaCoreUiBindingMode Mode = MetaCoreUiBindingMode::OneWay;
MC_PROPERTY()
std::string Format{};
MC_PROPERTY()
std::string Converter{};
MC_PROPERTY()
std::string Fallback{};
};
MC_STRUCT()
@ -211,12 +285,51 @@ struct MetaCoreUiNodeDocument {
MC_PROPERTY()
std::string DataFormat{};
MC_PROPERTY()
std::vector<MetaCoreUiBindingDocument> Bindings{};
MC_PROPERTY()
std::int32_t NavigationOrder = -1;
MC_PROPERTY()
std::string Placeholder{};
MC_PROPERTY()
MetaCoreUiInputValueType InputValueType = MetaCoreUiInputValueType::Text;
MC_PROPERTY()
bool ReadOnly = false;
MC_PROPERTY()
std::int32_t MaxLength = 0;
MC_PROPERTY()
std::int32_t SelectedIndex = -1;
MC_PROPERTY()
std::string ItemTemplateNodeId{};
MC_PROPERTY()
std::string LocalizationKey{};
MC_PROPERTY()
std::string ShowAnimation{};
MC_PROPERTY()
std::string HideAnimation{};
};
MC_STRUCT()
struct MetaCoreUiDocument {
MC_GENERATED_BODY()
MC_PROPERTY()
std::uint32_t SchemaVersion = 2;
MC_PROPERTY()
std::uint32_t GeneratorVersion = 1;
MC_PROPERTY()
std::string Name{};
@ -226,6 +339,39 @@ struct MetaCoreUiDocument {
MC_PROPERTY()
std::int32_t ReferenceHeight = 1080;
MC_PROPERTY()
MetaCoreUiCanvasScaleMode ScaleMode = MetaCoreUiCanvasScaleMode::ScaleWithScreenSize;
MC_PROPERTY()
float MatchWidthOrHeight = 0.5F;
MC_PROPERTY()
float ReferenceDpi = 96.0F;
MC_PROPERTY()
float MinScale = 0.5F;
MC_PROPERTY()
float MaxScale = 4.0F;
MC_PROPERTY()
bool UseSafeArea = true;
MC_PROPERTY()
MetaCoreAssetGuid ThemeAssetGuid{};
MC_PROPERTY()
MetaCoreAssetGuid FontFamilyAssetGuid{};
MC_PROPERTY()
MetaCoreAssetGuid LocalizationAssetGuid{};
MC_PROPERTY()
std::string DefaultLocale{"en-US"};
MC_PROPERTY()
std::string FallbackLocale{"en-US"};
MC_PROPERTY()
glm::vec3 BackgroundColor{0.0F, 0.0F, 0.0F};

View File

@ -2,6 +2,7 @@
#include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h"
#include "MetaCorePhysics/MetaCorePhysics.h"
#include "MetaCoreAnimation/MetaCoreAnimation.h"
#include "MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h"
#include "MetaCoreFoundation/MetaCoreHash.h"
#include <nlohmann/json.hpp>
@ -453,10 +454,11 @@ struct MetaCoreScriptRuntime::Impl {
MetaCoreRuntimeInput* Input = nullptr;
MetaCorePhysicsWorld* Physics = nullptr;
MetaCoreAnimationRuntime* Animation = nullptr;
MetaCoreRuntimeUiSystem* RuntimeUi = nullptr;
Impl(MetaCoreScriptRuntime& owner, MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger,
MetaCoreRuntimeInput* input, MetaCorePhysicsWorld* physics, MetaCoreAnimationRuntime* animation)
: Owner(owner), Scene(scene), Registry(registry), Logger(std::move(logger)), Input(input), Physics(physics), Animation(animation) {}
MetaCoreRuntimeInput* input, MetaCorePhysicsWorld* physics, MetaCoreAnimationRuntime* animation, MetaCoreRuntimeUiSystem* runtimeUi)
: Owner(owner), Scene(scene), Registry(registry), Logger(std::move(logger)), Input(input), Physics(physics), Animation(animation), RuntimeUi(runtimeUi) {}
void Log(std::uint32_t level, std::string_view category, std::string_view text) const { if (Logger) Logger(level, category, text); }
Entry* FindEntry(MetaCoreId object, std::uint64_t instance) {
@ -562,8 +564,8 @@ struct MetaCoreScriptRuntime::Impl {
};
MetaCoreScriptRuntime::MetaCoreScriptRuntime(MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger,
MetaCoreRuntimeInput* input, MetaCorePhysicsWorld* physics, MetaCoreAnimationRuntime* animation)
: Impl_(std::make_unique<Impl>(*this, scene, registry, std::move(logger), input, physics, animation)) {}
MetaCoreRuntimeInput* input, MetaCorePhysicsWorld* physics, MetaCoreAnimationRuntime* animation, MetaCoreRuntimeUiSystem* runtimeUi)
: Impl_(std::make_unique<Impl>(*this, scene, registry, std::move(logger), input, physics, animation, runtimeUi)) {}
MetaCoreScriptRuntime::~MetaCoreScriptRuntime() { Stop(); }
void MetaCoreScriptRuntime::Start() { if (Impl_->Running) return; Impl_->Running = true; ++WorldGeneration_; for (auto o : Impl_->Scene.GetGameObjects()) if (o.HasComponent<MetaCoreScriptComponent>()) for (auto& i : o.GetComponent<MetaCoreScriptComponent>().Instances) if (i.Enabled) Impl_->Ensure(o, i); }
void MetaCoreScriptRuntime::FixedUpdate(double delta) { if (!Impl_->Running) return; Time_.FixedDeltaSeconds = std::max(0.0, delta); Impl_->Dispatch(Impl::Phase::Fixed, Time_.FixedDeltaSeconds); }
@ -588,6 +590,7 @@ void MetaCoreScriptRuntime::PublishToObject(MetaCoreId object, std::string topic
MetaCoreRuntimeInput* MetaCoreScriptRuntime::GetRuntimeInput() const { return Impl_->Input; }
MetaCorePhysicsWorld* MetaCoreScriptRuntime::GetPhysicsWorld() const { return Impl_->Physics; }
MetaCoreAnimationRuntime* MetaCoreScriptRuntime::GetAnimationRuntime() const { return Impl_->Animation; }
MetaCoreRuntimeUiSystem* MetaCoreScriptRuntime::GetRuntimeUi() const { return Impl_->RuntimeUi; }
std::uint64_t MetaCoreScriptRuntime::ScheduleTimer(MetaCoreId object, std::uint64_t instance, double delay, double interval, bool unscaled, std::function<void()> callback) { const auto id = Impl_->NextTimer++; Impl_->Timers.push_back({id, object, instance, std::max(0.0, delay), std::max(0.0, interval), unscaled, std::move(callback)}); return id; }
void MetaCoreScriptRuntime::CancelTimer(std::uint64_t id) { std::erase_if(Impl_->Timers, [id](const auto& t) { return t.Id == id; }); }
bool MetaCoreScriptRuntime::IsHandleValid(MetaCoreScriptObjectHandleV1 handle) const { return handle.WorldGeneration == WorldGeneration_ && handle.ObjectId != 0 && static_cast<bool>(Impl_->Scene.FindGameObject(handle.ObjectId)); }
@ -686,7 +689,41 @@ bool HTimelineStop(void*c,MetaCoreScriptObjectHandleV1 h) noexcept{try{auto*a=HA
bool HTimelineSeek(void*c,MetaCoreScriptObjectHandleV1 h,double s) noexcept{try{auto*a=HAnimation(c,h);return a&&a->TimelineSeek(h.ObjectId,static_cast<float>(s));}catch(...){return false;}}
bool HTimelineSpeed(void*c,MetaCoreScriptObjectHandleV1 h,double s) noexcept{try{auto*a=HAnimation(c,h);return a&&a->TimelineSetSpeed(h.ObjectId,static_cast<float>(s));}catch(...){return false;}}
bool HAnimationConsumeRootMotion(void*c,MetaCoreScriptObjectHandleV1 h,MetaCoreScriptRootMotionV1*out) noexcept{try{auto*a=HAnimation(c,h);if(!a||!out)return false;const auto delta=a->ConsumeRootMotion(h.ObjectId);if(!delta)return false;out->Translation[0]=delta->Translation.x;out->Translation[1]=delta->Translation.y;out->Translation[2]=delta->Translation.z;out->Rotation[0]=delta->Rotation.w;out->Rotation[1]=delta->Rotation.x;out->Rotation[2]=delta->Rotation.y;out->Rotation[3]=delta->Rotation.z;return true;}catch(...){return false;}}
const MetaCoreScriptHostApiV1& HostApi() { static const MetaCoreScriptHostApiV1 api{METACORE_SCRIPT_ABI_MAJOR, METACORE_SCRIPT_ABI_MINOR, METACORE_SCRIPT_HOST_CAPABILITIES, HValid, HHas, HGet, HSet, HGetField, HSetField, HLog, HPublish, HSubscribe, HUnsubscribe, HSchedule, HCancel, HInputAction, HInputAxis, HInputContext, HInputRebind, HInputReset, HPhysicsRay, HPhysicsShape, HPhysicsOverlap, HPhysicsForce, HPhysicsImpulse, HPhysicsGetVelocity, HPhysicsSetVelocity, HPhysicsWake, HPhysicsMove, HPhysicsJump, HPhysicsGrounded, HPhysicsConstraintEnabled, HPhysicsConstraintMotor, HAnimationBool, HAnimationInt, HAnimationFloat, HAnimationTrigger, HAnimationPlay, HAnimationCrossFade, HAnimationState, HTimelinePlay, HTimelinePause, HTimelineStop, HTimelineSeek, HTimelineSpeed, HAnimationConsumeRootMotion}; return api; }
MetaCoreRuntimeUiValue HUiValue(const MetaCoreScriptValueV1& source) {
MetaCoreRuntimeUiValue value;
switch (source.Type) {
case MetaCoreScriptAbiValue_Bool: value.Type=MetaCoreRuntimeUiValueType::Bool;value.BoolValue=source.IntValue!=0;break;
case MetaCoreScriptAbiValue_Int: value.Type=MetaCoreRuntimeUiValueType::Int64;value.Int64Value=source.IntValue;break;
case MetaCoreScriptAbiValue_Float: value.Type=MetaCoreRuntimeUiValueType::Double;value.DoubleValue=source.NumberValue;break;
case MetaCoreScriptAbiValue_Vec3: value.Type=MetaCoreRuntimeUiValueType::Vec3;value.Vec3Value={source.Vec3[0],source.Vec3[1],source.Vec3[2]};break;
case MetaCoreScriptAbiValue_AssetRef: value.Type=MetaCoreRuntimeUiValueType::AssetRef;if(auto guid=MetaCoreAssetGuid::Parse(source.Text))value.AssetValue=*guid;break;
case MetaCoreScriptAbiValue_GameObjectRef: value.Type=MetaCoreRuntimeUiValueType::GameObjectRef;value.ObjectValue=source.ObjectValue.ObjectId;break;
case MetaCoreScriptAbiValue_String:
default: value.Type=MetaCoreRuntimeUiValueType::String;value.StringValue=source.Text;break;
}
return value;
}
void HScriptValue(const MetaCoreRuntimeUiValue& source, MetaCoreScriptValueV1& value) {
value={};
switch(source.Type) {
case MetaCoreRuntimeUiValueType::Bool:value.Type=MetaCoreScriptAbiValue_Bool;value.IntValue=source.BoolValue?1:0;break;
case MetaCoreRuntimeUiValueType::Int64:value.Type=MetaCoreScriptAbiValue_Int;value.IntValue=source.Int64Value;break;
case MetaCoreRuntimeUiValueType::Double:value.Type=MetaCoreScriptAbiValue_Float;value.NumberValue=source.DoubleValue;break;
case MetaCoreRuntimeUiValueType::Vec3:value.Type=MetaCoreScriptAbiValue_Vec3;value.Vec3[0]=source.Vec3Value.x;value.Vec3[1]=source.Vec3Value.y;value.Vec3[2]=source.Vec3Value.z;break;
case MetaCoreRuntimeUiValueType::AssetRef:value.Type=MetaCoreScriptAbiValue_AssetRef;std::snprintf(value.Text,sizeof(value.Text),"%s",source.AssetValue.ToString().c_str());break;
case MetaCoreRuntimeUiValueType::GameObjectRef:value.Type=MetaCoreScriptAbiValue_GameObjectRef;value.ObjectValue.ObjectId=source.ObjectValue;break;
case MetaCoreRuntimeUiValueType::String:value.Type=MetaCoreScriptAbiValue_String;std::snprintf(value.Text,sizeof(value.Text),"%s",source.StringValue.c_str());break;
}
}
MetaCoreRuntimeUiSystem* HUi(void*c){auto*r=static_cast<MetaCoreScriptRuntime*>(c);return r?r->GetRuntimeUi():nullptr;}
bool HUiVisible(void*c,const char*id,bool visible)noexcept{try{auto*ui=HUi(c);return ui&&id&&ui->SetDocumentVisible(id,visible);}catch(...){return false;}}
bool HUiSet(void*c,const char*path,const MetaCoreScriptValueV1*value)noexcept{try{auto*ui=HUi(c);return ui&&path&&value&&ui->SetValue(path,HUiValue(*value));}catch(...){return false;}}
bool HUiGet(void*c,const char*path,MetaCoreScriptValueV1*out)noexcept{try{auto*ui=HUi(c);if(!ui||!path||!out)return false;auto value=ui->GetValue(path);if(!value)return false;HScriptValue(*value,*out);return true;}catch(...){return false;}}
bool HUiRows(void*c,const char*path,const MetaCoreScriptUiRowV1*rows,std::size_t count)noexcept{try{auto*ui=HUi(c);if(!ui||!path||(!rows&&count))return false;std::vector<MetaCoreRuntimeUiRow> converted;converted.reserve(count);for(std::size_t i=0;i<count;++i){MetaCoreRuntimeUiRow row;for(std::size_t f=0;f<rows[i].FieldCount;++f)if(rows[i].Fields[f].Key)row[rows[i].Fields[f].Key]=HUiValue(rows[i].Fields[f].Value);converted.push_back(std::move(row));}return ui->SetListRows(path,std::move(converted));}catch(...){return false;}}
bool HUiFocus(void*c,std::uint64_t instance,const char*element)noexcept{try{auto*ui=HUi(c);return ui&&element&&ui->RequestFocus(instance,element);}catch(...){return false;}}
bool HUiLocale(void*c,const char*locale)noexcept{try{auto*ui=HUi(c);if(!ui||!locale)return false;ui->SetLocale(locale);return true;}catch(...){return false;}}
bool HUiTheme(void*c,const char*theme)noexcept{try{auto*ui=HUi(c);if(!ui||!theme)return false;ui->SetTheme(theme);return true;}catch(...){return false;}}
const MetaCoreScriptHostApiV1& HostApi() { static const MetaCoreScriptHostApiV1 api{METACORE_SCRIPT_ABI_MAJOR, METACORE_SCRIPT_ABI_MINOR, METACORE_SCRIPT_HOST_CAPABILITIES, HValid, HHas, HGet, HSet, HGetField, HSetField, HLog, HPublish, HSubscribe, HUnsubscribe, HSchedule, HCancel, HInputAction, HInputAxis, HInputContext, HInputRebind, HInputReset, HPhysicsRay, HPhysicsShape, HPhysicsOverlap, HPhysicsForce, HPhysicsImpulse, HPhysicsGetVelocity, HPhysicsSetVelocity, HPhysicsWake, HPhysicsMove, HPhysicsJump, HPhysicsGrounded, HPhysicsConstraintEnabled, HPhysicsConstraintMotor, HAnimationBool, HAnimationInt, HAnimationFloat, HAnimationTrigger, HAnimationPlay, HAnimationCrossFade, HAnimationState, HTimelinePlay, HTimelinePause, HTimelineStop, HTimelineSeek, HTimelineSpeed, HAnimationConsumeRootMotion, HUiVisible, HUiSet, HUiGet, HUiRows, HUiFocus, HUiLocale, HUiTheme}; return api; }
} // namespace
bool MetaCoreGenerateScriptProject(const std::filesystem::path& root, std::string_view projectName, const std::filesystem::path& sdkRoot, std::string* error) {

View File

@ -12,16 +12,17 @@
extern "C" {
inline constexpr std::uint32_t METACORE_SCRIPT_ABI_MAJOR = 1U;
inline constexpr std::uint32_t METACORE_SCRIPT_ABI_MINOR = 4U;
inline constexpr std::uint32_t METACORE_SCRIPT_ABI_MINOR = 5U;
inline constexpr std::uint64_t METACORE_SCRIPT_CAP_PROPERTIES = 1ULL << 0U;
inline constexpr std::uint64_t METACORE_SCRIPT_CAP_EVENTS = 1ULL << 1U;
inline constexpr std::uint64_t METACORE_SCRIPT_CAP_TIMERS = 1ULL << 2U;
inline constexpr std::uint64_t METACORE_SCRIPT_CAP_INPUT = 1ULL << 3U;
inline constexpr std::uint64_t METACORE_SCRIPT_CAP_PHYSICS = 1ULL << 4U;
inline constexpr std::uint64_t METACORE_SCRIPT_CAP_ANIMATION = 1ULL << 5U;
inline constexpr std::uint64_t METACORE_SCRIPT_CAP_UI = 1ULL << 6U;
inline constexpr std::uint64_t METACORE_SCRIPT_HOST_CAPABILITIES =
METACORE_SCRIPT_CAP_PROPERTIES | METACORE_SCRIPT_CAP_EVENTS | METACORE_SCRIPT_CAP_TIMERS | METACORE_SCRIPT_CAP_INPUT |
METACORE_SCRIPT_CAP_PHYSICS | METACORE_SCRIPT_CAP_ANIMATION;
METACORE_SCRIPT_CAP_PHYSICS | METACORE_SCRIPT_CAP_ANIMATION | METACORE_SCRIPT_CAP_UI;
enum MetaCoreScriptAbiValueType : std::uint32_t {
MetaCoreScriptAbiValue_None = 0,
@ -89,6 +90,16 @@ struct MetaCoreScriptRootMotionV1 {
double Rotation[4]; // w, x, y, z
};
struct MetaCoreScriptUiFieldV1 {
const char* Key;
MetaCoreScriptValueV1 Value;
};
struct MetaCoreScriptUiRowV1 {
std::size_t FieldCount;
const MetaCoreScriptUiFieldV1* Fields;
};
struct MetaCoreScriptExecutionContextV1;
using MetaCoreScriptTimerCallbackV1 = const char* (*)(void*, const MetaCoreScriptExecutionContextV1*) noexcept;
using MetaCoreScriptEventCallbackV1 = const char* (*)(void*, const MetaCoreScriptExecutionContextV1*, const char*, const MetaCoreScriptValueV1*, std::size_t) noexcept;
@ -140,6 +151,13 @@ struct MetaCoreScriptHostApiV1 {
bool (*TimelineSeek)(void*, MetaCoreScriptObjectHandleV1, double) noexcept;
bool (*TimelineSetSpeed)(void*, MetaCoreScriptObjectHandleV1, double) noexcept;
bool (*AnimationConsumeRootMotion)(void*, MetaCoreScriptObjectHandleV1, MetaCoreScriptRootMotionV1*) noexcept;
bool (*UiSetDocumentVisible)(void*, const char*, bool) noexcept;
bool (*UiSetValue)(void*, const char*, const MetaCoreScriptValueV1*) noexcept;
bool (*UiGetValue)(void*, const char*, MetaCoreScriptValueV1*) noexcept;
bool (*UiSetListRows)(void*, const char*, const MetaCoreScriptUiRowV1*, std::size_t) noexcept;
bool (*UiRequestFocus)(void*, std::uint64_t, const char*) noexcept;
bool (*UiSetLocale)(void*, const char*) noexcept;
bool (*UiSetTheme)(void*, const char*) noexcept;
};
struct MetaCoreScriptExecutionContextV1 {

View File

@ -13,6 +13,7 @@
namespace MetaCore {
class MetaCoreAnimationRuntime;
class MetaCoreRuntimeUiSystem;
enum class MetaCoreScriptFieldType {
Bool = 0,
@ -119,7 +120,8 @@ public:
class MetaCoreScriptRuntime {
public:
MetaCoreScriptRuntime(MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger = {},
MetaCoreRuntimeInput* input = nullptr, MetaCorePhysicsWorld* physics = nullptr, MetaCoreAnimationRuntime* animation = nullptr);
MetaCoreRuntimeInput* input = nullptr, MetaCorePhysicsWorld* physics = nullptr, MetaCoreAnimationRuntime* animation = nullptr,
MetaCoreRuntimeUiSystem* runtimeUi = nullptr);
~MetaCoreScriptRuntime();
void Start();
@ -140,6 +142,7 @@ public:
[[nodiscard]] MetaCoreRuntimeInput* GetRuntimeInput() const;
[[nodiscard]] MetaCorePhysicsWorld* GetPhysicsWorld() const;
[[nodiscard]] MetaCoreAnimationRuntime* GetAnimationRuntime() const;
[[nodiscard]] MetaCoreRuntimeUiSystem* GetRuntimeUi() const;
std::uint64_t ScheduleTimer(MetaCoreId objectId, std::uint64_t instanceId, double delay, double interval, bool unscaled, std::function<void()> callback);
void CancelTimer(std::uint64_t timerId);

View File

@ -334,14 +334,27 @@ R8.1R8.5 已落地的工程边界:
### 9. UI 系统
需要补齐:
当前状态:**核心资产、编译、运行时、Cook 和脚本接口已迁移到 v2但图形签收门禁尚未满足问题 9 保持打开。**
- [ ] 稳定的 Button、Image、Text、Input、List、Scroll 等控件库。
- [ ] UI 事件、焦点、键盘导航和输入消费规则。
- [ ] Anchor、缩放、DPI、宽高比和多分辨率适配。
- [ ] 主题、字体、图集、本地化和基础动效。
- [ ] UI 到 RuntimeData、脚本和场景对象的统一数据绑定。
- [ ] 编辑器预览与 Player 渲染的一致性测试。
2026-07-27 已实施:
- `MetaCoreUiDocument` 成为正式创作源,具有 Schema/Generator Version、稳定节点 ID、Canvas 参数及 Theme/Font/Localization GUID控件扩展为 Panel、Text、Image、Button、Input、List、Scroll旧四控件文档具有确定性默认迁移。
- 新增独立校验与确定性编译器,拒绝重复 ID、损坏层级、循环/不可达节点、非法绑定命名空间、错误双向目标和不兼容版本,并生成版本化 RML/RCSS 与 GUID 依赖集合。Editor 导出、Cook 和 Runtime 使用同一编译入口;旧直接 RML/清单编辑面板已隐藏。
- RectTransform 已覆盖 AnchorMin/AnchorMax、Pivot、Offset/Size、最小/最大尺寸和裁剪Canvas 数据已覆盖三种缩放模式、参考分辨率、宽高匹配、参考 DPI、缩放上下限和安全区声明。
- Runtime UI 清单升级为 v2 UI Asset GUID 实例清单,包含 Instance/Scene Object、RenderMode、Layer 和目标分辨率;旧 `DocumentId + RmlPath` 继续读取。Cook 从启动场景 UI 组件生成 v2 清单与 `Ui/Compiled/<guid>` 产物,并收集、验证和复制 UI 依赖。
- 运行时提供类型化 Bool/Int64/Double/String/Vec3/AssetRef/GameObjectRef 值、列表行、增量路径刷新、fallback、实例创建销毁、焦点、Locale、Theme 和诊断接口;输入优先于 3D 交互,事件涵盖 Pointer Enter/Leave、Click、Submit、Value/Selection Changed、Scroll、Focus/Blur 和不可写 Provider 的 WriteRequest。
- 脚本 ABI Minor 升至 5新增 `METACORE_SCRIPT_CAP_UI`提供文档显示、节点值、列表、焦点、Locale 和 Theme 接口UI 事件同时发布到全局 `MetaCore.UI` 和清单指定的场景对象。
- UI Designer 已补齐新控件、Canvas、Anchor/Pivot、导航、输入/列表/滚动和类型化绑定 Inspector并以正式编译产物作为导出/预览来源。
- 新增独立 `MetaCoreRuntimeUiTests`,覆盖七类节点、文档校验/迁移、确定性编译、Anchor、双向绑定及 JSON/二进制往返Smoke 覆盖启动场景 GUID 依赖扫描、v2 清单和编译产物。Linux Development 全量构建成功CTest **12/12** 通过。
尚未满足、因此不能正式关闭问题 9
- World Space 目前仍复用 Screen Space overlay 路径;独立离屏 RmlUi Context、Filament Render Target、`PixelsPerUnit` 尺寸映射、遮挡和射线 UV 输入尚未完成。
- Canvas 参数已进入格式和编译元数据,但运行时 DPI/物理尺寸、安全区、宽高比约束的逐实例求值尚未完成;方向键空间导航和焦点失效转移也缺少完整自动化覆盖。
- `UiTheme`、`UiFontFamily`、`UiLocalizationTable` 仍只有文档 GUID 引用和运行时切换接口Token 状态样式解析、字体 fallback/字符集图集、本地化表 fallback、图片图集及基础动效编译尚未形成完整资产管线。
- `runtime.*` 已连接 RuntimeData`script.*` 可由脚本发布;`scene.*` 的稳定对象句柄反射 Provider、严格类型转换/格式诊断及完整双向写请求测试仍需补齐。
- 旧 RML 目前是运行时兼容与 Cook 复制不是一次性结构化导入器Designer 的拖放重挂、资产级 Undo/Redo、未保存切换保护及完整分辨率/DPI/Locale/Theme/输入模拟尚未全部验收。
- 2,000 节点/500 绑定/100 行列表基准、Screen/World Space GPU 黄金图、4:3/16:9/21:9 与 DPI/中英文矩阵、SSIM ≥ 0.995、16.67 ms 帧预算和 Linux x86_64 图形参考机 Editor/Player 像素及交互签收尚未执行。
### 10. Scene 与 Prefab

View File

@ -0,0 +1,117 @@
#include "MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h"
#include "MetaCoreFoundation/MetaCoreGeneratedReflection.h"
#include "MetaCoreScene/MetaCoreSceneSerializer.h"
#include <cstdlib>
#include <filesystem>
#include <iostream>
#include <string>
namespace {
void Expect(bool value, const char* message) {
if (!value) {
std::cerr << "MetaCoreRuntimeUiTests: " << message << '\n';
std::exit(1);
}
}
MetaCore::MetaCoreUiDocument BuildDocument() {
using namespace MetaCore;
MetaCoreUiDocument document;
document.Name = "Controls";
document.ReferenceWidth = 1920;
document.ReferenceHeight = 1080;
document.ScaleMode = MetaCoreUiCanvasScaleMode::ScaleWithScreenSize;
document.MatchWidthOrHeight = 0.5F;
MetaCoreUiNodeDocument root;
root.Id = "Root";
root.Name = "Root";
root.Type = MetaCoreUiNodeType::Panel;
root.RectTransform.AnchorMin = {0.0F, 0.0F, 0.0F};
root.RectTransform.AnchorMax = {1.0F, 1.0F, 0.0F};
root.RectTransform.Size = {0.0F, 0.0F, 0.0F};
const std::vector<MetaCoreUiNodeType> types{
MetaCoreUiNodeType::Text, MetaCoreUiNodeType::Image, MetaCoreUiNodeType::Button,
MetaCoreUiNodeType::Input, MetaCoreUiNodeType::List, MetaCoreUiNodeType::Scroll
};
for (std::size_t index = 0; index < types.size(); ++index) {
MetaCoreUiNodeDocument node;
node.Id = "Node" + std::to_string(index);
node.Name = node.Id;
node.ParentId = root.Id;
node.Type = types[index];
node.Text = "Safe <text>";
node.Interactable = types[index] == MetaCoreUiNodeType::Button || types[index] == MetaCoreUiNodeType::Input;
node.NavigationOrder = static_cast<std::int32_t>(index);
node.RectTransform.AnchorMin = {0.25F, 0.25F, 0.0F};
node.RectTransform.AnchorMax = {0.75F, 0.25F, 0.0F};
node.RectTransform.Position = {10.0F, 20.0F + static_cast<float>(index) * 50.0F, 0.0F};
if (types[index] == MetaCoreUiNodeType::Input) {
node.Placeholder = "Value";
node.MaxLength = 32;
node.Bindings.push_back({"script.form.value", MetaCoreUiBindingTarget::Value,
MetaCoreUiBindingMode::TwoWay, {}, {}, "default"});
}
if (types[index] == MetaCoreUiNodeType::List) {
node.Bindings.push_back({"script.rows", MetaCoreUiBindingTarget::Items,
MetaCoreUiBindingMode::OneWay, {}, {}, {}});
}
root.Children.push_back(node.Id);
document.Nodes.push_back(std::move(node));
}
document.Nodes.insert(document.Nodes.begin(), root);
document.RootNodeIds.push_back(root.Id);
return document;
}
} // namespace
int main() {
using namespace MetaCore;
const MetaCoreUiDocument document = BuildDocument();
Expect(MetaCoreValidateUiDocument(document).empty(), "valid document was rejected");
const MetaCoreCompiledUiDocument first = MetaCoreCompileUiDocument(document);
const MetaCoreCompiledUiDocument second = MetaCoreCompileUiDocument(document);
Expect(first.Succeeded, "document compilation failed");
Expect(first.Rml == second.Rml && first.Rcss == second.Rcss, "compiler output is not deterministic");
Expect(first.Rml.find("<input") != std::string::npos, "Input control was not compiled");
Expect(first.Rml.find("role=\"listbox\"") != std::string::npos, "List control was not compiled");
Expect(first.Rml.find("mcui-scroll") != std::string::npos, "Scroll control was not compiled");
Expect(first.Rml.find("Safe &lt;text&gt;") != std::string::npos, "text was not escaped");
Expect(first.Rcss.find("left:calc(25") != std::string::npos, "anchors were not compiled");
Expect(first.Rml.find("data-metacore-bind-mode-value=\"two-way\"") != std::string::npos, "two-way binding was not compiled");
MetaCoreUiDocument invalid = document;
invalid.Nodes.back().Id = invalid.Nodes.front().Id;
Expect(!MetaCoreValidateUiDocument(invalid).empty(), "duplicate stable ID was accepted");
invalid = document;
invalid.Nodes[1].Bindings.push_back({"unknown.path", MetaCoreUiBindingTarget::Text,
MetaCoreUiBindingMode::OneWay, {}, {}, {}});
Expect(!MetaCoreValidateUiDocument(invalid).empty(), "invalid binding namespace was accepted");
MetaCoreUiDocument legacy = document;
legacy.SchemaVersion = 1;
legacy.Nodes[1].Bindings.clear();
legacy.Nodes[1].DataBinding = "runtime.temperature";
legacy.Nodes[1].DataFormat = "1";
const MetaCoreUiDocument migrated = MetaCoreMigrateUiDocument(legacy);
Expect(migrated.SchemaVersion == METACORE_UI_SCHEMA_VERSION, "legacy schema was not migrated");
Expect(migrated.Nodes[1].Bindings.size() == 1, "legacy binding was not migrated");
const std::filesystem::path jsonPath = std::filesystem::temp_directory_path() / "MetaCoreRuntimeUiV2.mcui.json";
MetaCoreTypeRegistry registry;
MetaCoreRegisterFoundationGeneratedTypes(registry);
MetaCoreRegisterSceneGeneratedTypes(registry);
Expect(MetaCoreSerializeToBytes(document, registry).has_value(), "v2 binary serialization failed");
Expect(MetaCoreSceneSerializer::SaveUiToJson(jsonPath, document, registry), "v2 JSON write failed");
const auto loaded = MetaCoreSceneSerializer::LoadUiFromJson(jsonPath, registry);
Expect(loaded.has_value() && loaded->SchemaVersion == 2, "v2 JSON schema did not round-trip");
Expect(loaded->Nodes[4].Placeholder == "Value", "Input settings did not round-trip");
std::filesystem::remove(jsonPath);
std::cout << "MetaCoreRuntimeUiTests passed\n";
return 0;
}

View File

@ -3279,10 +3279,37 @@ void MetaCoreTestCookPipelineCooksJsonScenePrefabMaterialUi() {
MetaCoreExpect(materialRecord->PackagePath == materialPath, "JSON material 的 PackagePath 应保持源路径");
MetaCoreExpect(uiRecord->PackagePath == uiPath, "JSON UI 的 PackagePath 应保持源路径");
MetaCore::MetaCoreGameObjectData uiObject;
uiObject.Id = 100;
uiObject.Name = "Hud";
uiObject.UiRenderer = MetaCore::MetaCoreUiRendererComponent{};
uiObject.UiRenderer->UiDocumentAssetGuid = uiRecord->Guid;
uiObject.UiRenderer->Layer = 7;
sceneDocument.GameObjects.push_back(std::move(uiObject));
MetaCoreExpect(
MetaCore::MetaCoreSceneSerializer::SaveSceneToJson(tempProjectRoot / scenePath, sceneDocument, registry),
"应写回引用 UiDocument GUID 的启动场景"
);
MetaCoreExpect(cookService->CookAsset(sceneRecord->Guid), "应能 Cook JSON scene");
MetaCoreExpect(cookService->CookAsset(prefabRecord->Guid), "应能 Cook JSON prefab");
MetaCoreExpect(cookService->CookAsset(materialRecord->Guid), "应能 Cook JSON material");
MetaCoreExpect(cookService->CookAsset(uiRecord->Guid), "应能 Cook JSON UI");
MetaCoreExpect(cookService->CookRuntimeUiAssets(), "应能从启动场景依赖图编译 Runtime UI");
const std::filesystem::path compiledUiRoot =
tempProjectRoot / "Build" / "Ui" / "Compiled" / uiRecord->Guid.ToString();
MetaCoreExpect(std::filesystem::is_regular_file(compiledUiRoot / "document.rml"), "Runtime UI Cook 应生成确定性 RML");
MetaCoreExpect(std::filesystem::is_regular_file(compiledUiRoot / "document.rcss"), "Runtime UI Cook 应生成确定性 RCSS");
const auto cookedUiManifest = MetaCore::MetaCoreReadRuntimeUiManifest(
tempProjectRoot / "Build" / "Runtime" / "Ui.mcruntime",
registry
);
MetaCoreExpect(cookedUiManifest.has_value() && cookedUiManifest->SchemaVersion == 2, "Runtime UI Cook 应生成 v2 清单");
MetaCoreExpect(cookedUiManifest->Documents.size() == 1, "Runtime UI v2 清单应包含场景 UI 实例");
MetaCoreExpect(
cookedUiManifest->Documents.front().UiDocumentAssetGuidString == uiRecord->Guid.ToString(),
"Runtime UI v2 清单应以 UiDocument GUID 引用资产"
);
const auto expectCookedOutput = [&](const MetaCore::MetaCoreAssetGuid& guid, const char* message) {
const std::filesystem::path cookedPath = cookService->GetCookedPathForAsset(guid);