UI优化
This commit is contained in:
parent
3ba7b090d1
commit
92431296fa
@ -524,12 +524,13 @@ int main(int argc, char* argv[]) {
|
||||
[&animationAssets](MetaCore::MetaCoreAssetGuid guid) { return animationAssets.LoadController(guid); },
|
||||
[&animationAssets](MetaCore::MetaCoreAssetGuid guid) { return animationAssets.LoadAvatar(guid); },
|
||||
[&animationAssets](MetaCore::MetaCoreAssetGuid guid) { return animationAssets.LoadTimeline(guid); }, &physicsWorld);
|
||||
MetaCore::MetaCoreRuntimeUiSystem runtimeUi;
|
||||
MetaCore::MetaCoreScriptRuntime scriptRuntime(
|
||||
scene, scriptRegistry,
|
||||
[&output, &errors](std::uint32_t level, std::string_view category, std::string_view message) {
|
||||
std::ostream& stream = level >= 2U ? errors : output;
|
||||
stream << "MetaCorePlayer Script[" << category << "]: " << message << '\n';
|
||||
}, &runtimeInput, &physicsWorld, &animationRuntime
|
||||
}, &runtimeInput, &physicsWorld, &animationRuntime, &runtimeUi
|
||||
);
|
||||
scriptRuntime.Start();
|
||||
animationRuntime.SetEventCallback([&scriptRuntime](const MetaCore::MetaCoreAnimationRuntimeEvent& event) {
|
||||
@ -572,7 +573,6 @@ int main(int argc, char* argv[]) {
|
||||
const auto loadedUiManifest = runtimeProjectDocument.UiManifestPath.empty()
|
||||
? std::optional<MetaCore::MetaCoreRuntimeUiManifest>{}
|
||||
: MetaCore::MetaCoreReadRuntimeUiManifest(uiManifestPath, typeRegistry);
|
||||
MetaCore::MetaCoreRuntimeUiSystem runtimeUi;
|
||||
if (loadedUiManifest.has_value()) {
|
||||
if (!runtimeUi.Initialize(window, projectRoot, projectRoot / "Ui", *loadedUiManifest)) {
|
||||
errors << "MetaCorePlayer: failed to initialize runtime UI\n";
|
||||
@ -641,10 +641,22 @@ int main(int argc, char* argv[]) {
|
||||
MetaCoreScriptValueV1 value{}; value.Type = MetaCoreScriptAbiValue_String;
|
||||
std::snprintf(value.Text, sizeof(value.Text), "%s", text.c_str()); return value;
|
||||
};
|
||||
scriptRuntime.Publish("MetaCore.UI", {
|
||||
const auto intValue = [](std::int64_t number) {
|
||||
MetaCoreScriptValueV1 value{}; value.Type = MetaCoreScriptAbiValue_Int;
|
||||
value.IntValue = number; return value;
|
||||
};
|
||||
const std::vector<MetaCoreScriptValueV1> payload{
|
||||
textValue(event.DocumentId), textValue(event.ElementId), textValue(event.Action),
|
||||
textValue(event.Type), textValue(event.Value)
|
||||
});
|
||||
textValue(event.Type), textValue(event.Value), textValue(event.OldValue),
|
||||
intValue(static_cast<std::int64_t>(event.InstanceId)),
|
||||
textValue(event.DocumentAssetGuid.ToString()),
|
||||
intValue(static_cast<std::int64_t>(event.EventType)),
|
||||
intValue(static_cast<std::int64_t>(event.InputSource))
|
||||
};
|
||||
scriptRuntime.Publish("MetaCore.UI", payload);
|
||||
if (event.SceneObjectId != 0) {
|
||||
scriptRuntime.PublishToObject(event.SceneObjectId, "MetaCore.UI", payload);
|
||||
}
|
||||
});
|
||||
runtimeInteraction.SetEventCallback([&scriptRuntime](const MetaCore::MetaCorePointerEvent& event) {
|
||||
MetaCoreScriptValueV1 type{}; type.Type = MetaCoreScriptAbiValue_Int; type.IntValue = static_cast<std::int64_t>(event.Type);
|
||||
|
||||
@ -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})
|
||||
|
||||
@ -8889,7 +8889,7 @@ bool MetaCoreBuiltinCookService::CookScene(const std::filesystem::path& relative
|
||||
}
|
||||
|
||||
bool MetaCoreBuiltinCookService::CookRuntimeUiAssets() {
|
||||
if (AssetDatabaseService_ == nullptr || ReflectionRegistry_ == nullptr || !AssetDatabaseService_->HasProject()) {
|
||||
if (AssetDatabaseService_ == nullptr || PackageService_ == nullptr || ReflectionRegistry_ == nullptr || !AssetDatabaseService_->HasProject()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -8900,38 +8900,104 @@ bool MetaCoreBuiltinCookService::CookRuntimeUiAssets() {
|
||||
const std::filesystem::path targetUiRoot = project.BuildPath / "Ui";
|
||||
const std::filesystem::path targetManifestPath = targetRuntimeRoot / "Ui.mcruntime";
|
||||
|
||||
if (!std::filesystem::is_directory(sourceUiRoot) || !std::filesystem::exists(sourceManifestPath)) {
|
||||
return false;
|
||||
std::error_code errorCode;
|
||||
std::filesystem::create_directories(targetRuntimeRoot, errorCode);
|
||||
std::filesystem::create_directories(targetUiRoot, errorCode);
|
||||
if (errorCode) return false;
|
||||
|
||||
const MetaCoreTypeRegistry& registry = ReflectionRegistry_->GetTypeRegistry();
|
||||
const auto writeText = [](const std::filesystem::path& path, const std::string& text) {
|
||||
std::ofstream output(path, std::ios::binary | std::ios::trunc);
|
||||
return output.is_open() && static_cast<bool>(output.write(text.data(), static_cast<std::streamsize>(text.size())));
|
||||
};
|
||||
std::optional<MetaCoreSceneDocument> startupScene;
|
||||
if (!project.StartupScenePath.empty()) {
|
||||
const std::filesystem::path scenePath = project.RootPath / project.StartupScenePath;
|
||||
if (std::filesystem::is_regular_file(scenePath, errorCode)) {
|
||||
if (scenePath.extension() == ".json") {
|
||||
startupScene = MetaCoreSceneSerializer::LoadSceneFromJson(scenePath, registry);
|
||||
} else if (const auto package = PackageService_->ReadPackage(scenePath); package.has_value()) {
|
||||
startupScene = MetaCoreReadTypedPackagePayload<MetaCoreSceneDocument>(*package, registry, "MetaCoreSceneDocument");
|
||||
}
|
||||
}
|
||||
errorCode.clear();
|
||||
}
|
||||
|
||||
const auto manifest = MetaCoreReadRuntimeUiManifest(sourceManifestPath, ReflectionRegistry_->GetTypeRegistry());
|
||||
if (!manifest.has_value()) {
|
||||
return false;
|
||||
}
|
||||
for (const MetaCoreRuntimeUiDocumentEntry& entry : manifest->Documents) {
|
||||
const std::filesystem::path absoluteRmlPath = sourceUiRoot / entry.RmlPath;
|
||||
const std::filesystem::path normalizedRelative = entry.RmlPath.lexically_normal();
|
||||
if (entry.DocumentId.empty() ||
|
||||
normalizedRelative.empty() ||
|
||||
normalizedRelative.is_absolute() ||
|
||||
*normalizedRelative.begin() == ".." ||
|
||||
absoluteRmlPath.extension() != ".rml" ||
|
||||
!std::filesystem::exists(absoluteRmlPath)) {
|
||||
return false;
|
||||
MetaCoreRuntimeUiManifest cookedManifest;
|
||||
cookedManifest.SchemaVersion = 2;
|
||||
cookedManifest.GeneratorVersion = METACORE_UI_GENERATOR_VERSION;
|
||||
std::unordered_set<MetaCoreAssetGuid, MetaCoreAssetGuidHasher> compiledAssets;
|
||||
if (startupScene.has_value()) {
|
||||
for (const MetaCoreGameObjectData& object : startupScene->GameObjects) {
|
||||
if (!object.UiRenderer.has_value() || !object.UiRenderer->Visible ||
|
||||
!object.UiRenderer->UiDocumentAssetGuid.IsValid()) continue;
|
||||
const MetaCoreAssetGuid guid = object.UiRenderer->UiDocumentAssetGuid;
|
||||
const auto record = AssetDatabaseService_->FindAssetByGuid(guid);
|
||||
if (!record.has_value() || record->Type != "ui_document") return false;
|
||||
|
||||
if (compiledAssets.insert(guid).second) {
|
||||
std::optional<MetaCoreUiDocument> document;
|
||||
const std::filesystem::path sourcePath = project.RootPath / record->RelativePath;
|
||||
if (sourcePath.extension() == ".json") {
|
||||
document = MetaCoreSceneSerializer::LoadUiFromJson(sourcePath, registry);
|
||||
} else if (const auto package = PackageService_->ReadPackage(sourcePath); package.has_value()) {
|
||||
document = MetaCoreReadTypedPackagePayload<MetaCoreUiDocument>(*package, registry, "MetaCoreUiDocument");
|
||||
}
|
||||
if (!document.has_value()) return false;
|
||||
const MetaCoreCompiledUiDocument compiled = MetaCoreCompileUiDocument(*document);
|
||||
if (!compiled.Succeeded) return false;
|
||||
const std::filesystem::path outputDirectory = targetUiRoot / "Compiled" / guid.ToString();
|
||||
std::filesystem::create_directories(outputDirectory / "Assets", errorCode);
|
||||
if (errorCode ||
|
||||
!writeText(outputDirectory / "document.rml", compiled.Rml) ||
|
||||
!writeText(outputDirectory / "document.rcss", compiled.Rcss)) return false;
|
||||
for (const MetaCoreAssetGuid dependency : compiled.Dependencies) {
|
||||
const auto dependencyRecord = AssetDatabaseService_->FindAssetByGuid(dependency);
|
||||
if (!dependencyRecord.has_value()) return false;
|
||||
if (dependencyRecord->Type != "texture") continue;
|
||||
std::filesystem::path dependencySource = !dependencyRecord->SourcePath.empty()
|
||||
? dependencyRecord->SourcePath : dependencyRecord->RelativePath;
|
||||
if (dependencySource.is_relative()) dependencySource = project.RootPath / dependencySource;
|
||||
errorCode.clear();
|
||||
std::filesystem::copy_file(dependencySource, outputDirectory / "Assets" / (dependency.ToString() + ".png"),
|
||||
std::filesystem::copy_options::overwrite_existing, errorCode);
|
||||
if (errorCode) return false;
|
||||
}
|
||||
}
|
||||
|
||||
MetaCoreRuntimeUiDocumentEntry entry;
|
||||
entry.DocumentId = "scene-" + std::to_string(object.Id) + "-ui";
|
||||
entry.RmlPath = std::filesystem::path("Compiled") / guid.ToString() / "document.rml";
|
||||
entry.Layer = object.UiRenderer->Layer;
|
||||
entry.Visible = object.UiRenderer->Visible;
|
||||
entry.InputEnabled = object.UiRenderer->InputEnabled;
|
||||
entry.UiDocumentAssetGuid = guid;
|
||||
entry.UiDocumentAssetGuidString = guid.ToString();
|
||||
entry.InstanceId = object.Id;
|
||||
entry.SceneObjectId = object.Id;
|
||||
entry.RenderMode = static_cast<std::uint32_t>(object.UiRenderer->RenderMode);
|
||||
entry.TargetWidth = 0;
|
||||
entry.TargetHeight = 0;
|
||||
cookedManifest.Documents.push_back(std::move(entry));
|
||||
}
|
||||
}
|
||||
|
||||
if (!MetaCoreCopyDirectoryTree(sourceUiRoot, targetUiRoot)) {
|
||||
return false;
|
||||
if (cookedManifest.Documents.empty()) {
|
||||
const auto legacyManifest = MetaCoreReadRuntimeUiManifest(sourceManifestPath, registry);
|
||||
if (!legacyManifest.has_value() || !std::filesystem::is_directory(sourceUiRoot)) return false;
|
||||
for (const MetaCoreRuntimeUiDocumentEntry& entry : legacyManifest->Documents) {
|
||||
const std::filesystem::path normalizedRelative = entry.RmlPath.lexically_normal();
|
||||
if (entry.DocumentId.empty() || normalizedRelative.empty() || normalizedRelative.is_absolute() ||
|
||||
*normalizedRelative.begin() == ".." || normalizedRelative.extension() != ".rml" ||
|
||||
!std::filesystem::exists(sourceUiRoot / normalizedRelative)) return false;
|
||||
}
|
||||
if (!MetaCoreCopyDirectoryTree(sourceUiRoot, targetUiRoot)) return false;
|
||||
cookedManifest = *legacyManifest;
|
||||
}
|
||||
|
||||
std::error_code errorCode;
|
||||
std::filesystem::create_directories(targetRuntimeRoot, errorCode);
|
||||
if (errorCode) {
|
||||
return false;
|
||||
}
|
||||
std::filesystem::copy_file(sourceManifestPath, targetManifestPath, std::filesystem::copy_options::overwrite_existing, errorCode);
|
||||
return !errorCode;
|
||||
std::sort(cookedManifest.Documents.begin(), cookedManifest.Documents.end(), [](const auto& lhs, const auto& rhs) {
|
||||
return lhs.Layer != rhs.Layer ? lhs.Layer < rhs.Layer : lhs.InstanceId < rhs.InstanceId;
|
||||
});
|
||||
return MetaCoreWriteRuntimeUiManifest(targetManifestPath, cookedManifest, registry);
|
||||
}
|
||||
|
||||
std::filesystem::path MetaCoreBuiltinCookService::GetCookedPathForAsset(const MetaCoreAssetGuid& assetGuid) const {
|
||||
@ -10866,7 +10932,7 @@ public:
|
||||
const MetaCoreLogLevel mapped = level >= 2U ? MetaCoreLogLevel::Error : level == 1U ? MetaCoreLogLevel::Warning : MetaCoreLogLevel::Info;
|
||||
editorContext.AddConsoleMessage(mapped, std::string(category), std::string(message));
|
||||
}, Input_ ? &Input_->GetRuntime() : nullptr, Physics_ ? Physics_->GetOrCreate(editorContext) : nullptr,
|
||||
editorContext.GetActiveAnimationRuntime()
|
||||
editorContext.GetActiveAnimationRuntime(), Input_ ? &Input_->GetRuntimeUi() : nullptr
|
||||
);
|
||||
Runtime_->Start();
|
||||
editorContext.SetActiveScriptRuntime(Runtime_.get());
|
||||
@ -10904,8 +10970,22 @@ public:
|
||||
MetaCoreScriptValueV1 value{}; value.Type = MetaCoreScriptAbiValue_String;
|
||||
std::snprintf(value.Text, sizeof(value.Text), "%s", text.c_str()); return value;
|
||||
};
|
||||
Runtime_->Publish("MetaCore.UI", {textValue(event.DocumentId), textValue(event.ElementId),
|
||||
textValue(event.Action), textValue(event.Type), textValue(event.Value)});
|
||||
const auto intValue = [](std::int64_t number) {
|
||||
MetaCoreScriptValueV1 value{}; value.Type = MetaCoreScriptAbiValue_Int;
|
||||
value.IntValue = number; return value;
|
||||
};
|
||||
const std::vector<MetaCoreScriptValueV1> payload{
|
||||
textValue(event.DocumentId), textValue(event.ElementId), textValue(event.Action),
|
||||
textValue(event.Type), textValue(event.Value), textValue(event.OldValue),
|
||||
intValue(static_cast<std::int64_t>(event.InstanceId)),
|
||||
textValue(event.DocumentAssetGuid.ToString()),
|
||||
intValue(static_cast<std::int64_t>(event.EventType)),
|
||||
intValue(static_cast<std::int64_t>(event.InputSource))
|
||||
};
|
||||
Runtime_->Publish("MetaCore.UI", payload);
|
||||
if (event.SceneObjectId != 0) {
|
||||
Runtime_->PublishToObject(event.SceneObjectId, "MetaCore.UI", payload);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h"
|
||||
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h"
|
||||
#include "MetaCoreRuntimeInput/MetaCoreInputMap.h"
|
||||
#include "MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h"
|
||||
#include "MetaCorePlatform/MetaCoreWindow.h"
|
||||
#include "MetaCorePhysics/MetaCorePhysics.h"
|
||||
#include "MetaCoreAnimation/MetaCoreAnimation.h"
|
||||
@ -346,6 +347,12 @@ void DrawMaterialTextureSlotSummary(
|
||||
return "Image";
|
||||
case MetaCoreUiNodeType::Button:
|
||||
return "Button";
|
||||
case MetaCoreUiNodeType::Input:
|
||||
return "Input";
|
||||
case MetaCoreUiNodeType::List:
|
||||
return "List";
|
||||
case MetaCoreUiNodeType::Scroll:
|
||||
return "Scroll";
|
||||
case MetaCoreUiNodeType::Panel:
|
||||
default:
|
||||
return "Panel";
|
||||
@ -4678,6 +4685,11 @@ private:
|
||||
if (ImGui::Button("+ 按钮")) AddNativeNode(MetaCoreUiNodeType::Button);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("+ 图片")) AddNativeNode(MetaCoreUiNodeType::Image);
|
||||
if (ImGui::Button("+ 输入")) AddNativeNode(MetaCoreUiNodeType::Input);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("+ 列表")) AddNativeNode(MetaCoreUiNodeType::List);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("+ 滚动")) AddNativeNode(MetaCoreUiNodeType::Scroll);
|
||||
ImGui::Separator();
|
||||
const bool hasSelection = !SelectedNodeId_.empty() && MetaCoreFindUiNode(Document_, SelectedNodeId_) != nullptr;
|
||||
ImGui::BeginDisabled(!hasSelection);
|
||||
@ -4957,6 +4969,22 @@ private:
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.Type == MetaCoreUiNodeType::Input) {
|
||||
node.Interactable = true;
|
||||
node.Text.clear();
|
||||
node.Placeholder = "请输入";
|
||||
node.Style.BackgroundColor = glm::vec3(0.10F, 0.12F, 0.15F);
|
||||
node.RectTransform.Size = glm::vec3(std::max(220.0F, node.RectTransform.Size.x), std::max(40.0F, node.RectTransform.Size.y), 0.0F);
|
||||
return;
|
||||
}
|
||||
if (node.Type == MetaCoreUiNodeType::List || node.Type == MetaCoreUiNodeType::Scroll) {
|
||||
node.Interactable = true;
|
||||
node.Style.BackgroundColor = glm::vec3(0.08F, 0.10F, 0.13F);
|
||||
node.RectTransform.ClipChildren = true;
|
||||
node.RectTransform.Size = glm::vec3(std::max(280.0F, node.RectTransform.Size.x), std::max(200.0F, node.RectTransform.Size.y), 0.0F);
|
||||
return;
|
||||
}
|
||||
|
||||
node.Interactable = false;
|
||||
node.Action.clear();
|
||||
if (node.Type == MetaCoreUiNodeType::Text) {
|
||||
@ -5148,6 +5176,10 @@ private:
|
||||
case MetaCoreUiNodeType::Button:
|
||||
ApplyNativeButtonStylePreset(node, 0);
|
||||
break;
|
||||
case MetaCoreUiNodeType::Input:
|
||||
case MetaCoreUiNodeType::List:
|
||||
case MetaCoreUiNodeType::Scroll:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5190,6 +5222,11 @@ private:
|
||||
drawPreset("幽灵按钮", 3, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativeButtonStylePreset(target, index); });
|
||||
drawPreset("工具按钮", 4, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativeButtonStylePreset(target, index); });
|
||||
break;
|
||||
case MetaCoreUiNodeType::Input:
|
||||
case MetaCoreUiNodeType::List:
|
||||
case MetaCoreUiNodeType::Scroll:
|
||||
ImGui::TextDisabled("该控件使用主题默认样式。");
|
||||
break;
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
return changed;
|
||||
@ -5260,6 +5297,19 @@ private:
|
||||
Dirty_ |= DrawNativeString("文档名称", Document_.Name, 128);
|
||||
if (ImGui::DragInt("参考宽度", &Document_.ReferenceWidth, 1.0F, 320, 7680)) Dirty_ = true;
|
||||
if (ImGui::DragInt("参考高度", &Document_.ReferenceHeight, 1.0F, 240, 4320)) Dirty_ = true;
|
||||
const char* scaleModeLabels[] = {"固定像素", "随分辨率缩放", "固定物理尺寸"};
|
||||
int scaleMode = static_cast<int>(Document_.ScaleMode);
|
||||
if (ImGui::Combo("Canvas 缩放", &scaleMode, scaleModeLabels, IM_ARRAYSIZE(scaleModeLabels))) {
|
||||
Document_.ScaleMode = static_cast<MetaCoreUiCanvasScaleMode>(std::clamp(scaleMode, 0, 2));
|
||||
Dirty_ = true;
|
||||
}
|
||||
Dirty_ |= ImGui::SliderFloat("宽高匹配", &Document_.MatchWidthOrHeight, 0.0F, 1.0F);
|
||||
Dirty_ |= ImGui::DragFloat("参考 DPI", &Document_.ReferenceDpi, 1.0F, 48.0F, 600.0F);
|
||||
Dirty_ |= ImGui::DragFloat("最小缩放", &Document_.MinScale, 0.05F, 0.1F, 8.0F);
|
||||
Dirty_ |= ImGui::DragFloat("最大缩放", &Document_.MaxScale, 0.05F, Document_.MinScale, 8.0F);
|
||||
Dirty_ |= ImGui::Checkbox("使用安全区", &Document_.UseSafeArea);
|
||||
Dirty_ |= DrawNativeString("默认 Locale", Document_.DefaultLocale, 64);
|
||||
Dirty_ |= DrawNativeString("Fallback Locale", Document_.FallbackLocale, 64);
|
||||
Dirty_ |= ImGui::ColorEdit3("UI 背景颜色", &Document_.BackgroundColor.x);
|
||||
if (ImGui::SliderFloat("UI 背景透明度", &Document_.BackgroundAlpha, 0.0F, 1.0F, "%.2f")) {
|
||||
Document_.BackgroundAlpha = std::clamp(Document_.BackgroundAlpha, 0.0F, 1.0F);
|
||||
@ -5287,11 +5337,11 @@ private:
|
||||
return;
|
||||
}
|
||||
Dirty_ |= DrawNativeString("名称", node->Name, 128);
|
||||
const char* nodeTypeLabels[] = {"面板", "文本", "图片", "按钮"};
|
||||
const char* nodeTypeLabels[] = {"面板", "文本", "图片", "按钮", "输入", "列表", "滚动"};
|
||||
int nodeTypeIndex = static_cast<int>(node->Type);
|
||||
if (ImGui::Combo("类型", &nodeTypeIndex, nodeTypeLabels, IM_ARRAYSIZE(nodeTypeLabels))) {
|
||||
const MetaCoreUiNodeType previousType = node->Type;
|
||||
node->Type = static_cast<MetaCoreUiNodeType>(std::clamp(nodeTypeIndex, 0, 3));
|
||||
node->Type = static_cast<MetaCoreUiNodeType>(std::clamp(nodeTypeIndex, 0, 6));
|
||||
ApplyNativeNodeTypeDefaults(*node, previousType);
|
||||
ApplyNativeDefaultStylePreset(*node);
|
||||
Dirty_ = true;
|
||||
@ -5316,17 +5366,41 @@ private:
|
||||
node->RectTransform.Size.y = std::max(1.0F, size[1]);
|
||||
Dirty_ = true;
|
||||
}
|
||||
float anchorMin[2]{node->RectTransform.AnchorMin.x, node->RectTransform.AnchorMin.y};
|
||||
float anchorMax[2]{node->RectTransform.AnchorMax.x, node->RectTransform.AnchorMax.y};
|
||||
float pivot[2]{node->RectTransform.Pivot.x, node->RectTransform.Pivot.y};
|
||||
if (ImGui::DragFloat2("Anchor Min", anchorMin, 0.01F, 0.0F, 1.0F)) {
|
||||
node->RectTransform.AnchorMin = {std::clamp(anchorMin[0], 0.0F, 1.0F), std::clamp(anchorMin[1], 0.0F, 1.0F), 0.0F};
|
||||
Dirty_ = true;
|
||||
}
|
||||
if (ImGui::DragFloat2("Anchor Max", anchorMax, 0.01F, 0.0F, 1.0F)) {
|
||||
node->RectTransform.AnchorMax = {std::max(node->RectTransform.AnchorMin.x, std::clamp(anchorMax[0], 0.0F, 1.0F)),
|
||||
std::max(node->RectTransform.AnchorMin.y, std::clamp(anchorMax[1], 0.0F, 1.0F)), 0.0F};
|
||||
Dirty_ = true;
|
||||
}
|
||||
if (ImGui::DragFloat2("Pivot", pivot, 0.01F, 0.0F, 1.0F)) {
|
||||
node->RectTransform.Pivot = {std::clamp(pivot[0], 0.0F, 1.0F), std::clamp(pivot[1], 0.0F, 1.0F), 0.0F};
|
||||
Dirty_ = true;
|
||||
}
|
||||
Dirty_ |= ImGui::DragInt("导航顺序", &node->NavigationOrder, 1.0F, -1, 100000);
|
||||
Dirty_ |= ImGui::SliderFloat("透明度", &node->Style.Opacity, 0.0F, 1.0F);
|
||||
Dirty_ |= ImGui::DragFloat("圆角", &node->Style.BorderRadius, 0.5F, 0.0F, 256.0F);
|
||||
Dirty_ |= DrawNativeString("主题 Token", node->Style.ThemeToken, 128);
|
||||
Dirty_ |= DrawNativeString("本地化 Key", node->LocalizationKey, 128);
|
||||
|
||||
const bool isPanel = node->Type == MetaCoreUiNodeType::Panel;
|
||||
const bool isText = node->Type == MetaCoreUiNodeType::Text;
|
||||
const bool isImage = node->Type == MetaCoreUiNodeType::Image;
|
||||
const bool isButton = node->Type == MetaCoreUiNodeType::Button;
|
||||
const bool isInput = node->Type == MetaCoreUiNodeType::Input;
|
||||
const bool isList = node->Type == MetaCoreUiNodeType::List;
|
||||
const bool isScroll = node->Type == MetaCoreUiNodeType::Scroll;
|
||||
|
||||
if (isPanel || isButton || isImage) {
|
||||
Dirty_ |= ImGui::ColorEdit3(isImage ? "背景占位" : "背景", &node->Style.BackgroundColor.x);
|
||||
}
|
||||
|
||||
if (isText || isButton) {
|
||||
if (isText || isButton || isInput) {
|
||||
Dirty_ |= DrawNativeString("文本", node->Text, 256);
|
||||
Dirty_ |= ImGui::ColorEdit3("文字颜色", &node->Style.TextColor.x);
|
||||
Dirty_ |= ImGui::DragFloat("字号", &node->Style.FontSize, 0.5F, 1.0F, 200.0F);
|
||||
@ -5361,6 +5435,14 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
if (isInput) {
|
||||
Dirty_ |= DrawNativeString("占位文本", node->Placeholder, 256);
|
||||
Dirty_ |= ImGui::Checkbox("只读", &node->ReadOnly);
|
||||
Dirty_ |= ImGui::DragInt("最大长度", &node->MaxLength, 1.0F, 0, 4096);
|
||||
}
|
||||
if (isList) Dirty_ |= ImGui::DragInt("选中索引", &node->SelectedIndex, 1.0F, -1, 100000);
|
||||
if (isList || isScroll) Dirty_ |= ImGui::Checkbox("裁剪子节点", &node->RectTransform.ClipChildren);
|
||||
|
||||
ImGui::Separator();
|
||||
if (isButton) {
|
||||
bool interactable = node->Interactable;
|
||||
@ -5370,9 +5452,39 @@ private:
|
||||
}
|
||||
Dirty_ |= DrawNativeString("按钮 Action", node->Action, 128);
|
||||
}
|
||||
if (isText || isButton) {
|
||||
Dirty_ |= DrawNativeString("数据绑定", node->DataBinding, 128);
|
||||
Dirty_ |= DrawNativeString("绑定格式", node->DataFormat, 128);
|
||||
if (isText || isButton || isInput || isList) {
|
||||
if (node->Bindings.empty() && ImGui::Button("添加类型化绑定")) {
|
||||
node->Bindings.push_back({"script.value",
|
||||
isInput ? MetaCoreUiBindingTarget::Value : isList ? MetaCoreUiBindingTarget::Items : MetaCoreUiBindingTarget::Text,
|
||||
isInput ? MetaCoreUiBindingMode::TwoWay : MetaCoreUiBindingMode::OneWay, {}, {}, {}});
|
||||
Dirty_ = true;
|
||||
}
|
||||
for (std::size_t index = 0; index < node->Bindings.size(); ++index) {
|
||||
ImGui::PushID(static_cast<int>(index));
|
||||
ImGui::SeparatorText("绑定");
|
||||
auto& binding = node->Bindings[index];
|
||||
Dirty_ |= DrawNativeString("Source Path", binding.SourcePath, 192);
|
||||
const char* targetLabels[] = {"Text", "Value", "Visible", "Enabled", "Items", "SelectedIndex"};
|
||||
int target = static_cast<int>(binding.Target);
|
||||
if (ImGui::Combo("Target", &target, targetLabels, IM_ARRAYSIZE(targetLabels))) {
|
||||
binding.Target = static_cast<MetaCoreUiBindingTarget>(std::clamp(target, 0, 5));
|
||||
Dirty_ = true;
|
||||
}
|
||||
bool twoWay = binding.Mode == MetaCoreUiBindingMode::TwoWay;
|
||||
if (ImGui::Checkbox("双向", &twoWay)) {
|
||||
binding.Mode = twoWay ? MetaCoreUiBindingMode::TwoWay : MetaCoreUiBindingMode::OneWay;
|
||||
Dirty_ = true;
|
||||
}
|
||||
Dirty_ |= DrawNativeString("Format", binding.Format, 96);
|
||||
Dirty_ |= DrawNativeString("Fallback", binding.Fallback, 128);
|
||||
if (ImGui::SmallButton("删除绑定")) {
|
||||
node->Bindings.erase(node->Bindings.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
Dirty_ = true;
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
if (ImGui::Button("删除节点")) {
|
||||
DeleteNativeNode(node->Id);
|
||||
@ -5430,7 +5542,7 @@ private:
|
||||
void AddNativeNode(MetaCoreUiNodeType type) {
|
||||
MetaCoreUiNodeDocument node;
|
||||
node.Type = type;
|
||||
node.Id = MetaCoreMakeUniqueUiNodeId(Document_, type == MetaCoreUiNodeType::Button ? "Button" : type == MetaCoreUiNodeType::Text ? "Text" : type == MetaCoreUiNodeType::Image ? "Image" : "Panel");
|
||||
node.Id = MetaCoreMakeUniqueUiNodeId(Document_, MetaCoreUiNodeTypeLabel(type));
|
||||
node.Name = node.Id;
|
||||
node.RectTransform.Position = glm::vec3(80.0F + Document_.Nodes.size() * 12.0F, 80.0F + Document_.Nodes.size() * 12.0F, 0.0F);
|
||||
node.RectTransform.Size = type == MetaCoreUiNodeType::Panel ? glm::vec3(320.0F, 180.0F, 0.0F) : glm::vec3(180.0F, 44.0F, 0.0F);
|
||||
@ -5438,7 +5550,8 @@ private:
|
||||
ApplyNativeDefaultStylePreset(node);
|
||||
if (!SelectedNodeId_.empty()) {
|
||||
const MetaCoreUiNodeDocument* selectedNode = MetaCoreFindUiNode(Document_, SelectedNodeId_);
|
||||
if (selectedNode != nullptr && selectedNode->Type == MetaCoreUiNodeType::Panel) {
|
||||
if (selectedNode != nullptr && (selectedNode->Type == MetaCoreUiNodeType::Panel ||
|
||||
selectedNode->Type == MetaCoreUiNodeType::List || selectedNode->Type == MetaCoreUiNodeType::Scroll)) {
|
||||
node.ParentId = SelectedNodeId_;
|
||||
}
|
||||
}
|
||||
@ -5703,8 +5816,28 @@ private:
|
||||
rcssPath.replace_extension(".rcss");
|
||||
const MetaCoreNativeUiImageSourceMap imageSources =
|
||||
PrepareNativeImageRuntimeSources(editorContext, assetDatabaseService, generatedDirectory);
|
||||
const bool wroteRml = MetaCoreWriteTextFile(project.UiPath / rmlPath, MetaCoreCompileUiDocumentToRml(Document_, rcssPath.filename(), imageSources));
|
||||
const bool wroteRcss = MetaCoreWriteTextFile(project.UiPath / rcssPath, MetaCoreBuildNativeUiRcssDocument(Document_));
|
||||
const MetaCoreCompiledUiDocument compiled = MetaCoreCompileUiDocument(Document_);
|
||||
if (!compiled.Succeeded) {
|
||||
for (const MetaCoreUiDiagnostic& diagnostic : compiled.Diagnostics)
|
||||
editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "UI", diagnostic.NodeId + ": " + diagnostic.Message);
|
||||
return;
|
||||
}
|
||||
std::string rml = compiled.Rml;
|
||||
const std::string generatedStylesheet = "document.rcss";
|
||||
const std::string actualStylesheet = rcssPath.filename().generic_string();
|
||||
if (const std::size_t position = rml.find(generatedStylesheet); position != std::string::npos)
|
||||
rml.replace(position, generatedStylesheet.size(), actualStylesheet);
|
||||
for (const auto& [guid, imagePath] : imageSources) {
|
||||
const std::string generatedPath = "Assets/" + guid.ToString() + ".png";
|
||||
std::size_t position = 0;
|
||||
while ((position = rml.find(generatedPath, position)) != std::string::npos) {
|
||||
const std::string actualPath = imagePath.generic_string();
|
||||
rml.replace(position, generatedPath.size(), actualPath);
|
||||
position += actualPath.size();
|
||||
}
|
||||
}
|
||||
const bool wroteRml = MetaCoreWriteTextFile(project.UiPath / rmlPath, rml);
|
||||
const bool wroteRcss = MetaCoreWriteTextFile(project.UiPath / rcssPath, compiled.Rcss);
|
||||
if (!wroteRml || !wroteRcss) {
|
||||
editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "UI", "导出运行时 RML 失败");
|
||||
return;
|
||||
@ -10211,12 +10344,10 @@ public:
|
||||
moduleRegistry.RegisterMenuProvider(std::make_unique<MetaCoreDefaultMenuProvider>());
|
||||
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreHierarchyPanelProvider>());
|
||||
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreScenePanelProvider>());
|
||||
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreRuntimeUiPanelProvider>());
|
||||
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreInputMapPanelProvider>());
|
||||
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreAnimationToolsPanelProvider>());
|
||||
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCorePhysicsSettingsPanelProvider>());
|
||||
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreRenderingSettingsPanelProvider>());
|
||||
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreRmlUiVisualEditorPanelProvider>());
|
||||
moduleRegistry.RegisterService<MetaCoreIUiDesignerService>(std::make_shared<MetaCoreNativeUiDesignerService>());
|
||||
// The central Scene / Game viewport is not a dockable provider, but it keeps
|
||||
// its existing visibility state separate from the scene settings panel.
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
|
||||
namespace MetaCore {
|
||||
|
||||
@ -14,7 +15,14 @@ namespace {
|
||||
}
|
||||
|
||||
input.seekg(0, std::ios::end);
|
||||
const auto size = static_cast<std::size_t>(input.tellg());
|
||||
const std::streampos end = input.tellg();
|
||||
if (end < std::streampos{0}) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto size = static_cast<std::size_t>(end);
|
||||
if (size > static_cast<std::size_t>(std::numeric_limits<std::streamsize>::max())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
input.seekg(0, std::ios::beg);
|
||||
|
||||
std::vector<std::byte> bytes(size);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "MetaCoreFoundation/MetaCoreId.h"
|
||||
#include "MetaCoreFoundation/MetaCoreAssetGuid.h"
|
||||
#include "MetaCoreFoundation/MetaCoreReflection.h"
|
||||
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h"
|
||||
|
||||
@ -148,6 +149,29 @@ struct MetaCoreRuntimeUiDocumentEntry {
|
||||
|
||||
MC_PROPERTY()
|
||||
bool InputEnabled = true;
|
||||
|
||||
// Schema v2 fields. RmlPath remains readable for legacy manifests only.
|
||||
// Runtime convenience value. The string below is the serialized ABI field so
|
||||
// RuntimeData documents remain readable without registering Foundation types.
|
||||
MetaCoreAssetGuid UiDocumentAssetGuid{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string UiDocumentAssetGuidString{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::uint64_t InstanceId = 0;
|
||||
|
||||
MC_PROPERTY()
|
||||
std::uint64_t SceneObjectId = 0;
|
||||
|
||||
MC_PROPERTY()
|
||||
std::uint32_t RenderMode = 0;
|
||||
|
||||
MC_PROPERTY()
|
||||
std::int32_t TargetWidth = 0;
|
||||
|
||||
MC_PROPERTY()
|
||||
std::int32_t TargetHeight = 0;
|
||||
};
|
||||
|
||||
MC_STRUCT()
|
||||
@ -156,6 +180,12 @@ struct MetaCoreRuntimeUiManifest {
|
||||
|
||||
MC_PROPERTY()
|
||||
std::vector<MetaCoreRuntimeUiDocumentEntry> Documents{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::uint32_t SchemaVersion = 2;
|
||||
|
||||
MC_PROPERTY()
|
||||
std::uint32_t GeneratorVersion = 1;
|
||||
};
|
||||
|
||||
[[nodiscard]] bool MetaCoreWriteRuntimeDataSourcesDocument(
|
||||
|
||||
@ -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<MetaCoreRuntimeUiValueType>(value.Type);
|
||||
result.BoolValue = value.BoolValue;
|
||||
result.Int64Value = value.Int64Value;
|
||||
result.DoubleValue = value.DoubleValue;
|
||||
result.StringValue = value.StringValue;
|
||||
result.Vec3Value = value.Vec3Value;
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] MetaCoreRuntimeUiEventType EventTypeFromRml(std::string_view type) {
|
||||
if (type == "mouseover") return MetaCoreRuntimeUiEventType::PointerEnter;
|
||||
if (type == "mouseout") return MetaCoreRuntimeUiEventType::PointerLeave;
|
||||
if (type == "submit") return MetaCoreRuntimeUiEventType::Submit;
|
||||
if (type == "change") return MetaCoreRuntimeUiEventType::ValueChanged;
|
||||
if (type == "scroll") return MetaCoreRuntimeUiEventType::Scroll;
|
||||
if (type == "focus") return MetaCoreRuntimeUiEventType::Focus;
|
||||
if (type == "blur") return MetaCoreRuntimeUiEventType::Blur;
|
||||
return MetaCoreRuntimeUiEventType::Click;
|
||||
}
|
||||
|
||||
[[nodiscard]] Rml::Input::KeyIdentifier MetaCoreMapGlfwKeyToRml(int key) {
|
||||
if (key >= GLFW_KEY_0 && key <= GLFW_KEY_9) {
|
||||
return static_cast<Rml::Input::KeyIdentifier>(Rml::Input::KI_0 + (key - GLFW_KEY_0));
|
||||
@ -541,13 +578,44 @@ public:
|
||||
Rml::Element* target = event.GetTargetElement();
|
||||
if (target == nullptr) return;
|
||||
const std::string action = target->GetAttribute<Rml::String>("data-metacore-action", "");
|
||||
if (action.empty()) return;
|
||||
MetaCoreRuntimeUiEvent uiEvent;
|
||||
const auto state = std::find_if(Owner_.Documents_.begin(), Owner_.Documents_.end(), [&](const DocumentState& value) {
|
||||
return value.Entry.DocumentId == DocumentId_;
|
||||
});
|
||||
if (state != Owner_.Documents_.end()) {
|
||||
uiEvent.InstanceId = state->Entry.InstanceId;
|
||||
uiEvent.DocumentAssetGuid = state->Entry.UiDocumentAssetGuid;
|
||||
uiEvent.SceneObjectId = state->Entry.SceneObjectId;
|
||||
}
|
||||
uiEvent.DocumentId = DocumentId_;
|
||||
uiEvent.ElementId = target->GetId();
|
||||
uiEvent.Action = action;
|
||||
uiEvent.Type = event.GetType();
|
||||
uiEvent.Value = target->GetAttribute<Rml::String>("value", "");
|
||||
uiEvent.EventType = EventTypeFromRml(uiEvent.Type);
|
||||
uiEvent.InputSource = Owner_.CurrentInputSource_;
|
||||
uiEvent.Consumed = !event.IsPropagating();
|
||||
const std::string writePath = target->GetAttribute<Rml::String>("data-metacore-bind-value", "");
|
||||
const std::string writeMode = target->GetAttribute<Rml::String>("data-metacore-bind-mode-value", "");
|
||||
if (uiEvent.EventType == MetaCoreRuntimeUiEventType::ValueChanged &&
|
||||
writeMode == "two-way" && !writePath.empty()) {
|
||||
uiEvent.Action = writePath;
|
||||
if (!writePath.starts_with("script.") && !writePath.starts_with("scene.")) {
|
||||
uiEvent.EventType = MetaCoreRuntimeUiEventType::WriteRequest;
|
||||
}
|
||||
const auto previous = Owner_.Values_.find(writePath);
|
||||
if (previous != Owner_.Values_.end()) uiEvent.OldValue = FormatUiValue(previous->second, {});
|
||||
MetaCoreRuntimeUiValue value;
|
||||
value.Type = MetaCoreRuntimeUiValueType::String;
|
||||
value.StringValue = uiEvent.Value;
|
||||
Owner_.Values_[writePath] = std::move(value);
|
||||
}
|
||||
if (uiEvent.Action.empty() &&
|
||||
uiEvent.EventType != MetaCoreRuntimeUiEventType::Focus &&
|
||||
uiEvent.EventType != MetaCoreRuntimeUiEventType::Blur &&
|
||||
uiEvent.EventType != MetaCoreRuntimeUiEventType::PointerEnter &&
|
||||
uiEvent.EventType != MetaCoreRuntimeUiEventType::PointerLeave &&
|
||||
uiEvent.EventType != MetaCoreRuntimeUiEventType::Scroll) return;
|
||||
Owner_.Events_.push_back(uiEvent);
|
||||
if (Owner_.Callback_) Owner_.Callback_(uiEvent);
|
||||
}
|
||||
@ -565,10 +633,15 @@ public:
|
||||
Rml::Context* Context_ = nullptr;
|
||||
std::vector<DocumentState> Documents_;
|
||||
std::vector<std::unique_ptr<EventListener>> Listeners_;
|
||||
std::unordered_map<std::string, MetaCoreRuntimeDataValue> Values_;
|
||||
std::unordered_map<std::string, MetaCoreRuntimeUiValue> Values_;
|
||||
std::unordered_map<std::string, std::vector<MetaCoreRuntimeUiRow>> Lists_;
|
||||
std::vector<MetaCoreRuntimeUiEvent> Events_;
|
||||
std::vector<std::string> Errors_;
|
||||
std::vector<MetaCoreUiDiagnostic> Diagnostics_;
|
||||
EventCallback Callback_;
|
||||
MetaCoreRuntimeUiInputSource CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Programmatic;
|
||||
std::string Locale_{"en-US"};
|
||||
std::string Theme_{};
|
||||
bool Initialized_ = false;
|
||||
|
||||
[[nodiscard]] bool HasInputEnabledDocument() const {
|
||||
@ -577,20 +650,69 @@ public:
|
||||
});
|
||||
}
|
||||
|
||||
void ApplyBindings() {
|
||||
void ApplyBindings(const std::string* onlyPath = nullptr) {
|
||||
for (DocumentState& state : Documents_) {
|
||||
if (state.Document == nullptr) continue;
|
||||
Rml::ElementList elements;
|
||||
state.Document->QuerySelectorAll(elements, "[data-metacore-bind]");
|
||||
for (Rml::Element* element : elements) {
|
||||
const std::string dataPointId = element->GetAttribute<Rml::String>("data-metacore-bind", "");
|
||||
const auto value = Values_.find(dataPointId);
|
||||
for (const std::string target : {"text", "value", "visible", "enabled", "selected-index"}) {
|
||||
Rml::ElementList elements;
|
||||
state.Document->QuerySelectorAll(elements, "[data-metacore-bind-" + target + "]");
|
||||
for (Rml::Element* element : elements) {
|
||||
const std::string path = element->GetAttribute<Rml::String>("data-metacore-bind-" + target, "");
|
||||
if (onlyPath != nullptr && path != *onlyPath) continue;
|
||||
const auto value = Values_.find(path);
|
||||
if (value == Values_.end()) {
|
||||
const std::string fallback = element->GetAttribute<Rml::String>("data-metacore-fallback-" + target, "");
|
||||
if (fallback.empty()) continue;
|
||||
if (target == "text") element->SetInnerRML(EscapeRmlText(fallback));
|
||||
else element->SetAttribute(target == "enabled" ? "disabled" : target, fallback);
|
||||
continue;
|
||||
}
|
||||
const std::string format = element->GetAttribute<Rml::String>("data-metacore-format-" + target, "");
|
||||
const std::string text = FormatUiValue(value->second, format);
|
||||
if (target == "text") element->SetInnerRML(EscapeRmlText(text));
|
||||
else if (target == "value") element->SetAttribute("value", text);
|
||||
else if (target == "visible") {
|
||||
if (value->second.BoolValue) element->RemoveAttribute("hidden"); else element->SetAttribute("hidden", "true");
|
||||
} else if (target == "enabled") {
|
||||
if (value->second.BoolValue) element->RemoveAttribute("disabled"); else element->SetAttribute("disabled", "true");
|
||||
} else element->SetAttribute("data-metacore-selected-index", text);
|
||||
}
|
||||
}
|
||||
Rml::ElementList legacyElements;
|
||||
state.Document->QuerySelectorAll(legacyElements, "[data-metacore-bind]");
|
||||
for (Rml::Element* element : legacyElements) {
|
||||
const std::string path = element->GetAttribute<Rml::String>("data-metacore-bind", "");
|
||||
if (onlyPath != nullptr && path != *onlyPath) continue;
|
||||
const auto value = Values_.find(path);
|
||||
if (value == Values_.end()) continue;
|
||||
const std::string format = element->GetAttribute<Rml::String>("data-metacore-format", "");
|
||||
element->SetInnerRML(FormatValue(value->second, format));
|
||||
element->SetInnerRML(EscapeRmlText(FormatUiValue(value->second,
|
||||
element->GetAttribute<Rml::String>("data-metacore-format", ""))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool LoadEntry(const MetaCoreRuntimeUiDocumentEntry& entry) {
|
||||
if (Context_ == nullptr) return false;
|
||||
std::filesystem::path relativePath = entry.RmlPath;
|
||||
if (relativePath.empty() && entry.UiDocumentAssetGuid.IsValid())
|
||||
relativePath = std::filesystem::path("Compiled") / entry.UiDocumentAssetGuid.ToString() / "document.rml";
|
||||
const std::filesystem::path absolutePath = UiRoot_ / relativePath;
|
||||
if (entry.DocumentId.empty() || relativePath.empty() || !IsPathInsideRoot(UiRoot_, absolutePath)) {
|
||||
Errors_.push_back("Invalid UI manifest entry: " + entry.DocumentId);
|
||||
return false;
|
||||
}
|
||||
Rml::ElementDocument* document = Context_->LoadDocument(absolutePath.string());
|
||||
if (document == nullptr) {
|
||||
Errors_.push_back("Failed to load RML: " + relativePath.generic_string());
|
||||
return false;
|
||||
}
|
||||
if (entry.Visible) document->Show(); else document->Hide();
|
||||
Listeners_.push_back(std::make_unique<EventListener>(*this, entry.DocumentId));
|
||||
for (const char* type : {"mouseover", "mouseout", "click", "submit", "change", "scroll", "focus", "blur"})
|
||||
document->AddEventListener(type, Listeners_.back().get());
|
||||
Documents_.push_back({entry, document});
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
MetaCoreRuntimeUiSystem::MetaCoreRuntimeUiSystem() : Impl_(std::make_unique<Impl>()) {}
|
||||
@ -599,6 +721,11 @@ MetaCoreRuntimeUiSystem::~MetaCoreRuntimeUiSystem() { Shutdown(); }
|
||||
bool MetaCoreRuntimeUiSystem::Initialize(MetaCoreWindow& window, const std::filesystem::path&, const std::filesystem::path& uiRoot, const MetaCoreRuntimeUiManifest& manifest) {
|
||||
Shutdown();
|
||||
if (uiRoot.empty() || !std::filesystem::is_directory(uiRoot)) return false;
|
||||
if (manifest.SchemaVersion > METACORE_UI_SCHEMA_VERSION ||
|
||||
manifest.GeneratorVersion > METACORE_UI_GENERATOR_VERSION) {
|
||||
Impl_->Errors_.push_back("Runtime UI manifest version is newer than this engine");
|
||||
return false;
|
||||
}
|
||||
Impl_->Window_ = &window;
|
||||
Impl_->UiRoot_ = std::filesystem::weakly_canonical(uiRoot);
|
||||
Impl_->RenderInterface_.SetUiRoot(Impl_->UiRoot_);
|
||||
@ -617,23 +744,13 @@ bool MetaCoreRuntimeUiSystem::Initialize(MetaCoreWindow& window, const std::file
|
||||
|
||||
std::vector<MetaCoreRuntimeUiDocumentEntry> entries = manifest.Documents;
|
||||
std::sort(entries.begin(), entries.end(), [](const auto& lhs, const auto& rhs) { return lhs.Layer < rhs.Layer; });
|
||||
for (const MetaCoreRuntimeUiDocumentEntry& entry : entries) {
|
||||
const std::filesystem::path absolutePath = Impl_->UiRoot_ / entry.RmlPath;
|
||||
if (entry.DocumentId.empty() || entry.RmlPath.empty() || !IsPathInsideRoot(Impl_->UiRoot_, absolutePath)) {
|
||||
Impl_->Errors_.push_back("Invalid UI manifest entry: " + entry.DocumentId);
|
||||
continue;
|
||||
}
|
||||
Rml::ElementDocument* document = Impl_->Context_->LoadDocument(absolutePath.string());
|
||||
if (document == nullptr) {
|
||||
Impl_->Errors_.push_back("Failed to load RML: " + entry.RmlPath.generic_string());
|
||||
continue;
|
||||
}
|
||||
if (entry.Visible) document->Show(); else document->Hide();
|
||||
Impl_->Listeners_.push_back(std::make_unique<Impl::EventListener>(*Impl_, entry.DocumentId));
|
||||
document->AddEventListener("click", Impl_->Listeners_.back().get());
|
||||
document->AddEventListener("submit", Impl_->Listeners_.back().get());
|
||||
document->AddEventListener("change", Impl_->Listeners_.back().get());
|
||||
Impl_->Documents_.push_back({entry, document});
|
||||
std::uint64_t generatedInstanceId = 1;
|
||||
for (MetaCoreRuntimeUiDocumentEntry& entry : entries) {
|
||||
if (!entry.UiDocumentAssetGuid.IsValid() && !entry.UiDocumentAssetGuidString.empty())
|
||||
if (const auto parsed = MetaCoreAssetGuid::Parse(entry.UiDocumentAssetGuidString); parsed.has_value())
|
||||
entry.UiDocumentAssetGuid = *parsed;
|
||||
if (entry.InstanceId == 0) entry.InstanceId = generatedInstanceId++;
|
||||
(void)Impl_->LoadEntry(entry);
|
||||
}
|
||||
Impl_->Initialized_ = true;
|
||||
return true;
|
||||
@ -660,6 +777,8 @@ void MetaCoreRuntimeUiSystem::Shutdown() {
|
||||
Impl_->FileInterface_.reset();
|
||||
Impl_->SystemInterface_.reset();
|
||||
Impl_->Values_.clear();
|
||||
Impl_->Lists_.clear();
|
||||
Impl_->Diagnostics_.clear();
|
||||
Impl_->Initialized_ = false;
|
||||
}
|
||||
|
||||
@ -677,6 +796,7 @@ MetaCoreInputConsumption MetaCoreRuntimeUiSystem::ProcessInput() {
|
||||
if (!Impl_->Initialized_ || Impl_->Context_ == nullptr || Impl_->Window_ == nullptr || !Impl_->HasInputEnabledDocument()) return consumed;
|
||||
MetaCoreInput& input = Impl_->Window_->GetInput();
|
||||
const glm::vec2 cursor = input.GetCursorPosition();
|
||||
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Mouse;
|
||||
consumed.Mouse = !Impl_->Context_->ProcessMouseMove(static_cast<int>(cursor.x), static_cast<int>(cursor.y), 0);
|
||||
for (int button = 0; button < 3; ++button) {
|
||||
const auto mouseButton = static_cast<MetaCoreMouseButton>(button);
|
||||
@ -685,21 +805,99 @@ MetaCoreInputConsumption MetaCoreRuntimeUiSystem::ProcessInput() {
|
||||
}
|
||||
if (std::abs(input.GetMouseWheelDelta()) > 0.001F) consumed.Mouse = !Impl_->Context_->ProcessMouseWheel(input.GetMouseWheelDelta(), 0) || consumed.Mouse;
|
||||
for (const MetaCoreKeyEvent& event : input.GetKeyEvents()) {
|
||||
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Keyboard;
|
||||
const Rml::Input::KeyIdentifier key = MetaCoreMapGlfwKeyToRml(event.Key);
|
||||
const int modifiers = MetaCoreMapGlfwModifiersToRml(event.Modifiers);
|
||||
const bool propagated = event.Pressed ? Impl_->Context_->ProcessKeyDown(key, modifiers) : Impl_->Context_->ProcessKeyUp(key, modifiers);
|
||||
consumed.Keyboard = !propagated || consumed.Keyboard;
|
||||
}
|
||||
for (const char32_t codePoint : input.GetTextInput()) consumed.Keyboard = !Impl_->Context_->ProcessTextInput(static_cast<Rml::Character>(codePoint)) || consumed.Keyboard;
|
||||
for (const char32_t codePoint : input.GetTextInput()) {
|
||||
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Keyboard;
|
||||
consumed.Keyboard = !Impl_->Context_->ProcessTextInput(static_cast<Rml::Character>(codePoint)) || consumed.Keyboard;
|
||||
}
|
||||
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Programmatic;
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void MetaCoreRuntimeUiSystem::ApplyDataUpdates(const std::vector<MetaCoreRuntimeDataUpdate>& updates) {
|
||||
if (!Impl_->Initialized_) return;
|
||||
for (const MetaCoreRuntimeDataUpdate& update : updates) Impl_->Values_[update.DataPointId] = update.Value;
|
||||
for (const MetaCoreRuntimeDataUpdate& update : updates) {
|
||||
const std::string path = update.DataPointId.starts_with("runtime.") ? update.DataPointId : "runtime." + update.DataPointId;
|
||||
Impl_->Values_[path] = ToUiValue(update.Value);
|
||||
Impl_->Values_[update.DataPointId] = ToUiValue(update.Value);
|
||||
}
|
||||
Impl_->ApplyBindings();
|
||||
}
|
||||
|
||||
bool MetaCoreRuntimeUiSystem::SetValue(const std::string& sourcePath, const MetaCoreRuntimeUiValue& value) {
|
||||
if (!Impl_->Initialized_ || sourcePath.empty()) return false;
|
||||
Impl_->Values_[sourcePath] = value;
|
||||
Impl_->ApplyBindings(&sourcePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<MetaCoreRuntimeUiValue> MetaCoreRuntimeUiSystem::GetValue(const std::string& sourcePath) const {
|
||||
const auto found = Impl_->Values_.find(sourcePath);
|
||||
return found == Impl_->Values_.end() ? std::nullopt : std::optional<MetaCoreRuntimeUiValue>(found->second);
|
||||
}
|
||||
|
||||
bool MetaCoreRuntimeUiSystem::SetListRows(const std::string& sourcePath, std::vector<MetaCoreRuntimeUiRow> rows) {
|
||||
if (!Impl_->Initialized_ || sourcePath.empty()) return false;
|
||||
Impl_->Lists_[sourcePath] = std::move(rows);
|
||||
for (auto& state : Impl_->Documents_) {
|
||||
if (state.Document == nullptr) continue;
|
||||
Rml::ElementList elements;
|
||||
state.Document->QuerySelectorAll(elements, "[data-metacore-bind-items]");
|
||||
for (Rml::Element* element : elements) {
|
||||
if (element->GetAttribute<Rml::String>("data-metacore-bind-items", "") != sourcePath) continue;
|
||||
std::ostringstream markup;
|
||||
std::size_t index = 0;
|
||||
for (const auto& row : Impl_->Lists_[sourcePath]) {
|
||||
markup << "<div class=\"mcui-list-row\" data-index=\"" << index++ << "\">";
|
||||
std::vector<std::string> keys;
|
||||
keys.reserve(row.size());
|
||||
for (const auto& [key, _] : row) keys.push_back(key);
|
||||
std::sort(keys.begin(), keys.end());
|
||||
for (const std::string& key : keys)
|
||||
markup << "<span data-field=\"" << EscapeRmlText(key) << "\">"
|
||||
<< EscapeRmlText(FormatUiValue(row.at(key), {})) << "</span>";
|
||||
markup << "</div>";
|
||||
}
|
||||
element->SetInnerRML(markup.str());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MetaCoreRuntimeUiSystem::RequestFocus(std::uint64_t instanceId, const std::string& elementId) {
|
||||
for (auto& state : Impl_->Documents_) {
|
||||
if (state.Entry.InstanceId != instanceId || state.Document == nullptr) continue;
|
||||
if (Rml::Element* element = state.Document->GetElementById(elementId); element != nullptr) {
|
||||
element->Focus();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MetaCoreRuntimeUiSystem::CreateInstance(const MetaCoreRuntimeUiDocumentEntry& entry) {
|
||||
if (!Impl_->Initialized_ || entry.InstanceId == 0 ||
|
||||
std::any_of(Impl_->Documents_.begin(), Impl_->Documents_.end(), [&](const auto& value) {
|
||||
return value.Entry.InstanceId == entry.InstanceId;
|
||||
})) return false;
|
||||
return Impl_->LoadEntry(entry);
|
||||
}
|
||||
|
||||
bool MetaCoreRuntimeUiSystem::DestroyInstance(std::uint64_t instanceId) {
|
||||
const auto found = std::find_if(Impl_->Documents_.begin(), Impl_->Documents_.end(), [&](const auto& value) {
|
||||
return value.Entry.InstanceId == instanceId;
|
||||
});
|
||||
if (found == Impl_->Documents_.end()) return false;
|
||||
if (found->Document != nullptr) found->Document->Close();
|
||||
Impl_->Documents_.erase(found);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MetaCoreRuntimeUiSystem::SetDocumentVisible(const std::string& documentId, bool visible) {
|
||||
for (Impl::DocumentState& state : Impl_->Documents_) {
|
||||
if (state.Entry.DocumentId != documentId || state.Document == nullptr) continue;
|
||||
@ -710,10 +908,25 @@ bool MetaCoreRuntimeUiSystem::SetDocumentVisible(const std::string& documentId,
|
||||
return false;
|
||||
}
|
||||
|
||||
void MetaCoreRuntimeUiSystem::SetLocale(std::string locale) {
|
||||
if (locale.empty()) return;
|
||||
Impl_->Locale_ = std::move(locale);
|
||||
for (auto& state : Impl_->Documents_) if (state.Document != nullptr) state.Document->SetAttribute("data-metacore-locale", Impl_->Locale_);
|
||||
}
|
||||
|
||||
void MetaCoreRuntimeUiSystem::SetTheme(std::string theme) {
|
||||
Impl_->Theme_ = std::move(theme);
|
||||
for (auto& state : Impl_->Documents_) if (state.Document != nullptr) state.Document->SetAttribute("data-metacore-theme", Impl_->Theme_);
|
||||
}
|
||||
|
||||
const std::string& MetaCoreRuntimeUiSystem::GetLocale() const { return Impl_->Locale_; }
|
||||
const std::string& MetaCoreRuntimeUiSystem::GetTheme() const { return Impl_->Theme_; }
|
||||
|
||||
std::vector<MetaCoreRuntimeUiEvent> MetaCoreRuntimeUiSystem::ConsumeEvents() { return std::exchange(Impl_->Events_, {}); }
|
||||
void MetaCoreRuntimeUiSystem::SetEventCallback(EventCallback callback) { Impl_->Callback_ = std::move(callback); }
|
||||
bool MetaCoreRuntimeUiSystem::IsInitialized() const { return Impl_->Initialized_; }
|
||||
std::size_t MetaCoreRuntimeUiSystem::GetLoadedDocumentCount() const { return Impl_->Documents_.size(); }
|
||||
const std::vector<std::string>& MetaCoreRuntimeUiSystem::GetErrors() const { return Impl_->Errors_; }
|
||||
std::vector<MetaCoreUiDiagnostic> MetaCoreRuntimeUiSystem::GetDiagnostics() const { return Impl_->Diagnostics_; }
|
||||
|
||||
} // namespace MetaCore
|
||||
|
||||
255
Source/MetaCoreRuntimeUi/Private/MetaCoreUiCompiler.cpp
Normal file
255
Source/MetaCoreRuntimeUi/Private/MetaCoreUiCompiler.cpp
Normal file
@ -0,0 +1,255 @@
|
||||
#include "MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace MetaCore {
|
||||
namespace {
|
||||
|
||||
std::string Escape(std::string_view value) {
|
||||
std::string result;
|
||||
result.reserve(value.size());
|
||||
for (char c : value) {
|
||||
switch (c) {
|
||||
case '&': result += "&"; 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<int>(std::clamp(value.x, 0.0F, 1.0F) * 255.0F) << ','
|
||||
<< static_cast<int>(std::clamp(value.y, 0.0F, 1.0F) * 255.0F) << ','
|
||||
<< static_cast<int>(std::clamp(value.z, 0.0F, 1.0F) * 255.0F) << ','
|
||||
<< std::fixed << std::setprecision(3) << std::clamp(alpha, 0.0F, 1.0F) << ')';
|
||||
return out.str();
|
||||
}
|
||||
|
||||
bool IsBindingPathValid(std::string_view path) {
|
||||
return path.starts_with("runtime.") || path.starts_with("script.") || path.starts_with("scene.");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MetaCoreUiDocument MetaCoreMigrateUiDocument(MetaCoreUiDocument document) {
|
||||
if (document.SchemaVersion >= METACORE_UI_SCHEMA_VERSION) {
|
||||
return document;
|
||||
}
|
||||
for (MetaCoreUiNodeDocument& node : document.Nodes) {
|
||||
if (!node.DataBinding.empty() && node.Bindings.empty()) {
|
||||
node.Bindings.push_back({
|
||||
node.DataBinding,
|
||||
node.Type == MetaCoreUiNodeType::Input ? MetaCoreUiBindingTarget::Value : MetaCoreUiBindingTarget::Text,
|
||||
node.Type == MetaCoreUiNodeType::Input ? MetaCoreUiBindingMode::TwoWay : MetaCoreUiBindingMode::OneWay,
|
||||
node.DataFormat,
|
||||
{},
|
||||
{}
|
||||
});
|
||||
}
|
||||
}
|
||||
document.SchemaVersion = METACORE_UI_SCHEMA_VERSION;
|
||||
document.GeneratorVersion = METACORE_UI_GENERATOR_VERSION;
|
||||
return document;
|
||||
}
|
||||
|
||||
std::vector<MetaCoreUiDiagnostic> MetaCoreValidateUiDocument(const MetaCoreUiDocument& document) {
|
||||
std::vector<MetaCoreUiDiagnostic> diagnostics;
|
||||
auto issue = [&](std::string node, std::string binding, std::string message) {
|
||||
diagnostics.push_back({document.Name, std::move(node), std::move(binding), std::move(message), true});
|
||||
};
|
||||
if (document.SchemaVersion > METACORE_UI_SCHEMA_VERSION) issue({}, {}, "UI document schema is newer than this engine");
|
||||
if (document.GeneratorVersion > METACORE_UI_GENERATOR_VERSION) issue({}, {}, "UI document generator is newer than this engine");
|
||||
if (document.ReferenceWidth <= 0 || document.ReferenceHeight <= 0) issue({}, {}, "Reference resolution must be positive");
|
||||
if (!std::isfinite(document.MatchWidthOrHeight) || document.MatchWidthOrHeight < 0.0F || document.MatchWidthOrHeight > 1.0F)
|
||||
issue({}, {}, "MatchWidthOrHeight must be in [0, 1]");
|
||||
if (document.ReferenceDpi <= 0.0F || document.MinScale <= 0.0F || document.MaxScale < document.MinScale)
|
||||
issue({}, {}, "Canvas DPI or scale limits are invalid");
|
||||
|
||||
std::unordered_map<std::string, const MetaCoreUiNodeDocument*> nodes;
|
||||
for (const MetaCoreUiNodeDocument& node : document.Nodes) {
|
||||
if (node.Id.empty()) issue({}, {}, "Every UI node requires a stable ID");
|
||||
else if (!nodes.emplace(node.Id, &node).second) issue(node.Id, {}, "Duplicate UI node ID");
|
||||
if (node.RectTransform.MinSize.x < 0.0F || node.RectTransform.MinSize.y < 0.0F ||
|
||||
node.RectTransform.MaxSize.x < node.RectTransform.MinSize.x ||
|
||||
node.RectTransform.MaxSize.y < node.RectTransform.MinSize.y)
|
||||
issue(node.Id, {}, "Node minimum/maximum size is invalid");
|
||||
if (node.Type == MetaCoreUiNodeType::Input && node.MaxLength < 0) issue(node.Id, {}, "Input MaxLength cannot be negative");
|
||||
for (const MetaCoreUiBindingDocument& binding : node.Bindings) {
|
||||
if (!IsBindingPathValid(binding.SourcePath)) issue(node.Id, binding.SourcePath, "Binding path must use runtime., script. or scene.");
|
||||
if (binding.Mode == MetaCoreUiBindingMode::TwoWay &&
|
||||
binding.Target != MetaCoreUiBindingTarget::Value &&
|
||||
binding.Target != MetaCoreUiBindingTarget::SelectedIndex)
|
||||
issue(node.Id, binding.SourcePath, "Two-way binding is only valid for Value or SelectedIndex");
|
||||
}
|
||||
}
|
||||
for (const std::string& root : document.RootNodeIds) {
|
||||
if (!nodes.contains(root)) issue(root, {}, "Root node does not exist");
|
||||
else if (!nodes.at(root)->ParentId.empty()) issue(root, {}, "Root node has a parent");
|
||||
}
|
||||
for (const MetaCoreUiNodeDocument& node : document.Nodes) {
|
||||
if (!node.ParentId.empty() && !nodes.contains(node.ParentId)) issue(node.Id, {}, "Parent node does not exist");
|
||||
std::unordered_set<std::string> children;
|
||||
for (const std::string& child : node.Children) {
|
||||
if (!children.insert(child).second) issue(node.Id, {}, "Child ID is duplicated");
|
||||
if (!nodes.contains(child)) issue(node.Id, {}, "Child node does not exist: " + child);
|
||||
else if (nodes.at(child)->ParentId != node.Id) issue(child, {}, "Parent/child relationship is inconsistent");
|
||||
}
|
||||
if (!node.ItemTemplateNodeId.empty() && !nodes.contains(node.ItemTemplateNodeId))
|
||||
issue(node.Id, {}, "List item template node does not exist");
|
||||
}
|
||||
enum class Visit { Visiting, Done };
|
||||
std::unordered_map<std::string, Visit> visited;
|
||||
std::function<void(const std::string&)> walk = [&](const std::string& id) {
|
||||
const auto state = visited.find(id);
|
||||
if (state != visited.end()) {
|
||||
if (state->second == Visit::Visiting) issue(id, {}, "UI hierarchy contains a cycle");
|
||||
return;
|
||||
}
|
||||
visited[id] = Visit::Visiting;
|
||||
const auto found = nodes.find(id);
|
||||
if (found != nodes.end()) for (const std::string& child : found->second->Children) walk(child);
|
||||
visited[id] = Visit::Done;
|
||||
};
|
||||
for (const std::string& root : document.RootNodeIds) walk(root);
|
||||
for (const auto& [id, _] : nodes) if (!visited.contains(id)) issue(id, {}, "Node is unreachable from RootNodeIds");
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
MetaCoreCompiledUiDocument MetaCoreCompileUiDocument(const MetaCoreUiDocument& source) {
|
||||
MetaCoreCompiledUiDocument result;
|
||||
const MetaCoreUiDocument document = MetaCoreMigrateUiDocument(source);
|
||||
result.Diagnostics = MetaCoreValidateUiDocument(document);
|
||||
if (std::any_of(result.Diagnostics.begin(), result.Diagnostics.end(), [](const auto& value) { return value.IsError; })) return result;
|
||||
|
||||
std::unordered_map<std::string, const MetaCoreUiNodeDocument*> nodes;
|
||||
for (const auto& node : document.Nodes) {
|
||||
nodes[node.Id] = &node;
|
||||
if (node.Style.ImageAssetGuid.IsValid()) result.Dependencies.push_back(node.Style.ImageAssetGuid);
|
||||
}
|
||||
for (MetaCoreAssetGuid guid : {document.ThemeAssetGuid, document.FontFamilyAssetGuid, document.LocalizationAssetGuid})
|
||||
if (guid.IsValid()) result.Dependencies.push_back(guid);
|
||||
std::sort(result.Dependencies.begin(), result.Dependencies.end(), [](const auto& a, const auto& b) { return a.ToString() < b.ToString(); });
|
||||
result.Dependencies.erase(std::unique(result.Dependencies.begin(), result.Dependencies.end()), result.Dependencies.end());
|
||||
|
||||
std::ostringstream rml;
|
||||
rml << "<rml><head><link type=\"text/rcss\" href=\"document.rcss\"/></head><body"
|
||||
<< " data-metacore-schema=\"" << METACORE_UI_SCHEMA_VERSION << "\""
|
||||
<< " data-metacore-scale-mode=\"" << static_cast<int>(document.ScaleMode) << "\""
|
||||
<< " data-metacore-reference=\"" << document.ReferenceWidth << 'x' << document.ReferenceHeight << "\""
|
||||
<< " data-metacore-match=\"" << document.MatchWidthOrHeight << "\""
|
||||
<< " data-metacore-reference-dpi=\"" << document.ReferenceDpi << "\""
|
||||
<< " data-metacore-safe-area=\"" << (document.UseSafeArea ? "true" : "false") << "\">\n";
|
||||
|
||||
std::function<void(const std::string&, int)> emit = [&](const std::string& id, int depth) {
|
||||
const MetaCoreUiNodeDocument& node = *nodes.at(id);
|
||||
const std::string indent(static_cast<std::size_t>(depth), ' ');
|
||||
const char* tag = "div";
|
||||
if (node.Type == MetaCoreUiNodeType::Button) tag = "button";
|
||||
else if (node.Type == MetaCoreUiNodeType::Input) tag = "input";
|
||||
else if (node.Type == MetaCoreUiNodeType::Image) tag = "img";
|
||||
rml << indent << '<' << tag << " id=\"" << Escape(node.Id) << "\" class=\"mcui mcui-" << NodeClass(node.Type);
|
||||
if (!node.Style.ThemeToken.empty()) rml << " theme-" << Escape(node.Style.ThemeToken);
|
||||
rml << "\" data-metacore-kind=\"" << NodeClass(node.Type) << "\"";
|
||||
if (!node.Action.empty()) rml << " data-metacore-action=\"" << Escape(node.Action) << "\"";
|
||||
if (node.NavigationOrder >= 0) rml << " tabindex=\"" << node.NavigationOrder << "\"";
|
||||
if (!node.LocalizationKey.empty()) rml << " data-metacore-loc=\"" << Escape(node.LocalizationKey) << "\"";
|
||||
if (!node.ShowAnimation.empty()) rml << " data-metacore-show-animation=\"" << Escape(node.ShowAnimation) << "\"";
|
||||
if (!node.HideAnimation.empty()) rml << " data-metacore-hide-animation=\"" << Escape(node.HideAnimation) << "\"";
|
||||
for (const auto& binding : node.Bindings) {
|
||||
rml << " data-metacore-bind-" << BindingTargetName(binding.Target) << "=\"" << Escape(binding.SourcePath) << "\""
|
||||
<< " data-metacore-bind-mode-" << BindingTargetName(binding.Target) << "=\""
|
||||
<< (binding.Mode == MetaCoreUiBindingMode::TwoWay ? "two-way" : "one-way") << "\"";
|
||||
if (!binding.Format.empty()) rml << " data-metacore-format-" << BindingTargetName(binding.Target) << "=\"" << Escape(binding.Format) << "\"";
|
||||
if (!binding.Fallback.empty()) rml << " data-metacore-fallback-" << BindingTargetName(binding.Target) << "=\"" << Escape(binding.Fallback) << "\"";
|
||||
}
|
||||
if (node.Bindings.empty() && !node.DataBinding.empty()) {
|
||||
rml << " data-metacore-bind-text=\"" << Escape(node.DataBinding) << "\" data-metacore-format-text=\"" << Escape(node.DataFormat) << "\"";
|
||||
}
|
||||
if (!node.Visible) rml << " hidden=\"true\"";
|
||||
if (!node.Interactable && (node.Type == MetaCoreUiNodeType::Button || node.Type == MetaCoreUiNodeType::Input)) rml << " disabled=\"true\"";
|
||||
if (node.Type == MetaCoreUiNodeType::Input) {
|
||||
rml << " value=\"" << Escape(node.Text) << "\" placeholder=\"" << Escape(node.Placeholder) << "\"";
|
||||
if (node.ReadOnly) rml << " readonly=\"true\"";
|
||||
if (node.MaxLength > 0) rml << " maxlength=\"" << node.MaxLength << "\"";
|
||||
rml << " type=\"" << (node.InputValueType == MetaCoreUiInputValueType::Text ? "text" : "number") << "\"/>\n";
|
||||
return;
|
||||
}
|
||||
if (node.Type == MetaCoreUiNodeType::Image && node.Style.ImageAssetGuid.IsValid())
|
||||
rml << " src=\"Assets/" << node.Style.ImageAssetGuid.ToString() << ".png\"";
|
||||
if (node.Type == MetaCoreUiNodeType::List) rml << " role=\"listbox\" data-metacore-selected-index=\"" << node.SelectedIndex << "\"";
|
||||
rml << '>';
|
||||
if (node.Type == MetaCoreUiNodeType::Text || node.Type == MetaCoreUiNodeType::Button) rml << Escape(node.Text);
|
||||
if (!node.Children.empty()) rml << '\n';
|
||||
for (const std::string& child : node.Children) emit(child, depth + 2);
|
||||
if (!node.Children.empty()) rml << indent;
|
||||
rml << "</" << tag << ">\n";
|
||||
};
|
||||
for (const std::string& root : document.RootNodeIds) emit(root, 2);
|
||||
rml << "</body></rml>\n";
|
||||
|
||||
std::ostringstream css;
|
||||
css << "body { margin:0; width:100%; height:100%; background-color:" << Color(document.BackgroundColor, document.BackgroundAlpha) << "; }\n"
|
||||
<< ".mcui { position:absolute; box-sizing:border-box; }\n"
|
||||
<< ".mcui-button:focus, .mcui-input:focus, .mcui-list:focus { outline:2px #4da3ff; }\n"
|
||||
<< ".mcui-button:disabled, .mcui-input:disabled { opacity:0.45; }\n"
|
||||
<< ".mcui-scroll { overflow:auto; }\n.mcui-list { overflow:auto; }\n";
|
||||
for (const auto& node : document.Nodes) {
|
||||
const auto& rt = node.RectTransform;
|
||||
const float leftOffset = rt.Position.x - rt.Pivot.x * rt.Size.x;
|
||||
const float topOffset = rt.Position.y - rt.Pivot.y * rt.Size.y;
|
||||
const float anchorWidth = (rt.AnchorMax.x - rt.AnchorMin.x) * 100.0F;
|
||||
const float anchorHeight = (rt.AnchorMax.y - rt.AnchorMin.y) * 100.0F;
|
||||
css << '#' << node.Id << " { left:calc(" << rt.AnchorMin.x * 100.0F << "% + " << leftOffset
|
||||
<< "px); top:calc(" << rt.AnchorMin.y * 100.0F << "% + " << topOffset << "px); width:calc("
|
||||
<< anchorWidth << "% + " << rt.Size.x << "px); height:calc(" << anchorHeight << "% + " << rt.Size.y
|
||||
<< "px); min-width:" << rt.MinSize.x << "px; min-height:" << rt.MinSize.y << "px; max-width:"
|
||||
<< rt.MaxSize.x << "px; max-height:" << rt.MaxSize.y << "px; color:" << Color(node.Style.TextColor)
|
||||
<< "; background-color:" << Color(node.Style.BackgroundColor, node.Style.Opacity) << "; font-size:"
|
||||
<< node.Style.FontSize << "px; padding:" << node.Style.Padding.y << "px " << node.Style.Padding.x
|
||||
<< "px; border-radius:" << node.Style.BorderRadius << "px; overflow:"
|
||||
<< (rt.ClipChildren ? "hidden" : (node.Type == MetaCoreUiNodeType::Scroll || node.Type == MetaCoreUiNodeType::List ? "auto" : "visible"))
|
||||
<< "; }\n";
|
||||
}
|
||||
result.Rml = rml.str();
|
||||
result.Rcss = css.str();
|
||||
result.Succeeded = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace MetaCore
|
||||
@ -3,11 +3,14 @@
|
||||
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h"
|
||||
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h"
|
||||
#include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h"
|
||||
#include "MetaCoreScene/MetaCoreSceneDocument.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace MetaCore {
|
||||
@ -15,14 +18,87 @@ namespace MetaCore {
|
||||
class MetaCoreWindow;
|
||||
class MetaCoreEditorViewportRenderer;
|
||||
|
||||
inline constexpr std::uint32_t METACORE_UI_SCHEMA_VERSION = 2U;
|
||||
inline constexpr std::uint32_t METACORE_UI_GENERATOR_VERSION = 1U;
|
||||
|
||||
enum class MetaCoreRuntimeUiEventType : std::uint32_t {
|
||||
PointerEnter = 0,
|
||||
PointerLeave,
|
||||
Click,
|
||||
Submit,
|
||||
ValueChanged,
|
||||
SelectionChanged,
|
||||
Scroll,
|
||||
Focus,
|
||||
Blur,
|
||||
WriteRequest
|
||||
};
|
||||
|
||||
enum class MetaCoreRuntimeUiInputSource : std::uint32_t {
|
||||
Programmatic = 0,
|
||||
Mouse,
|
||||
Keyboard,
|
||||
WorldPointer
|
||||
};
|
||||
|
||||
enum class MetaCoreRuntimeUiValueType : std::uint32_t {
|
||||
Bool = 0,
|
||||
Int64,
|
||||
Double,
|
||||
String,
|
||||
Vec3,
|
||||
AssetRef,
|
||||
GameObjectRef
|
||||
};
|
||||
|
||||
struct MetaCoreRuntimeUiValue {
|
||||
MetaCoreRuntimeUiValueType Type = MetaCoreRuntimeUiValueType::String;
|
||||
bool BoolValue = false;
|
||||
std::int64_t Int64Value = 0;
|
||||
double DoubleValue = 0.0;
|
||||
std::string StringValue{};
|
||||
glm::vec3 Vec3Value{0.0F};
|
||||
MetaCoreAssetGuid AssetValue{};
|
||||
std::uint64_t ObjectValue = 0;
|
||||
};
|
||||
|
||||
using MetaCoreRuntimeUiRow = std::unordered_map<std::string, MetaCoreRuntimeUiValue>;
|
||||
|
||||
struct MetaCoreRuntimeUiEvent {
|
||||
std::uint64_t InstanceId = 0;
|
||||
MetaCoreAssetGuid DocumentAssetGuid{};
|
||||
std::uint64_t SceneObjectId = 0;
|
||||
std::string DocumentId{};
|
||||
std::string ElementId{};
|
||||
std::string Action{};
|
||||
std::string Type{};
|
||||
std::string Value{};
|
||||
std::string OldValue{};
|
||||
MetaCoreRuntimeUiEventType EventType = MetaCoreRuntimeUiEventType::Click;
|
||||
MetaCoreRuntimeUiInputSource InputSource = MetaCoreRuntimeUiInputSource::Programmatic;
|
||||
bool Consumed = false;
|
||||
};
|
||||
|
||||
struct MetaCoreUiDiagnostic {
|
||||
std::string Document{};
|
||||
std::string NodeId{};
|
||||
std::string BindingPath{};
|
||||
std::string Message{};
|
||||
bool IsError = true;
|
||||
};
|
||||
|
||||
struct MetaCoreCompiledUiDocument {
|
||||
std::string Rml{};
|
||||
std::string Rcss{};
|
||||
std::vector<MetaCoreAssetGuid> Dependencies{};
|
||||
std::vector<MetaCoreUiDiagnostic> Diagnostics{};
|
||||
bool Succeeded = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::vector<MetaCoreUiDiagnostic> MetaCoreValidateUiDocument(const MetaCoreUiDocument& document);
|
||||
[[nodiscard]] MetaCoreCompiledUiDocument MetaCoreCompileUiDocument(const MetaCoreUiDocument& document);
|
||||
[[nodiscard]] MetaCoreUiDocument MetaCoreMigrateUiDocument(MetaCoreUiDocument document);
|
||||
|
||||
class MetaCoreRuntimeUiSystem {
|
||||
public:
|
||||
using EventCallback = std::function<void(const MetaCoreRuntimeUiEvent&)>;
|
||||
@ -43,13 +119,24 @@ public:
|
||||
void Update(float deltaSeconds);
|
||||
[[nodiscard]] MetaCoreInputConsumption ProcessInput();
|
||||
void ApplyDataUpdates(const std::vector<MetaCoreRuntimeDataUpdate>& updates);
|
||||
[[nodiscard]] bool SetValue(const std::string& sourcePath, const MetaCoreRuntimeUiValue& value);
|
||||
[[nodiscard]] std::optional<MetaCoreRuntimeUiValue> GetValue(const std::string& sourcePath) const;
|
||||
[[nodiscard]] bool SetListRows(const std::string& sourcePath, std::vector<MetaCoreRuntimeUiRow> rows);
|
||||
[[nodiscard]] bool RequestFocus(std::uint64_t instanceId, const std::string& elementId);
|
||||
[[nodiscard]] bool CreateInstance(const MetaCoreRuntimeUiDocumentEntry& entry);
|
||||
[[nodiscard]] bool DestroyInstance(std::uint64_t instanceId);
|
||||
[[nodiscard]] bool SetDocumentVisible(const std::string& documentId, bool visible);
|
||||
void SetLocale(std::string locale);
|
||||
void SetTheme(std::string theme);
|
||||
[[nodiscard]] const std::string& GetLocale() const;
|
||||
[[nodiscard]] const std::string& GetTheme() const;
|
||||
[[nodiscard]] std::vector<MetaCoreRuntimeUiEvent> ConsumeEvents();
|
||||
void SetEventCallback(EventCallback callback);
|
||||
|
||||
[[nodiscard]] bool IsInitialized() const;
|
||||
[[nodiscard]] std::size_t GetLoadedDocumentCount() const;
|
||||
[[nodiscard]] const std::vector<std::string>& GetErrors() const;
|
||||
[[nodiscard]] std::vector<MetaCoreUiDiagnostic> GetDiagnostics() const;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
|
||||
@ -860,6 +860,9 @@ static json RectTransformToJson(const MetaCoreUiRectTransformDocument& trans) {
|
||||
j["Pivot"] = Vec3ToJson(trans.Pivot);
|
||||
j["Position"] = Vec3ToJson(trans.Position);
|
||||
j["Size"] = Vec3ToJson(trans.Size);
|
||||
j["MinSize"] = Vec3ToJson(trans.MinSize);
|
||||
j["MaxSize"] = Vec3ToJson(trans.MaxSize);
|
||||
j["ClipChildren"] = trans.ClipChildren;
|
||||
return j;
|
||||
}
|
||||
|
||||
@ -870,6 +873,9 @@ static MetaCoreUiRectTransformDocument JsonToRectTransform(const json& j) {
|
||||
if (j.contains("Pivot")) trans.Pivot = JsonToVec3(j["Pivot"]);
|
||||
if (j.contains("Position")) trans.Position = JsonToVec3(j["Position"]);
|
||||
if (j.contains("Size")) trans.Size = JsonToVec3(j["Size"]);
|
||||
if (j.contains("MinSize")) trans.MinSize = JsonToVec3(j["MinSize"]);
|
||||
if (j.contains("MaxSize")) trans.MaxSize = JsonToVec3(j["MaxSize"]);
|
||||
if (j.contains("ClipChildren")) trans.ClipChildren = j["ClipChildren"].get<bool>();
|
||||
return trans;
|
||||
}
|
||||
|
||||
@ -884,6 +890,9 @@ static json UiStyleToJson(const MetaCoreUiStyleDocument& style) {
|
||||
j["VerticalAlignment"] = static_cast<int>(style.VerticalAlignment);
|
||||
j["ImageAssetGuid"] = GuidToString(style.ImageAssetGuid);
|
||||
j["PreserveAspect"] = style.PreserveAspect;
|
||||
j["Opacity"] = style.Opacity;
|
||||
j["BorderRadius"] = style.BorderRadius;
|
||||
j["ThemeToken"] = style.ThemeToken;
|
||||
return j;
|
||||
}
|
||||
|
||||
@ -898,9 +907,34 @@ static MetaCoreUiStyleDocument JsonToUiStyle(const json& j) {
|
||||
if (j.contains("VerticalAlignment")) style.VerticalAlignment = static_cast<MetaCoreUiVerticalAlignment>(j["VerticalAlignment"].get<int>());
|
||||
if (j.contains("ImageAssetGuid")) style.ImageAssetGuid = StringToGuid(j["ImageAssetGuid"].get<std::string>());
|
||||
if (j.contains("PreserveAspect")) style.PreserveAspect = j["PreserveAspect"].get<bool>();
|
||||
if (j.contains("Opacity")) style.Opacity = j["Opacity"].get<float>();
|
||||
if (j.contains("BorderRadius")) style.BorderRadius = j["BorderRadius"].get<float>();
|
||||
if (j.contains("ThemeToken")) style.ThemeToken = j["ThemeToken"].get<std::string>();
|
||||
return style;
|
||||
}
|
||||
|
||||
static json UiBindingToJson(const MetaCoreUiBindingDocument& binding) {
|
||||
return json{
|
||||
{"SourcePath", binding.SourcePath},
|
||||
{"Target", static_cast<int>(binding.Target)},
|
||||
{"Mode", static_cast<int>(binding.Mode)},
|
||||
{"Format", binding.Format},
|
||||
{"Converter", binding.Converter},
|
||||
{"Fallback", binding.Fallback}
|
||||
};
|
||||
}
|
||||
|
||||
static MetaCoreUiBindingDocument JsonToUiBinding(const json& j) {
|
||||
MetaCoreUiBindingDocument binding;
|
||||
if (j.contains("SourcePath")) binding.SourcePath = j["SourcePath"].get<std::string>();
|
||||
if (j.contains("Target")) binding.Target = static_cast<MetaCoreUiBindingTarget>(j["Target"].get<int>());
|
||||
if (j.contains("Mode")) binding.Mode = static_cast<MetaCoreUiBindingMode>(j["Mode"].get<int>());
|
||||
if (j.contains("Format")) binding.Format = j["Format"].get<std::string>();
|
||||
if (j.contains("Converter")) binding.Converter = j["Converter"].get<std::string>();
|
||||
if (j.contains("Fallback")) binding.Fallback = j["Fallback"].get<std::string>();
|
||||
return binding;
|
||||
}
|
||||
|
||||
static json UiNodeToJson(const MetaCoreUiNodeDocument& node) {
|
||||
json j = json::object();
|
||||
j["Id"] = node.Id;
|
||||
@ -916,6 +950,18 @@ static json UiNodeToJson(const MetaCoreUiNodeDocument& node) {
|
||||
j["Action"] = node.Action;
|
||||
j["DataBinding"] = node.DataBinding;
|
||||
j["DataFormat"] = node.DataFormat;
|
||||
j["Bindings"] = json::array();
|
||||
for (const auto& binding : node.Bindings) j["Bindings"].push_back(UiBindingToJson(binding));
|
||||
j["NavigationOrder"] = node.NavigationOrder;
|
||||
j["Placeholder"] = node.Placeholder;
|
||||
j["InputValueType"] = static_cast<int>(node.InputValueType);
|
||||
j["ReadOnly"] = node.ReadOnly;
|
||||
j["MaxLength"] = node.MaxLength;
|
||||
j["SelectedIndex"] = node.SelectedIndex;
|
||||
j["ItemTemplateNodeId"] = node.ItemTemplateNodeId;
|
||||
j["LocalizationKey"] = node.LocalizationKey;
|
||||
j["ShowAnimation"] = node.ShowAnimation;
|
||||
j["HideAnimation"] = node.HideAnimation;
|
||||
return j;
|
||||
}
|
||||
|
||||
@ -934,6 +980,18 @@ static MetaCoreUiNodeDocument JsonToUiNode(const json& j) {
|
||||
if (j.contains("Action")) node.Action = j["Action"].get<std::string>();
|
||||
if (j.contains("DataBinding")) node.DataBinding = j["DataBinding"].get<std::string>();
|
||||
if (j.contains("DataFormat")) node.DataFormat = j["DataFormat"].get<std::string>();
|
||||
if (j.contains("Bindings") && j["Bindings"].is_array())
|
||||
for (const auto& binding : j["Bindings"]) node.Bindings.push_back(JsonToUiBinding(binding));
|
||||
if (j.contains("NavigationOrder")) node.NavigationOrder = j["NavigationOrder"].get<std::int32_t>();
|
||||
if (j.contains("Placeholder")) node.Placeholder = j["Placeholder"].get<std::string>();
|
||||
if (j.contains("InputValueType")) node.InputValueType = static_cast<MetaCoreUiInputValueType>(j["InputValueType"].get<int>());
|
||||
if (j.contains("ReadOnly")) node.ReadOnly = j["ReadOnly"].get<bool>();
|
||||
if (j.contains("MaxLength")) node.MaxLength = j["MaxLength"].get<std::int32_t>();
|
||||
if (j.contains("SelectedIndex")) node.SelectedIndex = j["SelectedIndex"].get<std::int32_t>();
|
||||
if (j.contains("ItemTemplateNodeId")) node.ItemTemplateNodeId = j["ItemTemplateNodeId"].get<std::string>();
|
||||
if (j.contains("LocalizationKey")) node.LocalizationKey = j["LocalizationKey"].get<std::string>();
|
||||
if (j.contains("ShowAnimation")) node.ShowAnimation = j["ShowAnimation"].get<std::string>();
|
||||
if (j.contains("HideAnimation")) node.HideAnimation = j["HideAnimation"].get<std::string>();
|
||||
return node;
|
||||
}
|
||||
|
||||
@ -945,9 +1003,22 @@ bool MetaCoreSceneSerializer::SaveUiToJson(
|
||||
(void)registry;
|
||||
try {
|
||||
json uiJson;
|
||||
uiJson["SchemaVersion"] = uiDocument.SchemaVersion;
|
||||
uiJson["GeneratorVersion"] = uiDocument.GeneratorVersion;
|
||||
uiJson["Name"] = uiDocument.Name;
|
||||
uiJson["ReferenceWidth"] = uiDocument.ReferenceWidth;
|
||||
uiJson["ReferenceHeight"] = uiDocument.ReferenceHeight;
|
||||
uiJson["ScaleMode"] = static_cast<int>(uiDocument.ScaleMode);
|
||||
uiJson["MatchWidthOrHeight"] = uiDocument.MatchWidthOrHeight;
|
||||
uiJson["ReferenceDpi"] = uiDocument.ReferenceDpi;
|
||||
uiJson["MinScale"] = uiDocument.MinScale;
|
||||
uiJson["MaxScale"] = uiDocument.MaxScale;
|
||||
uiJson["UseSafeArea"] = uiDocument.UseSafeArea;
|
||||
uiJson["ThemeAssetGuid"] = GuidToString(uiDocument.ThemeAssetGuid);
|
||||
uiJson["FontFamilyAssetGuid"] = GuidToString(uiDocument.FontFamilyAssetGuid);
|
||||
uiJson["LocalizationAssetGuid"] = GuidToString(uiDocument.LocalizationAssetGuid);
|
||||
uiJson["DefaultLocale"] = uiDocument.DefaultLocale;
|
||||
uiJson["FallbackLocale"] = uiDocument.FallbackLocale;
|
||||
uiJson["BackgroundColor"] = Vec3ToJson(uiDocument.BackgroundColor);
|
||||
uiJson["BackgroundAlpha"] = uiDocument.BackgroundAlpha;
|
||||
uiJson["RootNodeIds"] = uiDocument.RootNodeIds;
|
||||
@ -983,9 +1054,22 @@ std::optional<MetaCoreUiDocument> MetaCoreSceneSerializer::LoadUiFromJson(
|
||||
file >> uiJson;
|
||||
|
||||
MetaCoreUiDocument doc;
|
||||
if (uiJson.contains("SchemaVersion")) doc.SchemaVersion = uiJson["SchemaVersion"].get<std::uint32_t>(); else doc.SchemaVersion = 1;
|
||||
if (uiJson.contains("GeneratorVersion")) doc.GeneratorVersion = uiJson["GeneratorVersion"].get<std::uint32_t>();
|
||||
if (uiJson.contains("Name")) doc.Name = uiJson["Name"].get<std::string>();
|
||||
if (uiJson.contains("ReferenceWidth")) doc.ReferenceWidth = uiJson["ReferenceWidth"].get<std::int32_t>();
|
||||
if (uiJson.contains("ReferenceHeight")) doc.ReferenceHeight = uiJson["ReferenceHeight"].get<std::int32_t>();
|
||||
if (uiJson.contains("ScaleMode")) doc.ScaleMode = static_cast<MetaCoreUiCanvasScaleMode>(uiJson["ScaleMode"].get<int>());
|
||||
if (uiJson.contains("MatchWidthOrHeight")) doc.MatchWidthOrHeight = uiJson["MatchWidthOrHeight"].get<float>();
|
||||
if (uiJson.contains("ReferenceDpi")) doc.ReferenceDpi = uiJson["ReferenceDpi"].get<float>();
|
||||
if (uiJson.contains("MinScale")) doc.MinScale = uiJson["MinScale"].get<float>();
|
||||
if (uiJson.contains("MaxScale")) doc.MaxScale = uiJson["MaxScale"].get<float>();
|
||||
if (uiJson.contains("UseSafeArea")) doc.UseSafeArea = uiJson["UseSafeArea"].get<bool>();
|
||||
if (uiJson.contains("ThemeAssetGuid")) doc.ThemeAssetGuid = StringToGuid(uiJson["ThemeAssetGuid"].get<std::string>());
|
||||
if (uiJson.contains("FontFamilyAssetGuid")) doc.FontFamilyAssetGuid = StringToGuid(uiJson["FontFamilyAssetGuid"].get<std::string>());
|
||||
if (uiJson.contains("LocalizationAssetGuid")) doc.LocalizationAssetGuid = StringToGuid(uiJson["LocalizationAssetGuid"].get<std::string>());
|
||||
if (uiJson.contains("DefaultLocale")) doc.DefaultLocale = uiJson["DefaultLocale"].get<std::string>();
|
||||
if (uiJson.contains("FallbackLocale")) doc.FallbackLocale = uiJson["FallbackLocale"].get<std::string>();
|
||||
if (uiJson.contains("BackgroundColor")) doc.BackgroundColor = JsonToVec3(uiJson["BackgroundColor"]);
|
||||
if (uiJson.contains("BackgroundAlpha")) doc.BackgroundAlpha = uiJson["BackgroundAlpha"].get<float>();
|
||||
if (uiJson.contains("RootNodeIds")) doc.RootNodeIds = uiJson["RootNodeIds"].get<std::vector<std::string>>();
|
||||
|
||||
@ -98,7 +98,40 @@ enum class MetaCoreUiNodeType {
|
||||
Panel = 0,
|
||||
Text,
|
||||
Image,
|
||||
Button
|
||||
Button,
|
||||
Input,
|
||||
List,
|
||||
Scroll
|
||||
};
|
||||
|
||||
MC_ENUM()
|
||||
enum class MetaCoreUiCanvasScaleMode {
|
||||
ConstantPixelSize = 0,
|
||||
ScaleWithScreenSize,
|
||||
ConstantPhysicalSize
|
||||
};
|
||||
|
||||
MC_ENUM()
|
||||
enum class MetaCoreUiBindingMode {
|
||||
OneWay = 0,
|
||||
TwoWay
|
||||
};
|
||||
|
||||
MC_ENUM()
|
||||
enum class MetaCoreUiBindingTarget {
|
||||
Text = 0,
|
||||
Value,
|
||||
Visible,
|
||||
Enabled,
|
||||
Items,
|
||||
SelectedIndex
|
||||
};
|
||||
|
||||
MC_ENUM()
|
||||
enum class MetaCoreUiInputValueType {
|
||||
Text = 0,
|
||||
Integer,
|
||||
Number
|
||||
};
|
||||
|
||||
MC_ENUM()
|
||||
@ -135,6 +168,15 @@ struct MetaCoreUiRectTransformDocument {
|
||||
|
||||
MC_PROPERTY()
|
||||
glm::vec3 Size{100.0F, 100.0F, 0.0F};
|
||||
|
||||
MC_PROPERTY()
|
||||
glm::vec3 MinSize{0.0F, 0.0F, 0.0F};
|
||||
|
||||
MC_PROPERTY()
|
||||
glm::vec3 MaxSize{100000.0F, 100000.0F, 0.0F};
|
||||
|
||||
MC_PROPERTY()
|
||||
bool ClipChildren = false;
|
||||
};
|
||||
|
||||
MC_STRUCT()
|
||||
@ -167,6 +209,38 @@ struct MetaCoreUiStyleDocument {
|
||||
|
||||
MC_PROPERTY()
|
||||
bool PreserveAspect = false;
|
||||
|
||||
MC_PROPERTY()
|
||||
float Opacity = 1.0F;
|
||||
|
||||
MC_PROPERTY()
|
||||
float BorderRadius = 0.0F;
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string ThemeToken{};
|
||||
};
|
||||
|
||||
MC_STRUCT()
|
||||
struct MetaCoreUiBindingDocument {
|
||||
MC_GENERATED_BODY()
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string SourcePath{};
|
||||
|
||||
MC_PROPERTY()
|
||||
MetaCoreUiBindingTarget Target = MetaCoreUiBindingTarget::Text;
|
||||
|
||||
MC_PROPERTY()
|
||||
MetaCoreUiBindingMode Mode = MetaCoreUiBindingMode::OneWay;
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string Format{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string Converter{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string Fallback{};
|
||||
};
|
||||
|
||||
MC_STRUCT()
|
||||
@ -211,12 +285,51 @@ struct MetaCoreUiNodeDocument {
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string DataFormat{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::vector<MetaCoreUiBindingDocument> Bindings{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::int32_t NavigationOrder = -1;
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string Placeholder{};
|
||||
|
||||
MC_PROPERTY()
|
||||
MetaCoreUiInputValueType InputValueType = MetaCoreUiInputValueType::Text;
|
||||
|
||||
MC_PROPERTY()
|
||||
bool ReadOnly = false;
|
||||
|
||||
MC_PROPERTY()
|
||||
std::int32_t MaxLength = 0;
|
||||
|
||||
MC_PROPERTY()
|
||||
std::int32_t SelectedIndex = -1;
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string ItemTemplateNodeId{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string LocalizationKey{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string ShowAnimation{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string HideAnimation{};
|
||||
};
|
||||
|
||||
MC_STRUCT()
|
||||
struct MetaCoreUiDocument {
|
||||
MC_GENERATED_BODY()
|
||||
|
||||
MC_PROPERTY()
|
||||
std::uint32_t SchemaVersion = 2;
|
||||
|
||||
MC_PROPERTY()
|
||||
std::uint32_t GeneratorVersion = 1;
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string Name{};
|
||||
|
||||
@ -226,6 +339,39 @@ struct MetaCoreUiDocument {
|
||||
MC_PROPERTY()
|
||||
std::int32_t ReferenceHeight = 1080;
|
||||
|
||||
MC_PROPERTY()
|
||||
MetaCoreUiCanvasScaleMode ScaleMode = MetaCoreUiCanvasScaleMode::ScaleWithScreenSize;
|
||||
|
||||
MC_PROPERTY()
|
||||
float MatchWidthOrHeight = 0.5F;
|
||||
|
||||
MC_PROPERTY()
|
||||
float ReferenceDpi = 96.0F;
|
||||
|
||||
MC_PROPERTY()
|
||||
float MinScale = 0.5F;
|
||||
|
||||
MC_PROPERTY()
|
||||
float MaxScale = 4.0F;
|
||||
|
||||
MC_PROPERTY()
|
||||
bool UseSafeArea = true;
|
||||
|
||||
MC_PROPERTY()
|
||||
MetaCoreAssetGuid ThemeAssetGuid{};
|
||||
|
||||
MC_PROPERTY()
|
||||
MetaCoreAssetGuid FontFamilyAssetGuid{};
|
||||
|
||||
MC_PROPERTY()
|
||||
MetaCoreAssetGuid LocalizationAssetGuid{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string DefaultLocale{"en-US"};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string FallbackLocale{"en-US"};
|
||||
|
||||
MC_PROPERTY()
|
||||
glm::vec3 BackgroundColor{0.0F, 0.0F, 0.0F};
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
#include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h"
|
||||
#include "MetaCorePhysics/MetaCorePhysics.h"
|
||||
#include "MetaCoreAnimation/MetaCoreAnimation.h"
|
||||
#include "MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h"
|
||||
#include "MetaCoreFoundation/MetaCoreHash.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
@ -453,10 +454,11 @@ struct MetaCoreScriptRuntime::Impl {
|
||||
MetaCoreRuntimeInput* Input = nullptr;
|
||||
MetaCorePhysicsWorld* Physics = nullptr;
|
||||
MetaCoreAnimationRuntime* Animation = nullptr;
|
||||
MetaCoreRuntimeUiSystem* RuntimeUi = nullptr;
|
||||
|
||||
Impl(MetaCoreScriptRuntime& owner, MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger,
|
||||
MetaCoreRuntimeInput* input, MetaCorePhysicsWorld* physics, MetaCoreAnimationRuntime* animation)
|
||||
: Owner(owner), Scene(scene), Registry(registry), Logger(std::move(logger)), Input(input), Physics(physics), Animation(animation) {}
|
||||
MetaCoreRuntimeInput* input, MetaCorePhysicsWorld* physics, MetaCoreAnimationRuntime* animation, MetaCoreRuntimeUiSystem* runtimeUi)
|
||||
: Owner(owner), Scene(scene), Registry(registry), Logger(std::move(logger)), Input(input), Physics(physics), Animation(animation), RuntimeUi(runtimeUi) {}
|
||||
|
||||
void Log(std::uint32_t level, std::string_view category, std::string_view text) const { if (Logger) Logger(level, category, text); }
|
||||
Entry* FindEntry(MetaCoreId object, std::uint64_t instance) {
|
||||
@ -562,8 +564,8 @@ struct MetaCoreScriptRuntime::Impl {
|
||||
};
|
||||
|
||||
MetaCoreScriptRuntime::MetaCoreScriptRuntime(MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger,
|
||||
MetaCoreRuntimeInput* input, MetaCorePhysicsWorld* physics, MetaCoreAnimationRuntime* animation)
|
||||
: Impl_(std::make_unique<Impl>(*this, scene, registry, std::move(logger), input, physics, animation)) {}
|
||||
MetaCoreRuntimeInput* input, MetaCorePhysicsWorld* physics, MetaCoreAnimationRuntime* animation, MetaCoreRuntimeUiSystem* runtimeUi)
|
||||
: Impl_(std::make_unique<Impl>(*this, scene, registry, std::move(logger), input, physics, animation, runtimeUi)) {}
|
||||
MetaCoreScriptRuntime::~MetaCoreScriptRuntime() { Stop(); }
|
||||
void MetaCoreScriptRuntime::Start() { if (Impl_->Running) return; Impl_->Running = true; ++WorldGeneration_; for (auto o : Impl_->Scene.GetGameObjects()) if (o.HasComponent<MetaCoreScriptComponent>()) for (auto& i : o.GetComponent<MetaCoreScriptComponent>().Instances) if (i.Enabled) Impl_->Ensure(o, i); }
|
||||
void MetaCoreScriptRuntime::FixedUpdate(double delta) { if (!Impl_->Running) return; Time_.FixedDeltaSeconds = std::max(0.0, delta); Impl_->Dispatch(Impl::Phase::Fixed, Time_.FixedDeltaSeconds); }
|
||||
@ -588,6 +590,7 @@ void MetaCoreScriptRuntime::PublishToObject(MetaCoreId object, std::string topic
|
||||
MetaCoreRuntimeInput* MetaCoreScriptRuntime::GetRuntimeInput() const { return Impl_->Input; }
|
||||
MetaCorePhysicsWorld* MetaCoreScriptRuntime::GetPhysicsWorld() const { return Impl_->Physics; }
|
||||
MetaCoreAnimationRuntime* MetaCoreScriptRuntime::GetAnimationRuntime() const { return Impl_->Animation; }
|
||||
MetaCoreRuntimeUiSystem* MetaCoreScriptRuntime::GetRuntimeUi() const { return Impl_->RuntimeUi; }
|
||||
std::uint64_t MetaCoreScriptRuntime::ScheduleTimer(MetaCoreId object, std::uint64_t instance, double delay, double interval, bool unscaled, std::function<void()> callback) { const auto id = Impl_->NextTimer++; Impl_->Timers.push_back({id, object, instance, std::max(0.0, delay), std::max(0.0, interval), unscaled, std::move(callback)}); return id; }
|
||||
void MetaCoreScriptRuntime::CancelTimer(std::uint64_t id) { std::erase_if(Impl_->Timers, [id](const auto& t) { return t.Id == id; }); }
|
||||
bool MetaCoreScriptRuntime::IsHandleValid(MetaCoreScriptObjectHandleV1 handle) const { return handle.WorldGeneration == WorldGeneration_ && handle.ObjectId != 0 && static_cast<bool>(Impl_->Scene.FindGameObject(handle.ObjectId)); }
|
||||
@ -686,7 +689,41 @@ bool HTimelineStop(void*c,MetaCoreScriptObjectHandleV1 h) noexcept{try{auto*a=HA
|
||||
bool HTimelineSeek(void*c,MetaCoreScriptObjectHandleV1 h,double s) noexcept{try{auto*a=HAnimation(c,h);return a&&a->TimelineSeek(h.ObjectId,static_cast<float>(s));}catch(...){return false;}}
|
||||
bool HTimelineSpeed(void*c,MetaCoreScriptObjectHandleV1 h,double s) noexcept{try{auto*a=HAnimation(c,h);return a&&a->TimelineSetSpeed(h.ObjectId,static_cast<float>(s));}catch(...){return false;}}
|
||||
bool HAnimationConsumeRootMotion(void*c,MetaCoreScriptObjectHandleV1 h,MetaCoreScriptRootMotionV1*out) noexcept{try{auto*a=HAnimation(c,h);if(!a||!out)return false;const auto delta=a->ConsumeRootMotion(h.ObjectId);if(!delta)return false;out->Translation[0]=delta->Translation.x;out->Translation[1]=delta->Translation.y;out->Translation[2]=delta->Translation.z;out->Rotation[0]=delta->Rotation.w;out->Rotation[1]=delta->Rotation.x;out->Rotation[2]=delta->Rotation.y;out->Rotation[3]=delta->Rotation.z;return true;}catch(...){return false;}}
|
||||
const MetaCoreScriptHostApiV1& HostApi() { static const MetaCoreScriptHostApiV1 api{METACORE_SCRIPT_ABI_MAJOR, METACORE_SCRIPT_ABI_MINOR, METACORE_SCRIPT_HOST_CAPABILITIES, HValid, HHas, HGet, HSet, HGetField, HSetField, HLog, HPublish, HSubscribe, HUnsubscribe, HSchedule, HCancel, HInputAction, HInputAxis, HInputContext, HInputRebind, HInputReset, HPhysicsRay, HPhysicsShape, HPhysicsOverlap, HPhysicsForce, HPhysicsImpulse, HPhysicsGetVelocity, HPhysicsSetVelocity, HPhysicsWake, HPhysicsMove, HPhysicsJump, HPhysicsGrounded, HPhysicsConstraintEnabled, HPhysicsConstraintMotor, HAnimationBool, HAnimationInt, HAnimationFloat, HAnimationTrigger, HAnimationPlay, HAnimationCrossFade, HAnimationState, HTimelinePlay, HTimelinePause, HTimelineStop, HTimelineSeek, HTimelineSpeed, HAnimationConsumeRootMotion}; return api; }
|
||||
MetaCoreRuntimeUiValue HUiValue(const MetaCoreScriptValueV1& source) {
|
||||
MetaCoreRuntimeUiValue value;
|
||||
switch (source.Type) {
|
||||
case MetaCoreScriptAbiValue_Bool: value.Type=MetaCoreRuntimeUiValueType::Bool;value.BoolValue=source.IntValue!=0;break;
|
||||
case MetaCoreScriptAbiValue_Int: value.Type=MetaCoreRuntimeUiValueType::Int64;value.Int64Value=source.IntValue;break;
|
||||
case MetaCoreScriptAbiValue_Float: value.Type=MetaCoreRuntimeUiValueType::Double;value.DoubleValue=source.NumberValue;break;
|
||||
case MetaCoreScriptAbiValue_Vec3: value.Type=MetaCoreRuntimeUiValueType::Vec3;value.Vec3Value={source.Vec3[0],source.Vec3[1],source.Vec3[2]};break;
|
||||
case MetaCoreScriptAbiValue_AssetRef: value.Type=MetaCoreRuntimeUiValueType::AssetRef;if(auto guid=MetaCoreAssetGuid::Parse(source.Text))value.AssetValue=*guid;break;
|
||||
case MetaCoreScriptAbiValue_GameObjectRef: value.Type=MetaCoreRuntimeUiValueType::GameObjectRef;value.ObjectValue=source.ObjectValue.ObjectId;break;
|
||||
case MetaCoreScriptAbiValue_String:
|
||||
default: value.Type=MetaCoreRuntimeUiValueType::String;value.StringValue=source.Text;break;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
void HScriptValue(const MetaCoreRuntimeUiValue& source, MetaCoreScriptValueV1& value) {
|
||||
value={};
|
||||
switch(source.Type) {
|
||||
case MetaCoreRuntimeUiValueType::Bool:value.Type=MetaCoreScriptAbiValue_Bool;value.IntValue=source.BoolValue?1:0;break;
|
||||
case MetaCoreRuntimeUiValueType::Int64:value.Type=MetaCoreScriptAbiValue_Int;value.IntValue=source.Int64Value;break;
|
||||
case MetaCoreRuntimeUiValueType::Double:value.Type=MetaCoreScriptAbiValue_Float;value.NumberValue=source.DoubleValue;break;
|
||||
case MetaCoreRuntimeUiValueType::Vec3:value.Type=MetaCoreScriptAbiValue_Vec3;value.Vec3[0]=source.Vec3Value.x;value.Vec3[1]=source.Vec3Value.y;value.Vec3[2]=source.Vec3Value.z;break;
|
||||
case MetaCoreRuntimeUiValueType::AssetRef:value.Type=MetaCoreScriptAbiValue_AssetRef;std::snprintf(value.Text,sizeof(value.Text),"%s",source.AssetValue.ToString().c_str());break;
|
||||
case MetaCoreRuntimeUiValueType::GameObjectRef:value.Type=MetaCoreScriptAbiValue_GameObjectRef;value.ObjectValue.ObjectId=source.ObjectValue;break;
|
||||
case MetaCoreRuntimeUiValueType::String:value.Type=MetaCoreScriptAbiValue_String;std::snprintf(value.Text,sizeof(value.Text),"%s",source.StringValue.c_str());break;
|
||||
}
|
||||
}
|
||||
MetaCoreRuntimeUiSystem* HUi(void*c){auto*r=static_cast<MetaCoreScriptRuntime*>(c);return r?r->GetRuntimeUi():nullptr;}
|
||||
bool HUiVisible(void*c,const char*id,bool visible)noexcept{try{auto*ui=HUi(c);return ui&&id&&ui->SetDocumentVisible(id,visible);}catch(...){return false;}}
|
||||
bool HUiSet(void*c,const char*path,const MetaCoreScriptValueV1*value)noexcept{try{auto*ui=HUi(c);return ui&&path&&value&&ui->SetValue(path,HUiValue(*value));}catch(...){return false;}}
|
||||
bool HUiGet(void*c,const char*path,MetaCoreScriptValueV1*out)noexcept{try{auto*ui=HUi(c);if(!ui||!path||!out)return false;auto value=ui->GetValue(path);if(!value)return false;HScriptValue(*value,*out);return true;}catch(...){return false;}}
|
||||
bool HUiRows(void*c,const char*path,const MetaCoreScriptUiRowV1*rows,std::size_t count)noexcept{try{auto*ui=HUi(c);if(!ui||!path||(!rows&&count))return false;std::vector<MetaCoreRuntimeUiRow> converted;converted.reserve(count);for(std::size_t i=0;i<count;++i){MetaCoreRuntimeUiRow row;for(std::size_t f=0;f<rows[i].FieldCount;++f)if(rows[i].Fields[f].Key)row[rows[i].Fields[f].Key]=HUiValue(rows[i].Fields[f].Value);converted.push_back(std::move(row));}return ui->SetListRows(path,std::move(converted));}catch(...){return false;}}
|
||||
bool HUiFocus(void*c,std::uint64_t instance,const char*element)noexcept{try{auto*ui=HUi(c);return ui&&element&&ui->RequestFocus(instance,element);}catch(...){return false;}}
|
||||
bool HUiLocale(void*c,const char*locale)noexcept{try{auto*ui=HUi(c);if(!ui||!locale)return false;ui->SetLocale(locale);return true;}catch(...){return false;}}
|
||||
bool HUiTheme(void*c,const char*theme)noexcept{try{auto*ui=HUi(c);if(!ui||!theme)return false;ui->SetTheme(theme);return true;}catch(...){return false;}}
|
||||
const MetaCoreScriptHostApiV1& HostApi() { static const MetaCoreScriptHostApiV1 api{METACORE_SCRIPT_ABI_MAJOR, METACORE_SCRIPT_ABI_MINOR, METACORE_SCRIPT_HOST_CAPABILITIES, HValid, HHas, HGet, HSet, HGetField, HSetField, HLog, HPublish, HSubscribe, HUnsubscribe, HSchedule, HCancel, HInputAction, HInputAxis, HInputContext, HInputRebind, HInputReset, HPhysicsRay, HPhysicsShape, HPhysicsOverlap, HPhysicsForce, HPhysicsImpulse, HPhysicsGetVelocity, HPhysicsSetVelocity, HPhysicsWake, HPhysicsMove, HPhysicsJump, HPhysicsGrounded, HPhysicsConstraintEnabled, HPhysicsConstraintMotor, HAnimationBool, HAnimationInt, HAnimationFloat, HAnimationTrigger, HAnimationPlay, HAnimationCrossFade, HAnimationState, HTimelinePlay, HTimelinePause, HTimelineStop, HTimelineSeek, HTimelineSpeed, HAnimationConsumeRootMotion, HUiVisible, HUiSet, HUiGet, HUiRows, HUiFocus, HUiLocale, HUiTheme}; return api; }
|
||||
} // namespace
|
||||
|
||||
bool MetaCoreGenerateScriptProject(const std::filesystem::path& root, std::string_view projectName, const std::filesystem::path& sdkRoot, std::string* error) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
|
||||
namespace MetaCore {
|
||||
class MetaCoreAnimationRuntime;
|
||||
class MetaCoreRuntimeUiSystem;
|
||||
|
||||
enum class MetaCoreScriptFieldType {
|
||||
Bool = 0,
|
||||
@ -119,7 +120,8 @@ public:
|
||||
class MetaCoreScriptRuntime {
|
||||
public:
|
||||
MetaCoreScriptRuntime(MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger = {},
|
||||
MetaCoreRuntimeInput* input = nullptr, MetaCorePhysicsWorld* physics = nullptr, MetaCoreAnimationRuntime* animation = nullptr);
|
||||
MetaCoreRuntimeInput* input = nullptr, MetaCorePhysicsWorld* physics = nullptr, MetaCoreAnimationRuntime* animation = nullptr,
|
||||
MetaCoreRuntimeUiSystem* runtimeUi = nullptr);
|
||||
~MetaCoreScriptRuntime();
|
||||
|
||||
void Start();
|
||||
@ -140,6 +142,7 @@ public:
|
||||
[[nodiscard]] MetaCoreRuntimeInput* GetRuntimeInput() const;
|
||||
[[nodiscard]] MetaCorePhysicsWorld* GetPhysicsWorld() const;
|
||||
[[nodiscard]] MetaCoreAnimationRuntime* GetAnimationRuntime() const;
|
||||
[[nodiscard]] MetaCoreRuntimeUiSystem* GetRuntimeUi() const;
|
||||
std::uint64_t ScheduleTimer(MetaCoreId objectId, std::uint64_t instanceId, double delay, double interval, bool unscaled, std::function<void()> callback);
|
||||
void CancelTimer(std::uint64_t timerId);
|
||||
|
||||
|
||||
@ -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/<guid>` 产物,并收集、验证和复制 UI 依赖。
|
||||
- 运行时提供类型化 Bool/Int64/Double/String/Vec3/AssetRef/GameObjectRef 值、列表行、增量路径刷新、fallback、实例创建销毁、焦点、Locale、Theme 和诊断接口;输入优先于 3D 交互,事件涵盖 Pointer Enter/Leave、Click、Submit、Value/Selection Changed、Scroll、Focus/Blur 和不可写 Provider 的 WriteRequest。
|
||||
- 脚本 ABI Minor 升至 5,新增 `METACORE_SCRIPT_CAP_UI`,提供文档显示、节点值、列表、焦点、Locale 和 Theme 接口;UI 事件同时发布到全局 `MetaCore.UI` 和清单指定的场景对象。
|
||||
- UI Designer 已补齐新控件、Canvas、Anchor/Pivot、导航、输入/列表/滚动和类型化绑定 Inspector,并以正式编译产物作为导出/预览来源。
|
||||
- 新增独立 `MetaCoreRuntimeUiTests`,覆盖七类节点、文档校验/迁移、确定性编译、Anchor、双向绑定及 JSON/二进制往返;Smoke 覆盖启动场景 GUID 依赖扫描、v2 清单和编译产物。Linux Development 全量构建成功,CTest **12/12** 通过。
|
||||
|
||||
尚未满足、因此不能正式关闭问题 9:
|
||||
|
||||
- World Space 目前仍复用 Screen Space overlay 路径;独立离屏 RmlUi Context、Filament Render Target、`PixelsPerUnit` 尺寸映射、遮挡和射线 UV 输入尚未完成。
|
||||
- Canvas 参数已进入格式和编译元数据,但运行时 DPI/物理尺寸、安全区、宽高比约束的逐实例求值尚未完成;方向键空间导航和焦点失效转移也缺少完整自动化覆盖。
|
||||
- `UiTheme`、`UiFontFamily`、`UiLocalizationTable` 仍只有文档 GUID 引用和运行时切换接口,Token 状态样式解析、字体 fallback/字符集图集、本地化表 fallback、图片图集及基础动效编译尚未形成完整资产管线。
|
||||
- `runtime.*` 已连接 RuntimeData,`script.*` 可由脚本发布;`scene.*` 的稳定对象句柄反射 Provider、严格类型转换/格式诊断及完整双向写请求测试仍需补齐。
|
||||
- 旧 RML 目前是运行时兼容与 Cook 复制,不是一次性结构化导入器;Designer 的拖放重挂、资产级 Undo/Redo、未保存切换保护及完整分辨率/DPI/Locale/Theme/输入模拟尚未全部验收。
|
||||
- 2,000 节点/500 绑定/100 行列表基准、Screen/World Space GPU 黄金图、4:3/16:9/21:9 与 DPI/中英文矩阵、SSIM ≥ 0.995、16.67 ms 帧预算和 Linux x86_64 图形参考机 Editor/Player 像素及交互签收尚未执行。
|
||||
|
||||
### 10. Scene 与 Prefab
|
||||
|
||||
|
||||
117
tests/MetaCoreRuntimeUiTests.cpp
Normal file
117
tests/MetaCoreRuntimeUiTests.cpp
Normal file
@ -0,0 +1,117 @@
|
||||
#include "MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h"
|
||||
#include "MetaCoreFoundation/MetaCoreGeneratedReflection.h"
|
||||
#include "MetaCoreScene/MetaCoreSceneSerializer.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
void Expect(bool value, const char* message) {
|
||||
if (!value) {
|
||||
std::cerr << "MetaCoreRuntimeUiTests: " << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
MetaCore::MetaCoreUiDocument BuildDocument() {
|
||||
using namespace MetaCore;
|
||||
MetaCoreUiDocument document;
|
||||
document.Name = "Controls";
|
||||
document.ReferenceWidth = 1920;
|
||||
document.ReferenceHeight = 1080;
|
||||
document.ScaleMode = MetaCoreUiCanvasScaleMode::ScaleWithScreenSize;
|
||||
document.MatchWidthOrHeight = 0.5F;
|
||||
|
||||
MetaCoreUiNodeDocument root;
|
||||
root.Id = "Root";
|
||||
root.Name = "Root";
|
||||
root.Type = MetaCoreUiNodeType::Panel;
|
||||
root.RectTransform.AnchorMin = {0.0F, 0.0F, 0.0F};
|
||||
root.RectTransform.AnchorMax = {1.0F, 1.0F, 0.0F};
|
||||
root.RectTransform.Size = {0.0F, 0.0F, 0.0F};
|
||||
|
||||
const std::vector<MetaCoreUiNodeType> types{
|
||||
MetaCoreUiNodeType::Text, MetaCoreUiNodeType::Image, MetaCoreUiNodeType::Button,
|
||||
MetaCoreUiNodeType::Input, MetaCoreUiNodeType::List, MetaCoreUiNodeType::Scroll
|
||||
};
|
||||
for (std::size_t index = 0; index < types.size(); ++index) {
|
||||
MetaCoreUiNodeDocument node;
|
||||
node.Id = "Node" + std::to_string(index);
|
||||
node.Name = node.Id;
|
||||
node.ParentId = root.Id;
|
||||
node.Type = types[index];
|
||||
node.Text = "Safe <text>";
|
||||
node.Interactable = types[index] == MetaCoreUiNodeType::Button || types[index] == MetaCoreUiNodeType::Input;
|
||||
node.NavigationOrder = static_cast<std::int32_t>(index);
|
||||
node.RectTransform.AnchorMin = {0.25F, 0.25F, 0.0F};
|
||||
node.RectTransform.AnchorMax = {0.75F, 0.25F, 0.0F};
|
||||
node.RectTransform.Position = {10.0F, 20.0F + static_cast<float>(index) * 50.0F, 0.0F};
|
||||
if (types[index] == MetaCoreUiNodeType::Input) {
|
||||
node.Placeholder = "Value";
|
||||
node.MaxLength = 32;
|
||||
node.Bindings.push_back({"script.form.value", MetaCoreUiBindingTarget::Value,
|
||||
MetaCoreUiBindingMode::TwoWay, {}, {}, "default"});
|
||||
}
|
||||
if (types[index] == MetaCoreUiNodeType::List) {
|
||||
node.Bindings.push_back({"script.rows", MetaCoreUiBindingTarget::Items,
|
||||
MetaCoreUiBindingMode::OneWay, {}, {}, {}});
|
||||
}
|
||||
root.Children.push_back(node.Id);
|
||||
document.Nodes.push_back(std::move(node));
|
||||
}
|
||||
document.Nodes.insert(document.Nodes.begin(), root);
|
||||
document.RootNodeIds.push_back(root.Id);
|
||||
return document;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
using namespace MetaCore;
|
||||
const MetaCoreUiDocument document = BuildDocument();
|
||||
Expect(MetaCoreValidateUiDocument(document).empty(), "valid document was rejected");
|
||||
const MetaCoreCompiledUiDocument first = MetaCoreCompileUiDocument(document);
|
||||
const MetaCoreCompiledUiDocument second = MetaCoreCompileUiDocument(document);
|
||||
Expect(first.Succeeded, "document compilation failed");
|
||||
Expect(first.Rml == second.Rml && first.Rcss == second.Rcss, "compiler output is not deterministic");
|
||||
Expect(first.Rml.find("<input") != std::string::npos, "Input control was not compiled");
|
||||
Expect(first.Rml.find("role=\"listbox\"") != std::string::npos, "List control was not compiled");
|
||||
Expect(first.Rml.find("mcui-scroll") != std::string::npos, "Scroll control was not compiled");
|
||||
Expect(first.Rml.find("Safe <text>") != std::string::npos, "text was not escaped");
|
||||
Expect(first.Rcss.find("left:calc(25") != std::string::npos, "anchors were not compiled");
|
||||
Expect(first.Rml.find("data-metacore-bind-mode-value=\"two-way\"") != std::string::npos, "two-way binding was not compiled");
|
||||
|
||||
MetaCoreUiDocument invalid = document;
|
||||
invalid.Nodes.back().Id = invalid.Nodes.front().Id;
|
||||
Expect(!MetaCoreValidateUiDocument(invalid).empty(), "duplicate stable ID was accepted");
|
||||
invalid = document;
|
||||
invalid.Nodes[1].Bindings.push_back({"unknown.path", MetaCoreUiBindingTarget::Text,
|
||||
MetaCoreUiBindingMode::OneWay, {}, {}, {}});
|
||||
Expect(!MetaCoreValidateUiDocument(invalid).empty(), "invalid binding namespace was accepted");
|
||||
|
||||
MetaCoreUiDocument legacy = document;
|
||||
legacy.SchemaVersion = 1;
|
||||
legacy.Nodes[1].Bindings.clear();
|
||||
legacy.Nodes[1].DataBinding = "runtime.temperature";
|
||||
legacy.Nodes[1].DataFormat = "1";
|
||||
const MetaCoreUiDocument migrated = MetaCoreMigrateUiDocument(legacy);
|
||||
Expect(migrated.SchemaVersion == METACORE_UI_SCHEMA_VERSION, "legacy schema was not migrated");
|
||||
Expect(migrated.Nodes[1].Bindings.size() == 1, "legacy binding was not migrated");
|
||||
|
||||
const std::filesystem::path jsonPath = std::filesystem::temp_directory_path() / "MetaCoreRuntimeUiV2.mcui.json";
|
||||
MetaCoreTypeRegistry registry;
|
||||
MetaCoreRegisterFoundationGeneratedTypes(registry);
|
||||
MetaCoreRegisterSceneGeneratedTypes(registry);
|
||||
Expect(MetaCoreSerializeToBytes(document, registry).has_value(), "v2 binary serialization failed");
|
||||
Expect(MetaCoreSceneSerializer::SaveUiToJson(jsonPath, document, registry), "v2 JSON write failed");
|
||||
const auto loaded = MetaCoreSceneSerializer::LoadUiFromJson(jsonPath, registry);
|
||||
Expect(loaded.has_value() && loaded->SchemaVersion == 2, "v2 JSON schema did not round-trip");
|
||||
Expect(loaded->Nodes[4].Placeholder == "Value", "Input settings did not round-trip");
|
||||
std::filesystem::remove(jsonPath);
|
||||
|
||||
std::cout << "MetaCoreRuntimeUiTests passed\n";
|
||||
return 0;
|
||||
}
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user