From 92431296fa6d41055cac817820f5f33f7ec162c1 Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Mon, 27 Jul 2026 10:36:59 +0800 Subject: [PATCH] =?UTF-8?q?UI=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Apps/MetaCorePlayer/main.cpp | 22 +- CMakeLists.txt | 33 +- .../MetaCoreBuiltinCoreServicesModule.cpp | 140 +++++++-- .../Private/MetaCoreBuiltinEditorModule.cpp | 155 +++++++++- .../Private/MetaCorePackage.cpp | 10 +- .../MetaCoreRuntimeDataProject.h | 30 ++ .../Private/MetaCoreRuntimeUiSystem.cpp | 287 +++++++++++++++--- .../Private/MetaCoreUiCompiler.cpp | 255 ++++++++++++++++ .../MetaCoreRuntimeUiSystem.h | 87 ++++++ .../Private/MetaCoreSceneSerializer.cpp | 84 +++++ .../MetaCoreScene/MetaCoreSceneDocument.h | 148 ++++++++- .../Private/MetaCoreScripting.cpp | 47 ++- .../MetaCoreScripting/MetaCoreScriptAbi.h | 22 +- .../MetaCoreScripting/MetaCoreScripting.h | 5 +- ...etacore-mature-engine-gap-and-work-plan.md | 27 +- tests/MetaCoreRuntimeUiTests.cpp | 117 +++++++ tests/MetaCoreSmokeTests.cpp | 27 ++ 17 files changed, 1390 insertions(+), 106 deletions(-) create mode 100644 Source/MetaCoreRuntimeUi/Private/MetaCoreUiCompiler.cpp create mode 100644 tests/MetaCoreRuntimeUiTests.cpp diff --git a/Apps/MetaCorePlayer/main.cpp b/Apps/MetaCorePlayer/main.cpp index 3a21ed8..1a73300 100644 --- a/Apps/MetaCorePlayer/main.cpp +++ b/Apps/MetaCorePlayer/main.cpp @@ -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::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 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(event.InstanceId)), + textValue(event.DocumentAssetGuid.ToString()), + intValue(static_cast(event.EventType)), + intValue(static_cast(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(event.Type); diff --git a/CMakeLists.txt b/CMakeLists.txt index 41a7700..e86779d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp index c0a03be..06c4819 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp @@ -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(output.write(text.data(), static_cast(text.size()))); + }; + std::optional 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(*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 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 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(*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(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 payload{ + textValue(event.DocumentId), textValue(event.ElementId), textValue(event.Action), + textValue(event.Type), textValue(event.Value), textValue(event.OldValue), + intValue(static_cast(event.InstanceId)), + textValue(event.DocumentAssetGuid.ToString()), + intValue(static_cast(event.EventType)), + intValue(static_cast(event.InputSource)) + }; + Runtime_->Publish("MetaCore.UI", payload); + if (event.SceneObjectId != 0) { + Runtime_->PublishToObject(event.SceneObjectId, "MetaCore.UI", payload); + } }); } } diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp index 883463e..ebd0aa8 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp @@ -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(Document_.ScaleMode); + if (ImGui::Combo("Canvas 缩放", &scaleMode, scaleModeLabels, IM_ARRAYSIZE(scaleModeLabels))) { + Document_.ScaleMode = static_cast(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(node->Type); if (ImGui::Combo("类型", &nodeTypeIndex, nodeTypeLabels, IM_ARRAYSIZE(nodeTypeLabels))) { const MetaCoreUiNodeType previousType = node->Type; - node->Type = static_cast(std::clamp(nodeTypeIndex, 0, 3)); + node->Type = static_cast(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(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(binding.Target); + if (ImGui::Combo("Target", &target, targetLabels, IM_ARRAYSIZE(targetLabels))) { + binding.Target = static_cast(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(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()); moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); - moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); - moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterService(std::make_shared()); // The central Scene / Game viewport is not a dockable provider, but it keeps // its existing visibility state separate from the scene settings panel. diff --git a/Source/MetaCoreFoundation/Private/MetaCorePackage.cpp b/Source/MetaCoreFoundation/Private/MetaCorePackage.cpp index 23598f2..3d4e46f 100644 --- a/Source/MetaCoreFoundation/Private/MetaCorePackage.cpp +++ b/Source/MetaCoreFoundation/Private/MetaCorePackage.cpp @@ -2,6 +2,7 @@ #include #include +#include namespace MetaCore { @@ -14,7 +15,14 @@ namespace { } input.seekg(0, std::ios::end); - const auto size = static_cast(input.tellg()); + const std::streampos end = input.tellg(); + if (end < std::streampos{0}) { + return std::nullopt; + } + const auto size = static_cast(end); + if (size > static_cast(std::numeric_limits::max())) { + return std::nullopt; + } input.seekg(0, std::ios::beg); std::vector bytes(size); diff --git a/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h b/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h index afa2d5c..d827967 100644 --- a/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h +++ b/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h @@ -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 Documents{}; + + MC_PROPERTY() + std::uint32_t SchemaVersion = 2; + + MC_PROPERTY() + std::uint32_t GeneratorVersion = 1; }; [[nodiscard]] bool MetaCoreWriteRuntimeDataSourcesDocument( diff --git a/Source/MetaCoreRuntimeUi/Private/MetaCoreRuntimeUiSystem.cpp b/Source/MetaCoreRuntimeUi/Private/MetaCoreRuntimeUiSystem.cpp index 1df6e2c..26ec6b5 100644 --- a/Source/MetaCoreRuntimeUi/Private/MetaCoreRuntimeUiSystem.cpp +++ b/Source/MetaCoreRuntimeUi/Private/MetaCoreRuntimeUiSystem.cpp @@ -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 += "&"; break; + case '<': result += "<"; break; + case '>': result += ">"; break; + case '"': result += """; 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(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::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("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("value", ""); + uiEvent.EventType = EventTypeFromRml(uiEvent.Type); + uiEvent.InputSource = Owner_.CurrentInputSource_; + uiEvent.Consumed = !event.IsPropagating(); + const std::string writePath = target->GetAttribute("data-metacore-bind-value", ""); + const std::string writeMode = target->GetAttribute("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 Documents_; std::vector> Listeners_; - std::unordered_map Values_; + std::unordered_map Values_; + std::unordered_map> Lists_; std::vector Events_; std::vector Errors_; + std::vector 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("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("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("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("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("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("data-metacore-format", ""); - element->SetInnerRML(FormatValue(value->second, format)); + element->SetInnerRML(EscapeRmlText(FormatUiValue(value->second, + element->GetAttribute("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(*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()) {} @@ -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 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_, 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(cursor.x), static_cast(cursor.y), 0); for (int button = 0; button < 3; ++button) { const auto mouseButton = static_cast(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(codePoint)) || consumed.Keyboard; + for (const char32_t codePoint : input.GetTextInput()) { + Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Keyboard; + consumed.Keyboard = !Impl_->Context_->ProcessTextInput(static_cast(codePoint)) || consumed.Keyboard; + } + Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Programmatic; return consumed; } void MetaCoreRuntimeUiSystem::ApplyDataUpdates(const std::vector& 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 MetaCoreRuntimeUiSystem::GetValue(const std::string& sourcePath) const { + const auto found = Impl_->Values_.find(sourcePath); + return found == Impl_->Values_.end() ? std::nullopt : std::optional(found->second); +} + +bool MetaCoreRuntimeUiSystem::SetListRows(const std::string& sourcePath, std::vector 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("data-metacore-bind-items", "") != sourcePath) continue; + std::ostringstream markup; + std::size_t index = 0; + for (const auto& row : Impl_->Lists_[sourcePath]) { + markup << "
"; + std::vector 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 << "" + << EscapeRmlText(FormatUiValue(row.at(key), {})) << ""; + markup << "
"; + } + 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 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& MetaCoreRuntimeUiSystem::GetErrors() const { return Impl_->Errors_; } +std::vector MetaCoreRuntimeUiSystem::GetDiagnostics() const { return Impl_->Diagnostics_; } } // namespace MetaCore diff --git a/Source/MetaCoreRuntimeUi/Private/MetaCoreUiCompiler.cpp b/Source/MetaCoreRuntimeUi/Private/MetaCoreUiCompiler.cpp new file mode 100644 index 0000000..782df49 --- /dev/null +++ b/Source/MetaCoreRuntimeUi/Private/MetaCoreUiCompiler.cpp @@ -0,0 +1,255 @@ +#include "MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h" + +#include +#include +#include +#include +#include +#include +#include + +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 += "&"; break; + case '<': result += "<"; break; + case '>': result += ">"; break; + case '"': result += """; break; + case '\'': result += "'"; 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(std::clamp(value.x, 0.0F, 1.0F) * 255.0F) << ',' + << static_cast(std::clamp(value.y, 0.0F, 1.0F) * 255.0F) << ',' + << static_cast(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 MetaCoreValidateUiDocument(const MetaCoreUiDocument& document) { + std::vector 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 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 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 visited; + std::function 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 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 << "(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 emit = [&](const std::string& id, int depth) { + const MetaCoreUiNodeDocument& node = *nodes.at(id); + const std::string indent(static_cast(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 << "\n"; + }; + for (const std::string& root : document.RootNodeIds) emit(root, 2); + 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 diff --git a/Source/MetaCoreRuntimeUi/Public/MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h b/Source/MetaCoreRuntimeUi/Public/MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h index 9c7ddf7..3ca90d1 100644 --- a/Source/MetaCoreRuntimeUi/Public/MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h +++ b/Source/MetaCoreRuntimeUi/Public/MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h @@ -3,11 +3,14 @@ #include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" #include "MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h" #include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h" +#include "MetaCoreScene/MetaCoreSceneDocument.h" #include #include #include +#include #include +#include #include 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; + 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 Dependencies{}; + std::vector Diagnostics{}; + bool Succeeded = false; +}; + +[[nodiscard]] std::vector MetaCoreValidateUiDocument(const MetaCoreUiDocument& document); +[[nodiscard]] MetaCoreCompiledUiDocument MetaCoreCompileUiDocument(const MetaCoreUiDocument& document); +[[nodiscard]] MetaCoreUiDocument MetaCoreMigrateUiDocument(MetaCoreUiDocument document); + class MetaCoreRuntimeUiSystem { public: using EventCallback = std::function; @@ -43,13 +119,24 @@ public: void Update(float deltaSeconds); [[nodiscard]] MetaCoreInputConsumption ProcessInput(); void ApplyDataUpdates(const std::vector& updates); + [[nodiscard]] bool SetValue(const std::string& sourcePath, const MetaCoreRuntimeUiValue& value); + [[nodiscard]] std::optional GetValue(const std::string& sourcePath) const; + [[nodiscard]] bool SetListRows(const std::string& sourcePath, std::vector 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 ConsumeEvents(); void SetEventCallback(EventCallback callback); [[nodiscard]] bool IsInitialized() const; [[nodiscard]] std::size_t GetLoadedDocumentCount() const; [[nodiscard]] const std::vector& GetErrors() const; + [[nodiscard]] std::vector GetDiagnostics() const; private: class Impl; diff --git a/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp b/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp index 4a001a4..e7b43c3 100644 --- a/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp +++ b/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp @@ -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(); return trans; } @@ -884,6 +890,9 @@ static json UiStyleToJson(const MetaCoreUiStyleDocument& style) { j["VerticalAlignment"] = static_cast(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(j["VerticalAlignment"].get()); if (j.contains("ImageAssetGuid")) style.ImageAssetGuid = StringToGuid(j["ImageAssetGuid"].get()); if (j.contains("PreserveAspect")) style.PreserveAspect = j["PreserveAspect"].get(); + if (j.contains("Opacity")) style.Opacity = j["Opacity"].get(); + if (j.contains("BorderRadius")) style.BorderRadius = j["BorderRadius"].get(); + if (j.contains("ThemeToken")) style.ThemeToken = j["ThemeToken"].get(); return style; } +static json UiBindingToJson(const MetaCoreUiBindingDocument& binding) { + return json{ + {"SourcePath", binding.SourcePath}, + {"Target", static_cast(binding.Target)}, + {"Mode", static_cast(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(); + if (j.contains("Target")) binding.Target = static_cast(j["Target"].get()); + if (j.contains("Mode")) binding.Mode = static_cast(j["Mode"].get()); + if (j.contains("Format")) binding.Format = j["Format"].get(); + if (j.contains("Converter")) binding.Converter = j["Converter"].get(); + if (j.contains("Fallback")) binding.Fallback = j["Fallback"].get(); + 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(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(); if (j.contains("DataBinding")) node.DataBinding = j["DataBinding"].get(); if (j.contains("DataFormat")) node.DataFormat = j["DataFormat"].get(); + 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(); + if (j.contains("Placeholder")) node.Placeholder = j["Placeholder"].get(); + if (j.contains("InputValueType")) node.InputValueType = static_cast(j["InputValueType"].get()); + if (j.contains("ReadOnly")) node.ReadOnly = j["ReadOnly"].get(); + if (j.contains("MaxLength")) node.MaxLength = j["MaxLength"].get(); + if (j.contains("SelectedIndex")) node.SelectedIndex = j["SelectedIndex"].get(); + if (j.contains("ItemTemplateNodeId")) node.ItemTemplateNodeId = j["ItemTemplateNodeId"].get(); + if (j.contains("LocalizationKey")) node.LocalizationKey = j["LocalizationKey"].get(); + if (j.contains("ShowAnimation")) node.ShowAnimation = j["ShowAnimation"].get(); + if (j.contains("HideAnimation")) node.HideAnimation = j["HideAnimation"].get(); 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(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 MetaCoreSceneSerializer::LoadUiFromJson( file >> uiJson; MetaCoreUiDocument doc; + if (uiJson.contains("SchemaVersion")) doc.SchemaVersion = uiJson["SchemaVersion"].get(); else doc.SchemaVersion = 1; + if (uiJson.contains("GeneratorVersion")) doc.GeneratorVersion = uiJson["GeneratorVersion"].get(); if (uiJson.contains("Name")) doc.Name = uiJson["Name"].get(); if (uiJson.contains("ReferenceWidth")) doc.ReferenceWidth = uiJson["ReferenceWidth"].get(); if (uiJson.contains("ReferenceHeight")) doc.ReferenceHeight = uiJson["ReferenceHeight"].get(); + if (uiJson.contains("ScaleMode")) doc.ScaleMode = static_cast(uiJson["ScaleMode"].get()); + if (uiJson.contains("MatchWidthOrHeight")) doc.MatchWidthOrHeight = uiJson["MatchWidthOrHeight"].get(); + if (uiJson.contains("ReferenceDpi")) doc.ReferenceDpi = uiJson["ReferenceDpi"].get(); + if (uiJson.contains("MinScale")) doc.MinScale = uiJson["MinScale"].get(); + if (uiJson.contains("MaxScale")) doc.MaxScale = uiJson["MaxScale"].get(); + if (uiJson.contains("UseSafeArea")) doc.UseSafeArea = uiJson["UseSafeArea"].get(); + if (uiJson.contains("ThemeAssetGuid")) doc.ThemeAssetGuid = StringToGuid(uiJson["ThemeAssetGuid"].get()); + if (uiJson.contains("FontFamilyAssetGuid")) doc.FontFamilyAssetGuid = StringToGuid(uiJson["FontFamilyAssetGuid"].get()); + if (uiJson.contains("LocalizationAssetGuid")) doc.LocalizationAssetGuid = StringToGuid(uiJson["LocalizationAssetGuid"].get()); + if (uiJson.contains("DefaultLocale")) doc.DefaultLocale = uiJson["DefaultLocale"].get(); + if (uiJson.contains("FallbackLocale")) doc.FallbackLocale = uiJson["FallbackLocale"].get(); if (uiJson.contains("BackgroundColor")) doc.BackgroundColor = JsonToVec3(uiJson["BackgroundColor"]); if (uiJson.contains("BackgroundAlpha")) doc.BackgroundAlpha = uiJson["BackgroundAlpha"].get(); if (uiJson.contains("RootNodeIds")) doc.RootNodeIds = uiJson["RootNodeIds"].get>(); diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h index 74f0478..f5b5144 100644 --- a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h @@ -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 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}; diff --git a/Source/MetaCoreScripting/Private/MetaCoreScripting.cpp b/Source/MetaCoreScripting/Private/MetaCoreScripting.cpp index 12aea48..ba0e33b 100644 --- a/Source/MetaCoreScripting/Private/MetaCoreScripting.cpp +++ b/Source/MetaCoreScripting/Private/MetaCoreScripting.cpp @@ -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 @@ -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(*this, scene, registry, std::move(logger), input, physics, animation)) {} + MetaCoreRuntimeInput* input, MetaCorePhysicsWorld* physics, MetaCoreAnimationRuntime* animation, MetaCoreRuntimeUiSystem* runtimeUi) + : Impl_(std::make_unique(*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()) for (auto& i : o.GetComponent().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 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(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(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(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(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 converted;converted.reserve(count);for(std::size_t i=0;iSetListRows(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) { diff --git a/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScriptAbi.h b/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScriptAbi.h index a023d93..bc767c8 100644 --- a/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScriptAbi.h +++ b/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScriptAbi.h @@ -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 { diff --git a/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScripting.h b/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScripting.h index 3c2c826..94312d9 100644 --- a/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScripting.h +++ b/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScripting.h @@ -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 callback); void CancelTimer(std::uint64_t timerId); diff --git a/docs/designs/metacore-mature-engine-gap-and-work-plan.md b/docs/designs/metacore-mature-engine-gap-and-work-plan.md index 867fd50..9795192 100644 --- a/docs/designs/metacore-mature-engine-gap-and-work-plan.md +++ b/docs/designs/metacore-mature-engine-gap-and-work-plan.md @@ -334,14 +334,27 @@ R8.1–R8.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/` 产物,并收集、验证和复制 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 diff --git a/tests/MetaCoreRuntimeUiTests.cpp b/tests/MetaCoreRuntimeUiTests.cpp new file mode 100644 index 0000000..a55680f --- /dev/null +++ b/tests/MetaCoreRuntimeUiTests.cpp @@ -0,0 +1,117 @@ +#include "MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h" +#include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" +#include "MetaCoreScene/MetaCoreSceneSerializer.h" + +#include +#include +#include +#include + +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 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 "; + node.Interactable = types[index] == MetaCoreUiNodeType::Button || types[index] == MetaCoreUiNodeType::Input; + node.NavigationOrder = static_cast(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(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("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; +} diff --git a/tests/MetaCoreSmokeTests.cpp b/tests/MetaCoreSmokeTests.cpp index 35d1461..f9c756a 100644 --- a/tests/MetaCoreSmokeTests.cpp +++ b/tests/MetaCoreSmokeTests.cpp @@ -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);