UI优化
This commit is contained in:
parent
92431296fa
commit
ddeba5bbbc
@ -578,6 +578,12 @@ int main(int argc, char* argv[]) {
|
||||
errors << "MetaCorePlayer: failed to initialize runtime UI\n";
|
||||
} else {
|
||||
runtimeUi.AttachToViewportRenderer(viewportRenderer);
|
||||
(void)runtimeUi.RegisterProvider(
|
||||
"scene",
|
||||
[&scriptRuntime](std::string_view path) { return scriptRuntime.ReadSceneUiBinding(path); },
|
||||
[&scriptRuntime](std::string_view path, const MetaCore::MetaCoreRuntimeUiValue& value) {
|
||||
return scriptRuntime.WriteSceneUiBinding(path, value);
|
||||
});
|
||||
}
|
||||
for (const std::string& error : runtimeUi.GetErrors()) {
|
||||
errors << "MetaCorePlayer UI: " << error << '\n';
|
||||
@ -658,7 +664,22 @@ int main(int argc, char* argv[]) {
|
||||
scriptRuntime.PublishToObject(event.SceneObjectId, "MetaCore.UI", payload);
|
||||
}
|
||||
});
|
||||
runtimeInteraction.SetEventCallback([&scriptRuntime](const MetaCore::MetaCorePointerEvent& event) {
|
||||
runtimeInteraction.SetEventCallback([&scriptRuntime, &runtimeUi](const MetaCore::MetaCorePointerEvent& event) {
|
||||
if (event.WorldSpaceUi) {
|
||||
using PointerType = MetaCore::MetaCoreRuntimeUiWorldPointerType;
|
||||
std::optional<PointerType> type;
|
||||
if (event.Type == MetaCore::MetaCorePointerEventType::Enter || event.Type == MetaCore::MetaCorePointerEventType::Move)
|
||||
type = PointerType::Move;
|
||||
else if (event.Type == MetaCore::MetaCorePointerEventType::Down) type = PointerType::Down;
|
||||
else if (event.Type == MetaCore::MetaCorePointerEventType::Up) type = PointerType::Up;
|
||||
else if (event.Type == MetaCore::MetaCorePointerEventType::Scroll) type = PointerType::Scroll;
|
||||
else if (event.Type == MetaCore::MetaCorePointerEventType::Leave ||
|
||||
event.Type == MetaCore::MetaCorePointerEventType::Cancel) type = PointerType::Cancel;
|
||||
if (type.has_value())
|
||||
(void)runtimeUi.ProcessWorldPointer(
|
||||
event.TargetObjectId, event.SurfaceUv, *type, event.Button, event.ScrollDelta);
|
||||
return;
|
||||
}
|
||||
MetaCoreScriptValueV1 type{}; type.Type = MetaCoreScriptAbiValue_Int; type.IntValue = static_cast<std::int64_t>(event.Type);
|
||||
MetaCoreScriptValueV1 button{}; button.Type = MetaCoreScriptAbiValue_Int; button.IntValue = event.Button;
|
||||
MetaCoreScriptValueV1 screen{}; screen.Type = MetaCoreScriptAbiValue_Vec3; screen.Vec3[0] = event.ScreenPosition.x; screen.Vec3[1] = event.ScreenPosition.y;
|
||||
|
||||
Binary file not shown.
@ -5458,6 +5458,9 @@ void MetaCoreWriteEmbeddedTextureRuntimePackages(
|
||||
if (filename.length() > 10 && filename.compare(filename.length() - 10, 10, ".mcui.json") == 0) {
|
||||
return "UiDocumentImporter";
|
||||
}
|
||||
if (filename.ends_with(".mcuitheme.json")) return "UiThemeImporter";
|
||||
if (filename.ends_with(".mcuifont.json")) return "UiFontFamilyImporter";
|
||||
if (filename.ends_with(".mculoc.json")) return "UiLocalizationImporter";
|
||||
|
||||
const std::string extension = path.extension().string();
|
||||
if (extension == ".mcanim" || extension == ".mcavatar" || extension == ".mcanimator" || extension == ".mctimeline") return "AnimationAssetImporter";
|
||||
@ -5499,6 +5502,9 @@ void MetaCoreWriteEmbeddedTextureRuntimePackages(
|
||||
if (filename.length() > 10 && filename.compare(filename.length() - 10, 10, ".mcui.json") == 0) {
|
||||
return "ui_document";
|
||||
}
|
||||
if (filename.ends_with(".mcuitheme.json")) return "ui_theme";
|
||||
if (filename.ends_with(".mcuifont.json")) return "ui_font_family";
|
||||
if (filename.ends_with(".mculoc.json")) return "ui_localization";
|
||||
|
||||
const std::string extension = path.extension().string();
|
||||
if (extension == ".mcanim") return "animation_clip";
|
||||
@ -8386,6 +8392,36 @@ bool MetaCoreBuiltinCookService::CookAsset(const MetaCoreAssetGuid& assetGuid) {
|
||||
writeSuccess = PackageService_->WritePackage(absoluteCookedPath, std::move(package));
|
||||
}
|
||||
}
|
||||
} else if (record->Type == "ui_theme") {
|
||||
isJsonAsset = true;
|
||||
if (auto doc = MetaCoreSceneSerializer::LoadUiThemeFromJson(absoluteSourcePath)) {
|
||||
if (auto payload = MetaCoreSerializeToBytes(*doc, registry)) {
|
||||
writeSuccess = PackageService_->WritePackage(absoluteCookedPath, MetaCoreBuildTypedPackage(
|
||||
MetaCorePackageType::Asset, assetGuid, record->RelativePath.filename().string(),
|
||||
"MetaCoreUiThemeDocument",
|
||||
MetaCoreHashBytes(std::span<const std::byte>(payload->data(), payload->size())), *payload));
|
||||
}
|
||||
}
|
||||
} else if (record->Type == "ui_font_family") {
|
||||
isJsonAsset = true;
|
||||
if (auto doc = MetaCoreSceneSerializer::LoadUiFontFamilyFromJson(absoluteSourcePath)) {
|
||||
if (auto payload = MetaCoreSerializeToBytes(*doc, registry)) {
|
||||
writeSuccess = PackageService_->WritePackage(absoluteCookedPath, MetaCoreBuildTypedPackage(
|
||||
MetaCorePackageType::Asset, assetGuid, record->RelativePath.filename().string(),
|
||||
"MetaCoreUiFontFamilyDocument",
|
||||
MetaCoreHashBytes(std::span<const std::byte>(payload->data(), payload->size())), *payload));
|
||||
}
|
||||
}
|
||||
} else if (record->Type == "ui_localization") {
|
||||
isJsonAsset = true;
|
||||
if (auto doc = MetaCoreSceneSerializer::LoadUiLocalizationTableFromJson(absoluteSourcePath)) {
|
||||
if (auto payload = MetaCoreSerializeToBytes(*doc, registry)) {
|
||||
writeSuccess = PackageService_->WritePackage(absoluteCookedPath, MetaCoreBuildTypedPackage(
|
||||
MetaCorePackageType::Asset, assetGuid, record->RelativePath.filename().string(),
|
||||
"MetaCoreUiLocalizationTableDocument",
|
||||
MetaCoreHashBytes(std::span<const std::byte>(payload->data(), payload->size())), *payload));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isJsonAsset) {
|
||||
@ -8910,6 +8946,147 @@ bool MetaCoreBuiltinCookService::CookRuntimeUiAssets() {
|
||||
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())));
|
||||
};
|
||||
const auto loadTheme = [&](const MetaCoreAssetGuid& guid) -> std::optional<MetaCoreUiThemeDocument> {
|
||||
if (!guid.IsValid()) return std::nullopt;
|
||||
const auto record = AssetDatabaseService_->FindAssetByGuid(guid);
|
||||
if (!record.has_value() || record->Type != "ui_theme") return std::nullopt;
|
||||
const std::filesystem::path path = project.RootPath / record->RelativePath;
|
||||
if (path.extension() == ".json") return MetaCoreSceneSerializer::LoadUiThemeFromJson(path);
|
||||
const auto package = PackageService_->ReadPackage(path);
|
||||
return package.has_value()
|
||||
? MetaCoreReadTypedPackagePayload<MetaCoreUiThemeDocument>(*package, registry, "MetaCoreUiThemeDocument")
|
||||
: std::nullopt;
|
||||
};
|
||||
const auto loadFontFamily = [&](const MetaCoreAssetGuid& guid) -> std::optional<MetaCoreUiFontFamilyDocument> {
|
||||
if (!guid.IsValid()) return std::nullopt;
|
||||
const auto record = AssetDatabaseService_->FindAssetByGuid(guid);
|
||||
if (!record.has_value() || record->Type != "ui_font_family") return std::nullopt;
|
||||
const std::filesystem::path path = project.RootPath / record->RelativePath;
|
||||
if (path.extension() == ".json") return MetaCoreSceneSerializer::LoadUiFontFamilyFromJson(path);
|
||||
const auto package = PackageService_->ReadPackage(path);
|
||||
return package.has_value()
|
||||
? MetaCoreReadTypedPackagePayload<MetaCoreUiFontFamilyDocument>(*package, registry, "MetaCoreUiFontFamilyDocument")
|
||||
: std::nullopt;
|
||||
};
|
||||
const auto loadLocalization = [&](const MetaCoreAssetGuid& guid) -> std::optional<MetaCoreUiLocalizationTableDocument> {
|
||||
if (!guid.IsValid()) return std::nullopt;
|
||||
const auto record = AssetDatabaseService_->FindAssetByGuid(guid);
|
||||
if (!record.has_value() || record->Type != "ui_localization") return std::nullopt;
|
||||
const std::filesystem::path path = project.RootPath / record->RelativePath;
|
||||
if (path.extension() == ".json") return MetaCoreSceneSerializer::LoadUiLocalizationTableFromJson(path);
|
||||
const auto package = PackageService_->ReadPackage(path);
|
||||
return package.has_value()
|
||||
? MetaCoreReadTypedPackagePayload<MetaCoreUiLocalizationTableDocument>(*package, registry, "MetaCoreUiLocalizationTableDocument")
|
||||
: std::nullopt;
|
||||
};
|
||||
const auto buildAtlas = [&](const MetaCoreUiDocument& document, MetaCoreUiCompileResources& resources,
|
||||
std::vector<std::uint8_t>& pixels) {
|
||||
struct Image {
|
||||
MetaCoreAssetGuid Guid{};
|
||||
int Width = 0;
|
||||
int Height = 0;
|
||||
std::vector<std::uint8_t> Pixels{};
|
||||
};
|
||||
std::vector<MetaCoreAssetGuid> imageGuids;
|
||||
for (const auto& node : document.Nodes)
|
||||
if (node.Type == MetaCoreUiNodeType::Image && node.Style.ImageAssetGuid.IsValid())
|
||||
imageGuids.push_back(node.Style.ImageAssetGuid);
|
||||
std::sort(imageGuids.begin(), imageGuids.end(), [](const auto& lhs, const auto& rhs) {
|
||||
return lhs.ToString() < rhs.ToString();
|
||||
});
|
||||
imageGuids.erase(std::unique(imageGuids.begin(), imageGuids.end()), imageGuids.end());
|
||||
if (imageGuids.empty()) return true;
|
||||
|
||||
std::vector<Image> images;
|
||||
for (const auto guid : imageGuids) {
|
||||
const auto record = AssetDatabaseService_->FindAssetByGuid(guid);
|
||||
if (!record.has_value() || record->Type != "texture") return false;
|
||||
std::filesystem::path path = !record->SourcePath.empty() ? record->SourcePath : record->RelativePath;
|
||||
if (path.is_relative()) path = project.RootPath / path;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int channels = 0;
|
||||
stbi_uc* decoded = stbi_load(path.string().c_str(), &width, &height, &channels, STBI_rgb_alpha);
|
||||
if (decoded == nullptr || width <= 0 || height <= 0 || width > 4094 || height > 4094) {
|
||||
stbi_image_free(decoded);
|
||||
return false;
|
||||
}
|
||||
Image image;
|
||||
image.Guid = guid;
|
||||
image.Width = width;
|
||||
image.Height = height;
|
||||
image.Pixels.assign(decoded, decoded + static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4U);
|
||||
stbi_image_free(decoded);
|
||||
images.push_back(std::move(image));
|
||||
}
|
||||
|
||||
constexpr int maximumAtlasSize = 4096;
|
||||
int x = 1;
|
||||
int y = 1;
|
||||
int rowHeight = 0;
|
||||
int usedWidth = 1;
|
||||
resources.AtlasEntries.clear();
|
||||
for (const auto& image : images) {
|
||||
if (x + image.Width + 1 > maximumAtlasSize) {
|
||||
x = 1;
|
||||
y += rowHeight + 1;
|
||||
rowHeight = 0;
|
||||
}
|
||||
if (y + image.Height + 1 > maximumAtlasSize) return false;
|
||||
resources.AtlasEntries.push_back({image.Guid, x, y, image.Width, image.Height});
|
||||
usedWidth = std::max(usedWidth, x + image.Width + 1);
|
||||
rowHeight = std::max(rowHeight, image.Height);
|
||||
x += image.Width + 1;
|
||||
}
|
||||
const int usedHeight = y + rowHeight + 1;
|
||||
const auto nextPowerOfTwo = [](int value) {
|
||||
int result = 1;
|
||||
while (result < value) result *= 2;
|
||||
return result;
|
||||
};
|
||||
resources.AtlasWidth = nextPowerOfTwo(usedWidth);
|
||||
resources.AtlasHeight = nextPowerOfTwo(usedHeight);
|
||||
resources.AtlasFilename = "atlas.tga";
|
||||
pixels.assign(static_cast<std::size_t>(resources.AtlasWidth) *
|
||||
static_cast<std::size_t>(resources.AtlasHeight) * 4U, 0);
|
||||
for (std::size_t imageIndex = 0; imageIndex < images.size(); ++imageIndex) {
|
||||
const auto& image = images[imageIndex];
|
||||
const auto& placement = resources.AtlasEntries[imageIndex];
|
||||
for (int row = 0; row < image.Height; ++row) {
|
||||
const auto sourceOffset = static_cast<std::size_t>(row) * static_cast<std::size_t>(image.Width) * 4U;
|
||||
const auto targetOffset = (static_cast<std::size_t>(placement.Y + row) *
|
||||
static_cast<std::size_t>(resources.AtlasWidth) + static_cast<std::size_t>(placement.X)) * 4U;
|
||||
std::copy_n(image.Pixels.data() + sourceOffset, static_cast<std::size_t>(image.Width) * 4U,
|
||||
pixels.data() + targetOffset);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const auto writeAtlas = [](const std::filesystem::path& path, int width, int height,
|
||||
const std::vector<std::uint8_t>& pixels) {
|
||||
if (width <= 0 || height <= 0 || width > 65535 || height > 65535 ||
|
||||
pixels.size() != static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4U) return false;
|
||||
std::ofstream output(path, std::ios::binary | std::ios::trunc);
|
||||
if (!output) return false;
|
||||
std::array<std::uint8_t, 18> header{};
|
||||
header[2] = 2;
|
||||
header[12] = static_cast<std::uint8_t>(width & 0xff);
|
||||
header[13] = static_cast<std::uint8_t>((width >> 8) & 0xff);
|
||||
header[14] = static_cast<std::uint8_t>(height & 0xff);
|
||||
header[15] = static_cast<std::uint8_t>((height >> 8) & 0xff);
|
||||
header[16] = 32;
|
||||
header[17] = 0x28;
|
||||
output.write(reinterpret_cast<const char*>(header.data()), static_cast<std::streamsize>(header.size()));
|
||||
std::vector<std::uint8_t> bgra(pixels.size());
|
||||
for (std::size_t index = 0; index < pixels.size(); index += 4) {
|
||||
bgra[index] = pixels[index + 2];
|
||||
bgra[index + 1] = pixels[index + 1];
|
||||
bgra[index + 2] = pixels[index];
|
||||
bgra[index + 3] = pixels[index + 3];
|
||||
}
|
||||
output.write(reinterpret_cast<const char*>(bgra.data()), static_cast<std::streamsize>(bgra.size()));
|
||||
return output.good();
|
||||
};
|
||||
std::optional<MetaCoreSceneDocument> startupScene;
|
||||
if (!project.StartupScenePath.empty()) {
|
||||
const std::filesystem::path scenePath = project.RootPath / project.StartupScenePath;
|
||||
@ -8944,22 +9121,44 @@ bool MetaCoreBuiltinCookService::CookRuntimeUiAssets() {
|
||||
document = MetaCoreReadTypedPackagePayload<MetaCoreUiDocument>(*package, registry, "MetaCoreUiDocument");
|
||||
}
|
||||
if (!document.has_value()) return false;
|
||||
const MetaCoreCompiledUiDocument compiled = MetaCoreCompileUiDocument(*document);
|
||||
MetaCoreUiCompileResources resources;
|
||||
if (document->ThemeAssetGuid.IsValid()) {
|
||||
resources.Theme = loadTheme(document->ThemeAssetGuid);
|
||||
if (!resources.Theme.has_value()) return false;
|
||||
}
|
||||
if (document->FontFamilyAssetGuid.IsValid()) {
|
||||
resources.FontFamily = loadFontFamily(document->FontFamilyAssetGuid);
|
||||
if (!resources.FontFamily.has_value()) return false;
|
||||
}
|
||||
if (document->LocalizationAssetGuid.IsValid()) {
|
||||
resources.Localization = loadLocalization(document->LocalizationAssetGuid);
|
||||
if (!resources.Localization.has_value()) return false;
|
||||
}
|
||||
std::vector<std::uint8_t> atlasPixels;
|
||||
if (!buildAtlas(*document, resources, atlasPixels)) return false;
|
||||
const MetaCoreCompiledUiDocument compiled = MetaCoreCompileUiDocument(*document, resources);
|
||||
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;
|
||||
if (!compiled.LocalizationJson.empty() &&
|
||||
!writeText(outputDirectory / "localization.json", compiled.LocalizationJson)) return false;
|
||||
if (!resources.AtlasEntries.empty() && !writeAtlas(
|
||||
outputDirectory / "Assets" / resources.AtlasFilename,
|
||||
resources.AtlasWidth, resources.AtlasHeight, atlasPixels)) 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;
|
||||
if (dependencyRecord->Type != "texture" && dependencyRecord->Type != "asset") continue;
|
||||
if (dependencyRecord->Type == "texture" && !resources.AtlasEntries.empty()) 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"),
|
||||
const std::string extension = dependencyRecord->Type == "texture" ? ".png" : ".font";
|
||||
std::filesystem::copy_file(dependencySource, outputDirectory / "Assets" / (dependency.ToString() + extension),
|
||||
std::filesystem::copy_options::overwrite_existing, errorCode);
|
||||
if (errorCode) return false;
|
||||
}
|
||||
@ -8976,8 +9175,14 @@ bool MetaCoreBuiltinCookService::CookRuntimeUiAssets() {
|
||||
entry.InstanceId = object.Id;
|
||||
entry.SceneObjectId = object.Id;
|
||||
entry.RenderMode = static_cast<std::uint32_t>(object.UiRenderer->RenderMode);
|
||||
entry.TargetWidth = 0;
|
||||
entry.TargetHeight = 0;
|
||||
entry.PixelsPerUnit = object.UiRenderer->PixelsPerUnit;
|
||||
entry.WorldSize = object.UiRenderer->WorldSize;
|
||||
if (object.UiRenderer->RenderMode == MetaCoreUiRenderMode::WorldSpace) {
|
||||
const auto surface = MetaCoreComputeUiWorldSurfaceLayout(
|
||||
object.UiRenderer->WorldSize, object.UiRenderer->PixelsPerUnit);
|
||||
entry.TargetWidth = surface.TargetWidth;
|
||||
entry.TargetHeight = surface.TargetHeight;
|
||||
}
|
||||
cookedManifest.Documents.push_back(std::move(entry));
|
||||
}
|
||||
}
|
||||
@ -10935,6 +11140,14 @@ public:
|
||||
editorContext.GetActiveAnimationRuntime(), Input_ ? &Input_->GetRuntimeUi() : nullptr
|
||||
);
|
||||
Runtime_->Start();
|
||||
if (Input_) {
|
||||
(void)Input_->GetRuntimeUi().RegisterProvider(
|
||||
"scene",
|
||||
[this](std::string_view path) { return Runtime_ ? Runtime_->ReadSceneUiBinding(path) : std::nullopt; },
|
||||
[this](std::string_view path, const MetaCoreRuntimeUiValue& value) {
|
||||
return Runtime_ && Runtime_->WriteSceneUiBinding(path, value);
|
||||
});
|
||||
}
|
||||
editorContext.SetActiveScriptRuntime(Runtime_.get());
|
||||
if (Physics_ && Physics_->GetOrCreate(editorContext)) {
|
||||
Physics_->GetOrCreate(editorContext)->SetEventCallback([this](const MetaCorePhysicsContactEvent& event) {
|
||||
@ -10956,6 +11169,21 @@ public:
|
||||
});
|
||||
Input_->GetInteraction().SetEventCallback([this](const MetaCorePointerEvent& event) {
|
||||
if (!Runtime_) return;
|
||||
if (event.WorldSpaceUi) {
|
||||
using PointerType = MetaCoreRuntimeUiWorldPointerType;
|
||||
std::optional<PointerType> type;
|
||||
if (event.Type == MetaCorePointerEventType::Enter || event.Type == MetaCorePointerEventType::Move)
|
||||
type = PointerType::Move;
|
||||
else if (event.Type == MetaCorePointerEventType::Down) type = PointerType::Down;
|
||||
else if (event.Type == MetaCorePointerEventType::Up) type = PointerType::Up;
|
||||
else if (event.Type == MetaCorePointerEventType::Scroll) type = PointerType::Scroll;
|
||||
else if (event.Type == MetaCorePointerEventType::Leave ||
|
||||
event.Type == MetaCorePointerEventType::Cancel) type = PointerType::Cancel;
|
||||
if (type.has_value())
|
||||
(void)Input_->GetRuntimeUi().ProcessWorldPointer(
|
||||
event.TargetObjectId, event.SurfaceUv, *type, event.Button, event.ScrollDelta);
|
||||
return;
|
||||
}
|
||||
MetaCoreScriptValueV1 type{}; type.Type = MetaCoreScriptAbiValue_Int; type.IntValue = static_cast<std::int64_t>(event.Type);
|
||||
MetaCoreScriptValueV1 button{}; button.Type = MetaCoreScriptAbiValue_Int; button.IntValue = event.Button;
|
||||
MetaCoreScriptValueV1 screen{}; screen.Type = MetaCoreScriptAbiValue_Vec3; screen.Vec3[0] = event.ScreenPosition.x; screen.Vec3[1] = event.ScreenPosition.y;
|
||||
@ -11005,6 +11233,7 @@ public:
|
||||
void OnPlayStop(MetaCoreEditorContext& editorContext) override {
|
||||
editorContext.SetActiveScriptRuntime(nullptr);
|
||||
if (Input_) {
|
||||
(void)Input_->GetRuntimeUi().UnregisterProvider("scene");
|
||||
Input_->GetRuntime().Unsubscribe(InputSubscription_);
|
||||
Input_->GetInteraction().SetEventCallback({});
|
||||
Input_->GetRuntimeUi().SetEventCallback({});
|
||||
|
||||
@ -4516,6 +4516,7 @@ public:
|
||||
ImGui::TextDisabled("选择或新建一个 .mcui UI 资产。");
|
||||
return;
|
||||
}
|
||||
const MetaCoreUiDocument historyBefore = Document_;
|
||||
|
||||
const float hierarchyWidth = 230.0F;
|
||||
const float inspectorWidth = 330.0F;
|
||||
@ -4531,6 +4532,16 @@ public:
|
||||
ImGui::BeginChild("NativeUiInspector", ImVec2(inspectorWidth, 0.0F), true);
|
||||
DrawNativeInspector(editorContext, *assetDatabaseService);
|
||||
ImGui::EndChild();
|
||||
if (!HistoryApplying_) {
|
||||
const auto beforeBytes = MetaCoreSerializeToBytes(historyBefore, reflectionRegistry->GetTypeRegistry());
|
||||
const auto afterBytes = MetaCoreSerializeToBytes(Document_, reflectionRegistry->GetTypeRegistry());
|
||||
if (beforeBytes.has_value() && afterBytes.has_value() && *beforeBytes != *afterBytes) {
|
||||
UndoHistory_.push_back(historyBefore);
|
||||
if (UndoHistory_.size() > 100) UndoHistory_.erase(UndoHistory_.begin());
|
||||
RedoHistory_.clear();
|
||||
Dirty_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] bool HasDirtyDocument() const override {
|
||||
@ -4577,14 +4588,30 @@ private:
|
||||
SaveNativeDocument(editorContext, assetDatabaseService, packageService, reflectionRegistry);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(UndoHistory_.empty());
|
||||
if (ImGui::Button("撤销")) {
|
||||
RedoHistory_.push_back(Document_);
|
||||
Document_ = std::move(UndoHistory_.back());
|
||||
UndoHistory_.pop_back();
|
||||
HistoryApplying_ = true;
|
||||
Dirty_ = true;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(RedoHistory_.empty());
|
||||
if (ImGui::Button("重做")) {
|
||||
UndoHistory_.push_back(Document_);
|
||||
Document_ = std::move(RedoHistory_.back());
|
||||
RedoHistory_.pop_back();
|
||||
HistoryApplying_ = true;
|
||||
Dirty_ = true;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("重新加载")) {
|
||||
ReloadNativeDocument(editorContext, assetDatabaseService, packageService, reflectionRegistry);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("导出到运行时")) {
|
||||
ExportNativeDocument(editorContext, assetDatabaseService);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
const char* placementModes[] = {"2D 屏幕", "3D 场景"};
|
||||
int placementMode = static_cast<int>(PlacementRenderMode_);
|
||||
ImGui::SetNextItemWidth(92.0F);
|
||||
@ -4600,6 +4627,7 @@ private:
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(0.95F, 0.72F, 0.32F, 1.0F), "未保存");
|
||||
}
|
||||
HistoryApplying_ = false;
|
||||
}
|
||||
|
||||
void DrawNativeHierarchyNode(const std::string& nodeId) {
|
||||
@ -4734,11 +4762,14 @@ private:
|
||||
const MetaCoreUiNodeDocument& node,
|
||||
const ImVec2& rectMin,
|
||||
const ImVec2& rectMax,
|
||||
const std::string& label
|
||||
const std::string& label,
|
||||
ImFont* font,
|
||||
float fontSize,
|
||||
float canvasScale
|
||||
) const {
|
||||
const ImVec2 textSize = ImGui::CalcTextSize(label.c_str());
|
||||
const float paddingX = std::max(3.0F, node.Style.Padding.x);
|
||||
const float paddingY = std::max(3.0F, node.Style.Padding.y);
|
||||
const ImVec2 textSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0F, label.c_str());
|
||||
const float paddingX = std::max(1.0F, node.Style.Padding.x * canvasScale);
|
||||
const float paddingY = std::max(1.0F, node.Style.Padding.y * canvasScale);
|
||||
float x = rectMin.x + paddingX;
|
||||
float y = rectMin.y + paddingY;
|
||||
|
||||
@ -4815,6 +4846,8 @@ private:
|
||||
const bool isButton = node.Type == MetaCoreUiNodeType::Button;
|
||||
const bool isImage = node.Type == MetaCoreUiNodeType::Image;
|
||||
const bool isPanel = node.Type == MetaCoreUiNodeType::Panel;
|
||||
ImFont* const previewFont = ImGui::GetFont();
|
||||
const float previewFontSize = std::max(1.0F, node.Style.FontSize * scale);
|
||||
const ImVec2 mousePosition = ImGui::GetMousePos();
|
||||
const bool nodeHovered =
|
||||
ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) &&
|
||||
@ -4888,8 +4921,12 @@ private:
|
||||
drawList->AddLine(visualMin, visualMax, IM_COL32(220, 230, 245, 128), 1.0F);
|
||||
drawList->AddLine(ImVec2(visualMax.x, visualMin.y), ImVec2(visualMin.x, visualMax.y), IM_COL32(220, 230, 245, 128), 1.0F);
|
||||
const std::string imageLabel = MetaCoreNativeUiTextureLabel(assetDatabaseService, node.Style.ImageAssetGuid);
|
||||
const ImVec2 labelPos = ComputeNativeTextPosition(node, visualMin, visualMax, imageLabel);
|
||||
drawList->AddText(labelPos, IM_COL32(235, 240, 248, 230), imageLabel.c_str());
|
||||
const ImVec2 labelPos = ComputeNativeTextPosition(
|
||||
node, visualMin, visualMax, imageLabel, previewFont, previewFontSize, scale
|
||||
);
|
||||
drawList->AddText(
|
||||
previewFont, previewFontSize, labelPos, IM_COL32(235, 240, 248, 230), imageLabel.c_str()
|
||||
);
|
||||
} else if (isText || isButton) {
|
||||
const std::string label = node.Text.empty() ? node.Name : node.Text;
|
||||
if (!label.empty()) {
|
||||
@ -4897,7 +4934,10 @@ private:
|
||||
? glm::vec3(0.55F, 0.58F, 0.64F)
|
||||
: node.Style.TextColor;
|
||||
const ImU32 textColor = MetaCoreUiColorToU32(textColorValue, 1.0F);
|
||||
drawList->AddText(ComputeNativeTextPosition(node, visualMin, visualMax, label), textColor, label.c_str());
|
||||
const ImVec2 textPosition = ComputeNativeTextPosition(
|
||||
node, visualMin, visualMax, label, previewFont, previewFontSize, scale
|
||||
);
|
||||
drawList->AddText(previewFont, previewFontSize, textPosition, textColor, label.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@ -5308,8 +5348,33 @@ private:
|
||||
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_ |= ImGui::DragFloat("最小宽高比", &Document_.MinAspectRatio, 0.01F, 0.0F, 10.0F);
|
||||
Dirty_ |= ImGui::DragFloat("最大宽高比", &Document_.MaxAspectRatio, 0.01F, 0.0F, 10.0F);
|
||||
Dirty_ |= DrawNativeString("默认 Locale", Document_.DefaultLocale, 64);
|
||||
Dirty_ |= DrawNativeString("Fallback Locale", Document_.FallbackLocale, 64);
|
||||
const auto drawResource = [&](const char* label, const char* type, MetaCoreAssetGuid& guid) {
|
||||
std::string preview = "无";
|
||||
if (const auto selected = assetDatabaseService.FindAssetByGuid(guid); selected.has_value())
|
||||
preview = selected->RelativePath.generic_string();
|
||||
bool changed = false;
|
||||
if (ImGui::BeginCombo(label, preview.c_str())) {
|
||||
if (ImGui::Selectable("无", !guid.IsValid())) { guid = {}; changed = true; }
|
||||
for (const auto& record : assetDatabaseService.GetAssetRegistry()) {
|
||||
if (record.Type != type) continue;
|
||||
const bool selected = record.Guid == guid;
|
||||
if (ImGui::Selectable(record.RelativePath.generic_string().c_str(), selected)) {
|
||||
guid = record.Guid;
|
||||
changed = true;
|
||||
}
|
||||
if (selected) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
return changed;
|
||||
};
|
||||
Dirty_ |= drawResource("主题资产", "ui_theme", Document_.ThemeAssetGuid);
|
||||
Dirty_ |= drawResource("字体族资产", "ui_font_family", Document_.FontFamilyAssetGuid);
|
||||
Dirty_ |= drawResource("本地化资产", "ui_localization", Document_.LocalizationAssetGuid);
|
||||
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);
|
||||
@ -5440,7 +5505,31 @@ private:
|
||||
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) {
|
||||
Dirty_ |= ImGui::DragInt("选中索引", &node->SelectedIndex, 1.0F, -1, 100000);
|
||||
Dirty_ |= DrawNativeString("行模板节点 ID", node->ItemTemplateNodeId, 128);
|
||||
if (ImGui::Button("添加静态项")) {
|
||||
const std::string id = "item-" + std::to_string(node->StaticItems.size() + 1);
|
||||
node->StaticItems.push_back({id, "Item", id, {}});
|
||||
Dirty_ = true;
|
||||
}
|
||||
for (std::size_t itemIndex = 0; itemIndex < node->StaticItems.size(); ++itemIndex) {
|
||||
ImGui::PushID(static_cast<int>(itemIndex));
|
||||
ImGui::SeparatorText("静态项");
|
||||
auto& item = node->StaticItems[itemIndex];
|
||||
Dirty_ |= DrawNativeString("项 ID", item.Id, 96);
|
||||
Dirty_ |= DrawNativeString("项文本", item.Text, 192);
|
||||
Dirty_ |= DrawNativeString("项值", item.Value, 128);
|
||||
Dirty_ |= DrawNativeString("项本地化 Key", item.LocalizationKey, 128);
|
||||
if (ImGui::SmallButton("删除静态项")) {
|
||||
node->StaticItems.erase(node->StaticItems.begin() + static_cast<std::ptrdiff_t>(itemIndex));
|
||||
Dirty_ = true;
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
if (isList || isScroll) Dirty_ |= ImGui::Checkbox("裁剪子节点", &node->RectTransform.ClipChildren);
|
||||
|
||||
ImGui::Separator();
|
||||
@ -5476,6 +5565,14 @@ private:
|
||||
Dirty_ = true;
|
||||
}
|
||||
Dirty_ |= DrawNativeString("Format", binding.Format, 96);
|
||||
const char* converterLabels[] = {"", "not", "to_string", "uppercase", "lowercase", "to_bool", "to_int", "to_double"};
|
||||
int converterIndex = 0;
|
||||
for (int candidate = 1; candidate < IM_ARRAYSIZE(converterLabels); ++candidate)
|
||||
if (binding.Converter == converterLabels[candidate]) converterIndex = candidate;
|
||||
if (ImGui::Combo("Converter", &converterIndex, converterLabels, IM_ARRAYSIZE(converterLabels))) {
|
||||
binding.Converter = converterLabels[converterIndex];
|
||||
Dirty_ = true;
|
||||
}
|
||||
Dirty_ |= DrawNativeString("Fallback", binding.Fallback, 128);
|
||||
if (ImGui::SmallButton("删除绑定")) {
|
||||
node->Bindings.erase(node->Bindings.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
@ -5486,6 +5583,42 @@ private:
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
ImGui::SeparatorText("动效");
|
||||
if (ImGui::Button("添加动效")) {
|
||||
node->Animations.push_back({"animation-" + std::to_string(node->Animations.size() + 1),
|
||||
MetaCoreUiAnimationTrigger::Show, MetaCoreUiAnimationProperty::Opacity,
|
||||
{0.0F, 0.0F, 0.0F}, {1.0F, 0.0F, 0.0F}, 0.2F, 0.0F, MetaCoreUiEasing::EaseOut});
|
||||
Dirty_ = true;
|
||||
}
|
||||
for (std::size_t animationIndex = 0; animationIndex < node->Animations.size(); ++animationIndex) {
|
||||
ImGui::PushID(static_cast<int>(animationIndex));
|
||||
auto& animation = node->Animations[animationIndex];
|
||||
Dirty_ |= DrawNativeString("动效 ID", animation.Id, 96);
|
||||
const char* triggerLabels[] = {"Show", "Hide", "Hover", "Pressed", "Focus", "Custom"};
|
||||
int trigger = static_cast<int>(animation.Trigger);
|
||||
if (ImGui::Combo("触发", &trigger, triggerLabels, IM_ARRAYSIZE(triggerLabels))) {
|
||||
animation.Trigger = static_cast<MetaCoreUiAnimationTrigger>(std::clamp(trigger, 0, 5));
|
||||
Dirty_ = true;
|
||||
}
|
||||
const char* propertyLabels[] = {"Opacity", "Translation", "Scale", "Color"};
|
||||
int property = static_cast<int>(animation.Property);
|
||||
if (ImGui::Combo("属性", &property, propertyLabels, IM_ARRAYSIZE(propertyLabels))) {
|
||||
animation.Property = static_cast<MetaCoreUiAnimationProperty>(std::clamp(property, 0, 3));
|
||||
Dirty_ = true;
|
||||
}
|
||||
Dirty_ |= ImGui::DragFloat3("起始值", &animation.From.x, 0.01F);
|
||||
Dirty_ |= ImGui::DragFloat3("结束值", &animation.To.x, 0.01F);
|
||||
Dirty_ |= ImGui::DragFloat("时长", &animation.DurationSeconds, 0.01F, 0.0F, 60.0F);
|
||||
Dirty_ |= ImGui::DragFloat("延迟", &animation.DelaySeconds, 0.01F, 0.0F, 60.0F);
|
||||
if (ImGui::SmallButton("删除动效")) {
|
||||
node->Animations.erase(node->Animations.begin() + static_cast<std::ptrdiff_t>(animationIndex));
|
||||
Dirty_ = true;
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
ImGui::Separator();
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (ImGui::Button("删除节点")) {
|
||||
DeleteNativeNode(node->Id);
|
||||
Dirty_ = true;
|
||||
@ -5678,6 +5811,8 @@ private:
|
||||
LoadedRecord_ = record;
|
||||
Document_ = *document;
|
||||
SelectedNodeId_ = Document_.RootNodeIds.empty() ? std::string{} : Document_.RootNodeIds.front();
|
||||
UndoHistory_.clear();
|
||||
RedoHistory_.clear();
|
||||
Dirty_ = false;
|
||||
return true;
|
||||
}
|
||||
@ -5722,6 +5857,8 @@ private:
|
||||
Document_.Name = NewUiNameBuffer_.data()[0] != '\0' ? NewUiNameBuffer_.data() : createdPath.stem().string();
|
||||
Document_.ReferenceWidth = 1920;
|
||||
Document_.ReferenceHeight = 1080;
|
||||
UndoHistory_.clear();
|
||||
RedoHistory_.clear();
|
||||
SelectedNodeId_.clear();
|
||||
AddNativeNode(MetaCoreUiNodeType::Panel);
|
||||
SaveNativeDocument(editorContext, assetDatabaseService, packageService, reflectionRegistry);
|
||||
@ -5744,119 +5881,6 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] MetaCoreNativeUiImageSourceMap PrepareNativeImageRuntimeSources(
|
||||
MetaCoreEditorContext& editorContext,
|
||||
MetaCoreIAssetDatabaseService& assetDatabaseService,
|
||||
const std::filesystem::path& generatedDirectory
|
||||
) {
|
||||
MetaCoreNativeUiImageSourceMap imageSources;
|
||||
const MetaCoreProjectDescriptor& project = assetDatabaseService.GetProjectDescriptor();
|
||||
const std::filesystem::path imageDirectoryRelative = generatedDirectory / "Images";
|
||||
const std::filesystem::path imageDirectoryAbsolute = project.UiPath / imageDirectoryRelative;
|
||||
std::error_code errorCode;
|
||||
std::filesystem::create_directories(imageDirectoryAbsolute, errorCode);
|
||||
if (errorCode) {
|
||||
editorContext.AddConsoleMessage(MetaCoreLogLevel::Warning, "UI", "无法创建 UI 图片导出目录");
|
||||
return imageSources;
|
||||
}
|
||||
|
||||
std::unordered_set<MetaCoreAssetGuid, MetaCoreAssetGuidHasher> copiedGuids;
|
||||
for (const MetaCoreUiNodeDocument& node : Document_.Nodes) {
|
||||
if (node.Type != MetaCoreUiNodeType::Image || !node.Style.ImageAssetGuid.IsValid()) {
|
||||
continue;
|
||||
}
|
||||
if (!copiedGuids.insert(node.Style.ImageAssetGuid).second) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::filesystem::path sourcePath =
|
||||
MetaCoreNativeUiResolveTextureSourcePath(assetDatabaseService, node.Style.ImageAssetGuid);
|
||||
if (sourcePath.empty()) {
|
||||
editorContext.AddConsoleMessage(
|
||||
MetaCoreLogLevel::Warning,
|
||||
"UI",
|
||||
"跳过缺失的 UI 图片贴图: " + node.Style.ImageAssetGuid.ToString()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
std::filesystem::path extension = sourcePath.extension();
|
||||
if (extension.empty()) {
|
||||
extension = ".png";
|
||||
}
|
||||
const std::filesystem::path targetFilename = node.Style.ImageAssetGuid.ToString() + extension.string();
|
||||
const std::filesystem::path targetAbsolute = imageDirectoryAbsolute / targetFilename;
|
||||
errorCode.clear();
|
||||
if (!std::filesystem::equivalent(sourcePath, targetAbsolute, errorCode)) {
|
||||
errorCode.clear();
|
||||
std::filesystem::copy_file(sourcePath, targetAbsolute, std::filesystem::copy_options::overwrite_existing, errorCode);
|
||||
if (errorCode) {
|
||||
editorContext.AddConsoleMessage(
|
||||
MetaCoreLogLevel::Warning,
|
||||
"UI",
|
||||
"复制 UI 图片失败: " + sourcePath.generic_string()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
imageSources[node.Style.ImageAssetGuid] = std::filesystem::path("Images") / targetFilename;
|
||||
}
|
||||
return imageSources;
|
||||
}
|
||||
|
||||
void ExportNativeDocument(MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService) {
|
||||
if (!LoadedRecord_.has_value()) {
|
||||
return;
|
||||
}
|
||||
const MetaCoreProjectDescriptor& project = assetDatabaseService.GetProjectDescriptor();
|
||||
std::filesystem::path generatedDirectory = std::filesystem::path("Generated");
|
||||
std::filesystem::path rmlPath = generatedDirectory / (Document_.Name.empty() ? LoadedRecord_->RelativePath.stem() : std::filesystem::path(Document_.Name));
|
||||
rmlPath.replace_extension(".rml");
|
||||
std::filesystem::path rcssPath = rmlPath;
|
||||
rcssPath.replace_extension(".rcss");
|
||||
const MetaCoreNativeUiImageSourceMap imageSources =
|
||||
PrepareNativeImageRuntimeSources(editorContext, assetDatabaseService, generatedDirectory);
|
||||
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;
|
||||
}
|
||||
|
||||
MetaCoreRuntimeUiManifest& manifest = editorContext.AccessRuntimeUiManifestDocument();
|
||||
if (!MetaCoreRuntimeUiManifestHasPath(manifest, rmlPath)) {
|
||||
manifest.Documents.push_back(MetaCoreRuntimeUiDocumentEntry{
|
||||
MetaCoreBuildRuntimeUiDocumentId(manifest, rmlPath),
|
||||
rmlPath,
|
||||
manifest.Documents.empty() ? 0 : manifest.Documents.back().Layer + 10,
|
||||
true,
|
||||
true
|
||||
});
|
||||
}
|
||||
(void)editorContext.SaveRuntimeDataConfig();
|
||||
editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "UI", "已导出 UI 到运行时 RML");
|
||||
}
|
||||
|
||||
void AddNativeDocumentToScene(
|
||||
MetaCoreEditorContext& editorContext,
|
||||
MetaCoreIAssetDatabaseService& assetDatabaseService,
|
||||
@ -5899,6 +5923,11 @@ private:
|
||||
transform.Position = glm::vec3(0.0F, 0.0F, 0.0F);
|
||||
transform.RotationEulerDegrees = glm::vec3(0.0F, 0.0F, 0.0F);
|
||||
transform.Scale = glm::vec3(1.0F, 1.0F, 1.0F);
|
||||
MetaCoreColliderComponent collider;
|
||||
collider.Shape = MetaCoreColliderShape::Box;
|
||||
collider.Size = glm::vec3(uiRenderer.WorldSize.x, uiRenderer.WorldSize.y, 0.01F);
|
||||
collider.IsTrigger = true;
|
||||
uiObject.AddComponent<MetaCoreColliderComponent>(collider);
|
||||
}
|
||||
|
||||
editorContext.SelectOnly(createdObjectId);
|
||||
@ -5921,6 +5950,9 @@ private:
|
||||
float CanvasZoom_ = 1.0F;
|
||||
bool SnapToGrid_ = true;
|
||||
float GridSize_ = 8.0F;
|
||||
std::vector<MetaCoreUiDocument> UndoHistory_{};
|
||||
std::vector<MetaCoreUiDocument> RedoHistory_{};
|
||||
bool HistoryApplying_ = false;
|
||||
};
|
||||
|
||||
struct MetaCoreGeneratedResourceListEntry {
|
||||
|
||||
@ -151,6 +151,12 @@ void MetaCoreEditorViewportRenderer::SetRuntimeOverlayRenderCallback(
|
||||
FilamentSceneBridge_.SetRuntimeOverlayRenderCallback(std::move(callback));
|
||||
}
|
||||
|
||||
void MetaCoreEditorViewportRenderer::SetRuntimeWorldUiRenderCallback(
|
||||
MetaCoreFilamentSceneBridge::RuntimeWorldUiRenderCallback callback
|
||||
) {
|
||||
FilamentSceneBridge_.SetRuntimeWorldUiRenderCallback(std::move(callback));
|
||||
}
|
||||
|
||||
uint32_t MetaCoreEditorViewportRenderer::GetFilamentGLTextureId() const {
|
||||
return FilamentSceneBridge_.GetGLTextureId();
|
||||
}
|
||||
|
||||
@ -468,6 +468,9 @@ public:
|
||||
void SetRuntimeOverlayRenderCallback(MetaCoreFilamentSceneBridge::RuntimeOverlayRenderCallback callback) {
|
||||
RuntimeOverlayRenderCallback_ = std::move(callback);
|
||||
}
|
||||
void SetRuntimeWorldUiRenderCallback(MetaCoreFilamentSceneBridge::RuntimeWorldUiRenderCallback callback) {
|
||||
RuntimeWorldUiRenderCallback_ = std::move(callback);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool VerifyEditorGridVisibleForTesting() const {
|
||||
return EditorGridEntityInScene_ && EditorGridAxisEntityInScene_;
|
||||
@ -2697,6 +2700,8 @@ public:
|
||||
}
|
||||
|
||||
if (Renderer_->beginFrame(SwapChain_)) {
|
||||
if (RuntimeWorldUiRenderCallback_ && Scene_ != nullptr)
|
||||
RuntimeWorldUiRenderCallback_(*Engine_, *Renderer_, *Scene_);
|
||||
const bool renderEditorGrid = GridView_ != nullptr &&
|
||||
(EditorGridEntityInScene_ || EditorGridAxisEntityInScene_);
|
||||
const auto renderScenePass = [&](bool outputToRenderTarget) {
|
||||
@ -4161,6 +4166,7 @@ private:
|
||||
filament::View* UIView_ = nullptr;
|
||||
MetaCoreImGuiHelper* ImGuiHelper_ = nullptr;
|
||||
MetaCoreFilamentSceneBridge::RuntimeOverlayRenderCallback RuntimeOverlayRenderCallback_{};
|
||||
MetaCoreFilamentSceneBridge::RuntimeWorldUiRenderCallback RuntimeWorldUiRenderCallback_{};
|
||||
|
||||
GLuint GLTextureId_ = 0;
|
||||
struct PendingRenderTargetResource {
|
||||
@ -4229,6 +4235,10 @@ void MetaCoreFilamentSceneBridge::SetRuntimeOverlayRenderCallback(RuntimeOverlay
|
||||
Impl_->SetRuntimeOverlayRenderCallback(std::move(callback));
|
||||
}
|
||||
|
||||
void MetaCoreFilamentSceneBridge::SetRuntimeWorldUiRenderCallback(RuntimeWorldUiRenderCallback callback) {
|
||||
Impl_->SetRuntimeWorldUiRenderCallback(std::move(callback));
|
||||
}
|
||||
|
||||
void MetaCoreFilamentSceneBridge::RenderAll() {
|
||||
Impl_->RenderAll();
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ public:
|
||||
void SetRenderDebugView(MetaCoreRenderDebugView debugView);
|
||||
[[nodiscard]] MetaCoreRenderDebugView GetRenderDebugView() const;
|
||||
void SetRuntimeOverlayRenderCallback(MetaCoreFilamentSceneBridge::RuntimeOverlayRenderCallback callback);
|
||||
void SetRuntimeWorldUiRenderCallback(MetaCoreFilamentSceneBridge::RuntimeWorldUiRenderCallback callback);
|
||||
[[nodiscard]] uint32_t GetFilamentGLTextureId() const;
|
||||
[[nodiscard]] void* GetFilamentTexturePointer() const;
|
||||
[[nodiscard]] MetaCoreVisibilityStatistics GetRenderingStatistics() const;
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
namespace filament {
|
||||
class Engine;
|
||||
class Renderer;
|
||||
class Scene;
|
||||
}
|
||||
|
||||
namespace MetaCore {
|
||||
@ -42,6 +43,11 @@ public:
|
||||
uint32_t width,
|
||||
uint32_t height
|
||||
)>;
|
||||
using RuntimeWorldUiRenderCallback = std::function<void(
|
||||
filament::Engine& engine,
|
||||
filament::Renderer& renderer,
|
||||
filament::Scene& scene
|
||||
)>;
|
||||
|
||||
MetaCoreFilamentSceneBridge();
|
||||
~MetaCoreFilamentSceneBridge();
|
||||
@ -57,6 +63,7 @@ public:
|
||||
void SetRenderDebugView(MetaCoreRenderDebugView debugView);
|
||||
[[nodiscard]] MetaCoreRenderDebugView GetRenderDebugView() const;
|
||||
void SetRuntimeOverlayRenderCallback(RuntimeOverlayRenderCallback callback);
|
||||
void SetRuntimeWorldUiRenderCallback(RuntimeWorldUiRenderCallback callback);
|
||||
void RenderAll();
|
||||
[[nodiscard]] bool RequestPick(std::uint32_t x, std::uint32_t y, PickCallback callback);
|
||||
[[nodiscard]] uint32_t GetGLTextureId() const;
|
||||
|
||||
@ -172,6 +172,12 @@ struct MetaCoreRuntimeUiDocumentEntry {
|
||||
|
||||
MC_PROPERTY()
|
||||
std::int32_t TargetHeight = 0;
|
||||
|
||||
MC_PROPERTY()
|
||||
float PixelsPerUnit = 1000.0F;
|
||||
|
||||
MC_PROPERTY()
|
||||
glm::vec3 WorldSize{1.92F, 1.08F, 0.0F};
|
||||
};
|
||||
|
||||
MC_STRUCT()
|
||||
|
||||
@ -6,8 +6,10 @@
|
||||
#include "MetaCoreRender/MetaCoreEditorViewportRenderer.h"
|
||||
#include "MetaCoreRender/MetaCoreRenderTypes.h"
|
||||
#include "MetaCoreScene/MetaCoreScene.h"
|
||||
#include "MetaCoreScene/MetaCoreTransformUtils.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <glm/gtc/matrix_inverse.hpp>
|
||||
#include <cmath>
|
||||
#include <deque>
|
||||
#include <limits>
|
||||
@ -39,13 +41,24 @@ public:
|
||||
MetaCoreId Captured = 0;
|
||||
glm::vec2 PressPosition{0.0F};
|
||||
glm::vec2 PreviousPosition{0.0F};
|
||||
glm::vec3 HoverWorld{0.0F};
|
||||
glm::vec2 HoverSurfaceUv{-1.0F};
|
||||
bool HoverIsWorldUi = false;
|
||||
bool Dragging = false;
|
||||
|
||||
void Emit(MetaCorePointerEventType type, MetaCoreId target, glm::vec2 position, glm::vec3 world = {}, std::uint32_t button = 0) {
|
||||
if (Callback && target != 0) Callback({type, target, button, position, position - PreviousPosition, world});
|
||||
void Emit(MetaCorePointerEventType type, MetaCoreId target, glm::vec2 position, glm::vec3 world = {},
|
||||
std::uint32_t button = 0, glm::vec2 surfaceUv = {-1.0F, -1.0F}, bool worldSpaceUi = false,
|
||||
float scrollDelta = 0.0F) {
|
||||
if (Callback && target != 0)
|
||||
Callback({type, target, button, position, position - PreviousPosition, world, surfaceUv, worldSpaceUi, scrollDelta});
|
||||
}
|
||||
bool IsInteractive(MetaCoreScene& scene, MetaCoreId id, std::uint32_t mask) const {
|
||||
auto object = scene.FindGameObject(id);
|
||||
if (object && object.HasComponent<MetaCoreUiRendererComponent>()) {
|
||||
const auto& ui = object.GetComponent<MetaCoreUiRendererComponent>();
|
||||
if (ui.RenderMode == MetaCoreUiRenderMode::WorldSpace && ui.Visible && ui.InputEnabled)
|
||||
return (mask & (MetaCoreInteraction_Hover | MetaCoreInteraction_Click)) != 0;
|
||||
}
|
||||
return object && object.HasComponent<MetaCoreInteractableComponent>() && object.GetComponent<MetaCoreInteractableComponent>().Enabled &&
|
||||
object.HasComponent<MetaCoreMeshRendererComponent>() && object.GetComponent<MetaCoreMeshRendererComponent>().Visible &&
|
||||
(object.GetComponent<MetaCoreInteractableComponent>().EventMask & mask) != 0;
|
||||
@ -63,6 +76,9 @@ void MetaCoreRuntimeInteraction::Cancel() {
|
||||
if (Impl_->Captured) Impl_->Emit(MetaCorePointerEventType::Cancel, Impl_->Captured, Impl_->PreviousPosition);
|
||||
if (Impl_->Hovered) Impl_->Emit(MetaCorePointerEventType::Leave, Impl_->Hovered, Impl_->PreviousPosition);
|
||||
Impl_->Captured = Impl_->Hovered = 0; Impl_->Dragging = false;
|
||||
Impl_->HoverWorld = {};
|
||||
Impl_->HoverSurfaceUv = {-1.0F, -1.0F};
|
||||
Impl_->HoverIsWorldUi = false;
|
||||
++Impl_->InteractionRevision;
|
||||
}
|
||||
|
||||
@ -110,21 +126,41 @@ void MetaCoreRuntimeInteraction::Update(MetaCoreScene& scene, const PickRequest&
|
||||
std::sort(completed.begin(), completed.end(), [](const auto& a, const auto& b){ return a.Sequence < b.Sequence; });
|
||||
const auto processResult = [&](const Impl::Completed& result) {
|
||||
if (result.SceneRevision != scene.GetRevision() || result.InteractionRevision != Impl_->InteractionRevision) return;
|
||||
const auto worldUi = [&]() -> std::pair<glm::vec2, bool> {
|
||||
auto object = scene.FindGameObject(result.Object);
|
||||
if (!object || !object.HasComponent<MetaCoreUiRendererComponent>()) return {{-1.0F, -1.0F}, false};
|
||||
const auto& ui = object.GetComponent<MetaCoreUiRendererComponent>();
|
||||
if (ui.RenderMode != MetaCoreUiRenderMode::WorldSpace || !ui.Visible || !ui.InputEnabled)
|
||||
return {{-1.0F, -1.0F}, false};
|
||||
const auto buildWorld = [&](const auto& self, MetaCoreGameObject current) -> glm::mat4 {
|
||||
const glm::mat4 local = MetaCoreBuildTransformMatrix(current.GetComponent<MetaCoreTransformComponent>());
|
||||
const auto parent = scene.FindGameObject(current.GetParentId());
|
||||
return parent ? self(self, parent) * local : local;
|
||||
};
|
||||
const glm::vec4 local = glm::inverse(buildWorld(buildWorld, object)) * glm::vec4(result.World, 1.0F);
|
||||
if (ui.WorldSize.x <= 0.0F || ui.WorldSize.y <= 0.0F) return {{-1.0F, -1.0F}, false};
|
||||
const glm::vec2 uv{local.x / ui.WorldSize.x + 0.5F, 0.5F - local.y / ui.WorldSize.y};
|
||||
if (uv.x < 0.0F || uv.y < 0.0F || uv.x > 1.0F || uv.y > 1.0F) return {{-1.0F, -1.0F}, false};
|
||||
return {uv, true};
|
||||
}();
|
||||
if (result.Kind == Impl::QueryKind::Hover) {
|
||||
if (result.Sequence != Impl_->LatestHoverSequence) return;
|
||||
const MetaCoreId next = Impl_->IsInteractive(scene, result.Object, MetaCoreInteraction_Hover) ? result.Object : 0;
|
||||
if (next != Impl_->Hovered) { if (Impl_->Hovered) Impl_->Emit(MetaCorePointerEventType::Leave, Impl_->Hovered, result.Position); Impl_->Hovered = next; if (next) Impl_->Emit(MetaCorePointerEventType::Enter, next, result.Position, result.World); }
|
||||
else if (next) Impl_->Emit(MetaCorePointerEventType::Move, next, result.Position, result.World);
|
||||
if (next != Impl_->Hovered) { if (Impl_->Hovered) Impl_->Emit(MetaCorePointerEventType::Leave, Impl_->Hovered, result.Position); Impl_->Hovered = next; if (next) Impl_->Emit(MetaCorePointerEventType::Enter, next, result.Position, result.World, 0, worldUi.first, worldUi.second); }
|
||||
else if (next) Impl_->Emit(MetaCorePointerEventType::Move, next, result.Position, result.World, 0, worldUi.first, worldUi.second);
|
||||
Impl_->HoverWorld = result.World;
|
||||
Impl_->HoverSurfaceUv = worldUi.first;
|
||||
Impl_->HoverIsWorldUi = worldUi.second;
|
||||
} else if (result.Kind == Impl::QueryKind::Down) {
|
||||
if (Impl_->IsInteractive(scene, result.Object, MetaCoreInteraction_Click | MetaCoreInteraction_Drag)) {
|
||||
Impl_->Captured = result.Object; Impl_->PressPosition = result.Position; Impl_->Dragging = false;
|
||||
Impl_->Emit(MetaCorePointerEventType::Down, result.Object, result.Position, result.World);
|
||||
Impl_->Emit(MetaCorePointerEventType::Down, result.Object, result.Position, result.World, 0, worldUi.first, worldUi.second);
|
||||
}
|
||||
} else if (result.Kind == Impl::QueryKind::Up && Impl_->Captured) {
|
||||
const MetaCoreId target = Impl_->Captured;
|
||||
Impl_->Emit(MetaCorePointerEventType::Up, target, result.Position, result.World);
|
||||
Impl_->Emit(MetaCorePointerEventType::Up, target, result.Position, result.World, 0, worldUi.first, worldUi.second);
|
||||
if (Impl_->Dragging) Impl_->Emit(MetaCorePointerEventType::DragEnd, target, result.Position, result.World);
|
||||
else if (result.Object == target && Impl_->IsInteractive(scene, target, MetaCoreInteraction_Click)) Impl_->Emit(MetaCorePointerEventType::Click, target, result.Position, result.World);
|
||||
else if (result.Object == target && Impl_->IsInteractive(scene, target, MetaCoreInteraction_Click)) Impl_->Emit(MetaCorePointerEventType::Click, target, result.Position, result.World, 0, worldUi.first, worldUi.second);
|
||||
Impl_->Captured = 0; Impl_->Dragging = false;
|
||||
}
|
||||
};
|
||||
@ -140,6 +176,9 @@ void MetaCoreRuntimeInteraction::Update(MetaCoreScene& scene, const PickRequest&
|
||||
Impl_->PointerSequenceOrder.pop_front();
|
||||
}
|
||||
const glm::vec2 cursor = input.GetCursorPosition();
|
||||
if (Impl_->Hovered != 0 && std::abs(input.GetMouseWheelDelta()) > 0.001F)
|
||||
Impl_->Emit(MetaCorePointerEventType::Scroll, Impl_->Hovered, cursor, Impl_->HoverWorld,
|
||||
0, Impl_->HoverSurfaceUv, Impl_->HoverIsWorldUi, input.GetMouseWheelDelta());
|
||||
if (Impl_->Captured && !Impl_->IsInteractive(scene, Impl_->Captured, MetaCoreInteraction_Click | MetaCoreInteraction_Drag)) Cancel();
|
||||
if (!input.IsWindowFocused() || mouseConsumed || cursor.x < viewport.Left || cursor.y < viewport.Top || cursor.x >= viewport.Left + viewport.Width || cursor.y >= viewport.Top + viewport.Height) { Cancel(); Impl_->PreviousPosition = cursor; return; }
|
||||
if (Impl_->Captured && input.IsMouseButtonDown(MetaCoreMouseButton::Left)) {
|
||||
|
||||
@ -20,7 +20,7 @@ class MetaCoreEditorViewportRenderer;
|
||||
struct MetaCoreViewportRect;
|
||||
struct MetaCoreSceneView;
|
||||
|
||||
enum class MetaCorePointerEventType : std::uint8_t { Enter, Move, Leave, Down, Up, Click, DragStart, Drag, DragEnd, Cancel };
|
||||
enum class MetaCorePointerEventType : std::uint8_t { Enter, Move, Leave, Down, Up, Click, Scroll, DragStart, Drag, DragEnd, Cancel };
|
||||
struct MetaCorePointerEvent {
|
||||
MetaCorePointerEventType Type = MetaCorePointerEventType::Move;
|
||||
MetaCoreId TargetObjectId = 0;
|
||||
@ -28,6 +28,9 @@ struct MetaCorePointerEvent {
|
||||
glm::vec2 ScreenPosition{0.0F};
|
||||
glm::vec2 ScreenDelta{0.0F};
|
||||
glm::vec3 WorldPosition{0.0F};
|
||||
glm::vec2 SurfaceUv{-1.0F};
|
||||
bool WorldSpaceUi = false;
|
||||
float ScrollDelta = 0.0F;
|
||||
};
|
||||
struct MetaCoreInteractionPickResult {
|
||||
MetaCoreId ObjectId = 0;
|
||||
|
||||
@ -4,7 +4,7 @@ material {
|
||||
blending : transparent,
|
||||
transparency : default,
|
||||
depthWrite : false,
|
||||
depthCulling : false,
|
||||
depthCulling : true,
|
||||
doubleSided : true,
|
||||
requires : [ uv0, color ],
|
||||
parameters : [
|
||||
|
||||
@ -6,8 +6,11 @@
|
||||
#include "stb/stb_image.h"
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
#include <RmlUi/Core/Elements/ElementFormControlSelect.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <GLFW/glfw3.h>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
|
||||
#include <filament/Camera.h>
|
||||
#include <filament/Engine.h>
|
||||
@ -15,6 +18,7 @@
|
||||
#include <filament/Material.h>
|
||||
#include <filament/MaterialInstance.h>
|
||||
#include <filament/RenderableManager.h>
|
||||
#include <filament/RenderTarget.h>
|
||||
#include <filament/Renderer.h>
|
||||
#include <filament/Scene.h>
|
||||
#include <filament/Texture.h>
|
||||
@ -28,6 +32,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
@ -38,6 +43,7 @@
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@ -85,6 +91,101 @@ namespace {
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<MetaCoreRuntimeUiValue> ParseUiValue(std::string_view text, MetaCoreUiInputValueType type) {
|
||||
MetaCoreRuntimeUiValue result;
|
||||
if (type == MetaCoreUiInputValueType::Text) {
|
||||
result.Type = MetaCoreRuntimeUiValueType::String;
|
||||
result.StringValue = std::string(text);
|
||||
return result;
|
||||
}
|
||||
const std::string owned(text);
|
||||
char* end = nullptr;
|
||||
if (type == MetaCoreUiInputValueType::Integer) {
|
||||
const long long value = std::strtoll(owned.c_str(), &end, 10);
|
||||
if (end == owned.c_str() || *end != '\0') return std::nullopt;
|
||||
result.Type = MetaCoreRuntimeUiValueType::Int64;
|
||||
result.Int64Value = static_cast<std::int64_t>(value);
|
||||
return result;
|
||||
}
|
||||
const double value = std::strtod(owned.c_str(), &end);
|
||||
if (end == owned.c_str() || *end != '\0' || !std::isfinite(value)) return std::nullopt;
|
||||
result.Type = MetaCoreRuntimeUiValueType::Double;
|
||||
result.DoubleValue = value;
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<MetaCoreRuntimeUiValue> ConvertUiValue(
|
||||
const MetaCoreRuntimeUiValue& source,
|
||||
std::string_view converter
|
||||
) {
|
||||
if (converter.empty()) return source;
|
||||
MetaCoreRuntimeUiValue result;
|
||||
if (converter == "not" && source.Type == MetaCoreRuntimeUiValueType::Bool) {
|
||||
result.Type = MetaCoreRuntimeUiValueType::Bool;
|
||||
result.BoolValue = !source.BoolValue;
|
||||
return result;
|
||||
}
|
||||
if (converter == "to_string") {
|
||||
result.Type = MetaCoreRuntimeUiValueType::String;
|
||||
result.StringValue = FormatUiValue(source, {});
|
||||
return result;
|
||||
}
|
||||
if ((converter == "uppercase" || converter == "lowercase") &&
|
||||
source.Type == MetaCoreRuntimeUiValueType::String) {
|
||||
result = source;
|
||||
std::transform(result.StringValue.begin(), result.StringValue.end(), result.StringValue.begin(),
|
||||
[&](unsigned char value) {
|
||||
return static_cast<char>(converter == "uppercase" ? std::toupper(value) : std::tolower(value));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
if (converter == "to_bool") {
|
||||
result.Type = MetaCoreRuntimeUiValueType::Bool;
|
||||
if (source.Type == MetaCoreRuntimeUiValueType::Bool) result.BoolValue = source.BoolValue;
|
||||
else if (source.Type == MetaCoreRuntimeUiValueType::Int64) result.BoolValue = source.Int64Value != 0;
|
||||
else if (source.Type == MetaCoreRuntimeUiValueType::Double) result.BoolValue = source.DoubleValue != 0.0;
|
||||
else if (source.Type == MetaCoreRuntimeUiValueType::String &&
|
||||
(source.StringValue == "true" || source.StringValue == "1")) result.BoolValue = true;
|
||||
else if (source.Type != MetaCoreRuntimeUiValueType::String ||
|
||||
(source.StringValue != "false" && source.StringValue != "0")) return std::nullopt;
|
||||
return result;
|
||||
}
|
||||
if (converter == "to_int" || converter == "to_double") {
|
||||
double numeric = 0.0;
|
||||
if (source.Type == MetaCoreRuntimeUiValueType::Int64) numeric = static_cast<double>(source.Int64Value);
|
||||
else if (source.Type == MetaCoreRuntimeUiValueType::Double) numeric = source.DoubleValue;
|
||||
else if (source.Type == MetaCoreRuntimeUiValueType::Bool) numeric = source.BoolValue ? 1.0 : 0.0;
|
||||
else if (source.Type == MetaCoreRuntimeUiValueType::String) {
|
||||
char* end = nullptr;
|
||||
numeric = std::strtod(source.StringValue.c_str(), &end);
|
||||
if (end == source.StringValue.c_str() || *end != '\0' || !std::isfinite(numeric)) return std::nullopt;
|
||||
} else return std::nullopt;
|
||||
if (converter == "to_int") {
|
||||
result.Type = MetaCoreRuntimeUiValueType::Int64;
|
||||
result.Int64Value = static_cast<std::int64_t>(numeric);
|
||||
} else {
|
||||
result.Type = MetaCoreRuntimeUiValueType::Double;
|
||||
result.DoubleValue = numeric;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool UiValuesEqual(const MetaCoreRuntimeUiValue& lhs, const MetaCoreRuntimeUiValue& rhs) {
|
||||
if (lhs.Type != rhs.Type) return false;
|
||||
switch (lhs.Type) {
|
||||
case MetaCoreRuntimeUiValueType::Bool: return lhs.BoolValue == rhs.BoolValue;
|
||||
case MetaCoreRuntimeUiValueType::Int64: return lhs.Int64Value == rhs.Int64Value;
|
||||
case MetaCoreRuntimeUiValueType::Double: return lhs.DoubleValue == rhs.DoubleValue;
|
||||
case MetaCoreRuntimeUiValueType::String: return lhs.StringValue == rhs.StringValue;
|
||||
case MetaCoreRuntimeUiValueType::Vec3: return lhs.Vec3Value == rhs.Vec3Value;
|
||||
case MetaCoreRuntimeUiValueType::AssetRef: return lhs.AssetValue == rhs.AssetValue;
|
||||
case MetaCoreRuntimeUiValueType::GameObjectRef: return lhs.ObjectValue == rhs.ObjectValue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] MetaCoreRuntimeUiValue ToUiValue(const MetaCoreRuntimeDataValue& value) {
|
||||
MetaCoreRuntimeUiValue result;
|
||||
result.Type = static_cast<MetaCoreRuntimeUiValueType>(value.Type);
|
||||
@ -222,7 +323,9 @@ public:
|
||||
|
||||
void RenderGeometry(Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation, Rml::TextureHandle texture) override {
|
||||
if (!Geometries_.contains(geometry)) return;
|
||||
DrawCommands_.push_back({geometry, translation, texture, ScissorEnabled_, Scissor_});
|
||||
DrawCommand command{geometry, translation, texture, ScissorEnabled_, Scissor_};
|
||||
if (ActiveWorldInstance_ != 0) WorldDrawCommands_[ActiveWorldInstance_].push_back(command);
|
||||
else DrawCommands_.push_back(command);
|
||||
}
|
||||
|
||||
void ReleaseGeometry(Rml::CompiledGeometryHandle geometry) override { Geometries_.erase(geometry); }
|
||||
@ -273,15 +376,25 @@ public:
|
||||
void EnableScissorRegion(bool enable) override { ScissorEnabled_ = enable; }
|
||||
void SetScissorRegion(Rml::Rectanglei region) override { Scissor_ = region; }
|
||||
|
||||
void BeginFrame() { DrawCommands_.clear(); }
|
||||
void BeginFrame() { DrawCommands_.clear(); WorldDrawCommands_.clear(); ActiveWorldInstance_ = 0; }
|
||||
void BeginWorld(std::uint64_t instanceId) { ActiveWorldInstance_ = instanceId; }
|
||||
void EndWorld() { ActiveWorldInstance_ = 0; }
|
||||
|
||||
void Render(filament::Engine& engine, filament::Renderer& renderer, uint32_t width, uint32_t height) {
|
||||
void Render(filament::Engine& engine, filament::Renderer& renderer, uint32_t width, uint32_t height,
|
||||
filament::RenderTarget* renderTarget = nullptr) {
|
||||
if (width == 0 || height == 0 || DrawCommands_.empty()) return;
|
||||
EnsureFilamentResources(engine);
|
||||
if (Scene_ == nullptr || View_ == nullptr || Material_ == nullptr || Camera_ == nullptr) return;
|
||||
|
||||
ClearFrameResources();
|
||||
View_->setViewport({0, 0, width, height});
|
||||
View_->setRenderTarget(renderTarget);
|
||||
if (renderTarget != nullptr) {
|
||||
filament::Renderer::ClearOptions clear;
|
||||
clear.clear = true;
|
||||
clear.clearColor = {0.0F, 0.0F, 0.0F, 0.0F};
|
||||
renderer.setClearOptions(clear);
|
||||
}
|
||||
Camera_->setProjection(filament::Camera::Projection::ORTHO, 0.0, static_cast<double>(width),
|
||||
static_cast<double>(height), 0.0, 0.0, 1.0);
|
||||
|
||||
@ -337,11 +450,56 @@ public:
|
||||
builder.build(engine, FrameRenderable_);
|
||||
Scene_->addEntity(FrameRenderable_);
|
||||
renderer.render(View_);
|
||||
View_->setRenderTarget(nullptr);
|
||||
}
|
||||
|
||||
struct WorldSurface {
|
||||
std::uint64_t InstanceId = 0;
|
||||
std::uint64_t SceneObjectId = 0;
|
||||
std::int32_t Width = 1;
|
||||
std::int32_t Height = 1;
|
||||
glm::vec3 WorldSize{1.0F};
|
||||
};
|
||||
|
||||
void RenderWorldSurfaces(
|
||||
filament::Engine& engine,
|
||||
filament::Renderer& renderer,
|
||||
filament::Scene& mainScene,
|
||||
const std::vector<WorldSurface>& surfaces,
|
||||
const std::function<bool(std::uint64_t, glm::mat4&)>& worldMatrixProvider
|
||||
) {
|
||||
EnsureFilamentResources(engine);
|
||||
std::unordered_set<std::uint64_t> active;
|
||||
for (const WorldSurface& surface : surfaces) {
|
||||
const auto commands = WorldDrawCommands_.find(surface.InstanceId);
|
||||
if (commands == WorldDrawCommands_.end() || commands->second.empty()) continue;
|
||||
active.insert(surface.InstanceId);
|
||||
WorldTarget& target = EnsureWorldTarget(engine, mainScene, surface);
|
||||
std::swap(DrawCommands_, commands->second);
|
||||
Render(engine, renderer, static_cast<std::uint32_t>(surface.Width),
|
||||
static_cast<std::uint32_t>(surface.Height), target.Target);
|
||||
std::swap(DrawCommands_, commands->second);
|
||||
glm::mat4 world(1.0F);
|
||||
if (worldMatrixProvider(surface.SceneObjectId, world) && target.SurfaceEntity) {
|
||||
auto& transforms = engine.getTransformManager();
|
||||
const auto instance = transforms.getInstance(target.SurfaceEntity);
|
||||
if (instance) transforms.setTransform(instance,
|
||||
*reinterpret_cast<const filament::math::mat4f*>(glm::value_ptr(world)));
|
||||
}
|
||||
}
|
||||
for (auto iterator = WorldTargets_.begin(); iterator != WorldTargets_.end();) {
|
||||
if (!active.contains(iterator->first)) {
|
||||
DestroyWorldTarget(iterator->second);
|
||||
iterator = WorldTargets_.erase(iterator);
|
||||
} else ++iterator;
|
||||
}
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
if (Engine_ == nullptr) return;
|
||||
ClearFrameResources();
|
||||
for (auto& [_, target] : WorldTargets_) DestroyWorldTarget(target);
|
||||
WorldTargets_.clear();
|
||||
for (auto& [_, texture] : Textures_) {
|
||||
if (texture.FilamentTexture != nullptr) Engine_->destroy(texture.FilamentTexture);
|
||||
}
|
||||
@ -384,6 +542,19 @@ private:
|
||||
filament::Texture* FilamentTexture = nullptr;
|
||||
};
|
||||
|
||||
struct WorldTarget {
|
||||
filament::Texture* Color = nullptr;
|
||||
filament::RenderTarget* Target = nullptr;
|
||||
filament::VertexBuffer* VertexBuffer = nullptr;
|
||||
filament::IndexBuffer* IndexBuffer = nullptr;
|
||||
filament::MaterialInstance* MaterialInstance = nullptr;
|
||||
filament::Scene* MainScene = nullptr;
|
||||
utils::Entity SurfaceEntity{};
|
||||
std::int32_t Width = 0;
|
||||
std::int32_t Height = 0;
|
||||
glm::vec3 WorldSize{0.0F};
|
||||
};
|
||||
|
||||
[[nodiscard]] Rml::TextureHandle StoreTexture(Texture texture) {
|
||||
const Rml::TextureHandle handle = NextTextureHandle_++;
|
||||
Textures_.emplace(handle, std::move(texture));
|
||||
@ -452,6 +623,84 @@ private:
|
||||
WhiteTexture_->setImage(*Engine_, 0, std::move(descriptor));
|
||||
}
|
||||
|
||||
WorldTarget& EnsureWorldTarget(filament::Engine& engine, filament::Scene& mainScene, const WorldSurface& surface) {
|
||||
auto& target = WorldTargets_[surface.InstanceId];
|
||||
if (target.Target != nullptr && (target.Width != surface.Width || target.Height != surface.Height ||
|
||||
target.WorldSize != surface.WorldSize || target.MainScene != &mainScene)) {
|
||||
DestroyWorldTarget(target);
|
||||
}
|
||||
if (target.Target != nullptr) return target;
|
||||
target.Width = surface.Width;
|
||||
target.Height = surface.Height;
|
||||
target.WorldSize = surface.WorldSize;
|
||||
target.MainScene = &mainScene;
|
||||
target.Color = filament::Texture::Builder()
|
||||
.width(static_cast<std::uint32_t>(surface.Width))
|
||||
.height(static_cast<std::uint32_t>(surface.Height))
|
||||
.levels(1)
|
||||
.format(filament::Texture::InternalFormat::RGBA8)
|
||||
.usage(filament::Texture::Usage::COLOR_ATTACHMENT | filament::Texture::Usage::SAMPLEABLE)
|
||||
.sampler(filament::Texture::Sampler::SAMPLER_2D)
|
||||
.build(engine);
|
||||
target.Target = filament::RenderTarget::Builder()
|
||||
.texture(filament::RenderTarget::AttachmentPoint::COLOR, target.Color)
|
||||
.build(engine);
|
||||
const float halfWidth = std::max(0.001F, surface.WorldSize.x) * 0.5F;
|
||||
const float halfHeight = std::max(0.001F, surface.WorldSize.y) * 0.5F;
|
||||
const std::array<GpuVertex, 4> vertices{{
|
||||
{{-halfWidth, -halfHeight}, {0.0F, 1.0F}, {255, 255, 255, 255}},
|
||||
{{ halfWidth, -halfHeight}, {1.0F, 1.0F}, {255, 255, 255, 255}},
|
||||
{{ halfWidth, halfHeight}, {1.0F, 0.0F}, {255, 255, 255, 255}},
|
||||
{{-halfWidth, halfHeight}, {0.0F, 0.0F}, {255, 255, 255, 255}}
|
||||
}};
|
||||
const std::array<std::uint32_t, 6> indices{{0, 1, 2, 0, 2, 3}};
|
||||
target.VertexBuffer = filament::VertexBuffer::Builder().vertexCount(4).bufferCount(1)
|
||||
.attribute(filament::VertexAttribute::POSITION, 0, filament::VertexBuffer::AttributeType::FLOAT2, 0, sizeof(GpuVertex))
|
||||
.attribute(filament::VertexAttribute::UV0, 0, filament::VertexBuffer::AttributeType::FLOAT2, sizeof(float) * 2, sizeof(GpuVertex))
|
||||
.attribute(filament::VertexAttribute::COLOR, 0, filament::VertexBuffer::AttributeType::UBYTE4, sizeof(float) * 4, sizeof(GpuVertex))
|
||||
.normalized(filament::VertexAttribute::COLOR).build(engine);
|
||||
target.IndexBuffer = filament::IndexBuffer::Builder().indexCount(6)
|
||||
.bufferType(filament::IndexBuffer::IndexType::UINT).build(engine);
|
||||
auto* vertexCopy = new std::array<GpuVertex, 4>(vertices);
|
||||
auto* indexCopy = new std::array<std::uint32_t, 6>(indices);
|
||||
target.VertexBuffer->setBufferAt(engine, 0, filament::VertexBuffer::BufferDescriptor(
|
||||
vertexCopy, sizeof(*vertexCopy), [](void* buffer, std::size_t, void*) {
|
||||
delete static_cast<std::array<GpuVertex, 4>*>(buffer);
|
||||
}, nullptr));
|
||||
target.IndexBuffer->setBuffer(engine, filament::IndexBuffer::BufferDescriptor(
|
||||
indexCopy, sizeof(*indexCopy), [](void* buffer, std::size_t, void*) {
|
||||
delete static_cast<std::array<std::uint32_t, 6>*>(buffer);
|
||||
}, nullptr));
|
||||
target.MaterialInstance = Material_->createInstance();
|
||||
target.MaterialInstance->setParameter("albedo", target.Color, filament::TextureSampler(
|
||||
filament::TextureSampler::MinFilter::LINEAR, filament::TextureSampler::MagFilter::LINEAR));
|
||||
target.SurfaceEntity = utils::EntityManager::get().create();
|
||||
filament::RenderableManager::Builder(1).boundingBox({{0.0F, 0.0F, 0.0F}, {halfWidth, halfHeight, 0.01F}})
|
||||
.culling(false).castShadows(false).receiveShadows(false)
|
||||
.geometry(0, filament::RenderableManager::PrimitiveType::TRIANGLES,
|
||||
target.VertexBuffer, target.IndexBuffer, 0, 6)
|
||||
.material(0, target.MaterialInstance).build(engine, target.SurfaceEntity);
|
||||
engine.getTransformManager().create(target.SurfaceEntity);
|
||||
mainScene.addEntity(target.SurfaceEntity);
|
||||
return target;
|
||||
}
|
||||
|
||||
void DestroyWorldTarget(WorldTarget& target) {
|
||||
if (Engine_ == nullptr) return;
|
||||
if (target.MainScene != nullptr && target.SurfaceEntity) target.MainScene->remove(target.SurfaceEntity);
|
||||
if (target.SurfaceEntity) {
|
||||
Engine_->getRenderableManager().destroy(target.SurfaceEntity);
|
||||
Engine_->getTransformManager().destroy(target.SurfaceEntity);
|
||||
utils::EntityManager::get().destroy(target.SurfaceEntity);
|
||||
}
|
||||
if (target.MaterialInstance != nullptr) Engine_->destroy(target.MaterialInstance);
|
||||
if (target.VertexBuffer != nullptr) Engine_->destroy(target.VertexBuffer);
|
||||
if (target.IndexBuffer != nullptr) Engine_->destroy(target.IndexBuffer);
|
||||
if (target.Target != nullptr) Engine_->destroy(target.Target);
|
||||
if (target.Color != nullptr) Engine_->destroy(target.Color);
|
||||
target = {};
|
||||
}
|
||||
|
||||
[[nodiscard]] bool CreateFrameGeometry(filament::Engine& engine, const Geometry& geometry, Rml::Vector2f translation) {
|
||||
if (geometry.Vertices.empty() || geometry.Indices.empty()) return false;
|
||||
std::vector<GpuVertex> translatedVertices = geometry.Vertices;
|
||||
@ -515,6 +764,9 @@ private:
|
||||
std::unordered_map<Rml::CompiledGeometryHandle, Geometry> Geometries_;
|
||||
std::unordered_map<Rml::TextureHandle, Texture> Textures_;
|
||||
std::vector<DrawCommand> DrawCommands_;
|
||||
std::unordered_map<std::uint64_t, std::vector<DrawCommand>> WorldDrawCommands_;
|
||||
std::uint64_t ActiveWorldInstance_ = 0;
|
||||
std::unordered_map<std::uint64_t, WorldTarget> WorldTargets_;
|
||||
Rml::CompiledGeometryHandle NextGeometryHandle_ = 1;
|
||||
Rml::TextureHandle NextTextureHandle_ = 1;
|
||||
bool ScissorEnabled_ = false;
|
||||
@ -566,11 +818,28 @@ private:
|
||||
|
||||
class MetaCoreRuntimeUiSystem::Impl {
|
||||
public:
|
||||
struct Provider {
|
||||
ProviderReadCallback Reader{};
|
||||
ProviderWriteCallback Writer{};
|
||||
};
|
||||
|
||||
struct DocumentState {
|
||||
MetaCoreRuntimeUiDocumentEntry Entry{};
|
||||
Rml::Context* Context = nullptr;
|
||||
std::string ContextName{};
|
||||
Rml::ElementDocument* Document = nullptr;
|
||||
std::string DefaultLocale{"en-US"};
|
||||
std::string FallbackLocale{"en-US"};
|
||||
std::unordered_map<std::string, std::unordered_map<std::string, std::string>> Localization{};
|
||||
MetaCoreUiDocument CanvasSettings{};
|
||||
};
|
||||
|
||||
static std::pair<std::string, std::string_view> SplitProviderPath(std::string_view path) {
|
||||
const std::size_t separator = path.find('.');
|
||||
if (separator == std::string_view::npos) return {std::string(path), {}};
|
||||
return {std::string(path.substr(0, separator)), path.substr(separator + 1)};
|
||||
}
|
||||
|
||||
class EventListener final : public Rml::EventListener {
|
||||
public:
|
||||
EventListener(Impl& owner, std::string documentId) : Owner_(owner), DocumentId_(std::move(documentId)) {}
|
||||
@ -593,22 +862,54 @@ public:
|
||||
uiEvent.Type = event.GetType();
|
||||
uiEvent.Value = target->GetAttribute<Rml::String>("value", "");
|
||||
uiEvent.EventType = EventTypeFromRml(uiEvent.Type);
|
||||
const std::string kind = target->GetAttribute<Rml::String>("data-metacore-kind", "");
|
||||
if (uiEvent.EventType == MetaCoreRuntimeUiEventType::ValueChanged && kind == "list") {
|
||||
uiEvent.EventType = MetaCoreRuntimeUiEventType::SelectionChanged;
|
||||
if (const auto* select = dynamic_cast<Rml::ElementFormControlSelect*>(target))
|
||||
uiEvent.Value = std::to_string(select->GetSelection());
|
||||
else uiEvent.Value = target->GetAttribute<Rml::String>("data-metacore-selected-index", "-1");
|
||||
target->SetAttribute("data-metacore-selected-index", uiEvent.Value);
|
||||
}
|
||||
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 &&
|
||||
const char* bindingTarget = uiEvent.EventType == MetaCoreRuntimeUiEventType::SelectionChanged
|
||||
? "selected-index" : "value";
|
||||
const std::string writePath = target->GetAttribute<Rml::String>(
|
||||
std::string("data-metacore-bind-") + bindingTarget, "");
|
||||
const std::string writeMode = target->GetAttribute<Rml::String>(
|
||||
std::string("data-metacore-bind-mode-") + bindingTarget, "");
|
||||
if ((uiEvent.EventType == MetaCoreRuntimeUiEventType::ValueChanged ||
|
||||
uiEvent.EventType == MetaCoreRuntimeUiEventType::SelectionChanged) &&
|
||||
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 (previous != Owner_.Values_.end()) {
|
||||
uiEvent.OldValue = FormatUiValue(previous->second, {});
|
||||
uiEvent.OldTypedValue = previous->second;
|
||||
}
|
||||
const MetaCoreUiInputValueType inputType =
|
||||
uiEvent.EventType == MetaCoreRuntimeUiEventType::SelectionChanged
|
||||
? MetaCoreUiInputValueType::Integer
|
||||
: static_cast<MetaCoreUiInputValueType>(
|
||||
target->GetAttribute<int>("data-metacore-input-type", 0));
|
||||
if (const auto value = ParseUiValue(uiEvent.Value, inputType); value.has_value()) {
|
||||
uiEvent.NewTypedValue = *value;
|
||||
const auto [providerName, relativePath] = Impl::SplitProviderPath(writePath);
|
||||
const auto provider = Owner_.Providers_.find(providerName);
|
||||
const bool providerKnown = provider != Owner_.Providers_.end();
|
||||
const bool providerWritable = providerKnown && static_cast<bool>(provider->second.Writer);
|
||||
const bool writeSucceeded = providerWritable && provider->second.Writer(relativePath, *value);
|
||||
if ((providerKnown && !writeSucceeded) ||
|
||||
(!providerKnown && !writePath.starts_with("script."))) {
|
||||
uiEvent.EventType = MetaCoreRuntimeUiEventType::WriteRequest;
|
||||
} else {
|
||||
Owner_.Values_[writePath] = *value;
|
||||
}
|
||||
} else {
|
||||
Owner_.Diagnostics_.push_back({
|
||||
DocumentId_, target->GetId(), writePath, "UI value failed basic type validation", true
|
||||
});
|
||||
}
|
||||
}
|
||||
if (uiEvent.Action.empty() &&
|
||||
uiEvent.EventType != MetaCoreRuntimeUiEventType::Focus &&
|
||||
@ -635,6 +936,8 @@ public:
|
||||
std::vector<std::unique_ptr<EventListener>> Listeners_;
|
||||
std::unordered_map<std::string, MetaCoreRuntimeUiValue> Values_;
|
||||
std::unordered_map<std::string, std::vector<MetaCoreRuntimeUiRow>> Lists_;
|
||||
std::unordered_map<std::string, Provider> Providers_;
|
||||
std::unordered_set<std::string> ProviderPaths_;
|
||||
std::vector<MetaCoreRuntimeUiEvent> Events_;
|
||||
std::vector<std::string> Errors_;
|
||||
std::vector<MetaCoreUiDiagnostic> Diagnostics_;
|
||||
@ -644,13 +947,133 @@ public:
|
||||
std::string Theme_{};
|
||||
bool Initialized_ = false;
|
||||
|
||||
void RefreshProviders() {
|
||||
std::vector<std::string> changed;
|
||||
for (const std::string& path : ProviderPaths_) {
|
||||
const auto [providerName, relativePath] = SplitProviderPath(path);
|
||||
const auto provider = Providers_.find(providerName);
|
||||
if (provider == Providers_.end() || !provider->second.Reader) continue;
|
||||
const auto value = provider->second.Reader(relativePath);
|
||||
if (!value.has_value()) {
|
||||
if (std::none_of(Diagnostics_.begin(), Diagnostics_.end(), [&](const MetaCoreUiDiagnostic& diagnostic) {
|
||||
return diagnostic.BindingPath == path && diagnostic.Message == "Binding provider source is unavailable";
|
||||
})) Diagnostics_.push_back({{}, {}, path, "Binding provider source is unavailable", true});
|
||||
continue;
|
||||
}
|
||||
const auto previous = Values_.find(path);
|
||||
if (previous == Values_.end() || !UiValuesEqual(previous->second, *value)) {
|
||||
Values_[path] = *value;
|
||||
changed.push_back(path);
|
||||
}
|
||||
}
|
||||
for (const std::string& path : changed) ApplyBindings(&path);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool HasInputEnabledDocument() const {
|
||||
return std::any_of(Documents_.begin(), Documents_.end(), [](const DocumentState& state) {
|
||||
return state.Entry.Visible && state.Entry.InputEnabled && state.Document != nullptr;
|
||||
});
|
||||
}
|
||||
|
||||
bool NavigateFocus(int direction, Rml::Context* restrictedContext = nullptr) {
|
||||
struct Candidate {
|
||||
std::int32_t Layer = 0;
|
||||
int Order = 0;
|
||||
std::size_t Sequence = 0;
|
||||
Rml::Element* Element = nullptr;
|
||||
};
|
||||
std::vector<Candidate> candidates;
|
||||
std::size_t sequence = 0;
|
||||
for (const DocumentState& state : Documents_) {
|
||||
if (restrictedContext != nullptr && state.Context != restrictedContext) continue;
|
||||
if (!state.Entry.Visible || !state.Entry.InputEnabled || state.Document == nullptr) continue;
|
||||
Rml::ElementList elements;
|
||||
state.Document->QuerySelectorAll(elements, "[tabindex]");
|
||||
for (Rml::Element* element : elements) {
|
||||
if (!element->IsVisible(true) || element->HasAttribute("disabled")) continue;
|
||||
candidates.push_back({
|
||||
state.Entry.Layer,
|
||||
element->GetAttribute<int>("tabindex", 100000),
|
||||
sequence++,
|
||||
element
|
||||
});
|
||||
}
|
||||
}
|
||||
if (candidates.empty()) {
|
||||
for (const DocumentState& state : Documents_)
|
||||
if (state.Context != nullptr && state.Context->GetFocusElement() != nullptr)
|
||||
state.Context->GetFocusElement()->Blur();
|
||||
return false;
|
||||
}
|
||||
std::stable_sort(candidates.begin(), candidates.end(), [](const Candidate& lhs, const Candidate& rhs) {
|
||||
if (lhs.Layer != rhs.Layer) return lhs.Layer > rhs.Layer;
|
||||
if (lhs.Order != rhs.Order) return lhs.Order < rhs.Order;
|
||||
return lhs.Sequence < rhs.Sequence;
|
||||
});
|
||||
Rml::Element* focused = nullptr;
|
||||
for (const DocumentState& state : Documents_)
|
||||
if (state.Context != nullptr && state.Context->GetFocusElement() != nullptr)
|
||||
focused = state.Context->GetFocusElement();
|
||||
auto current = std::find_if(candidates.begin(), candidates.end(), [&](const Candidate& value) {
|
||||
return value.Element == focused;
|
||||
});
|
||||
std::ptrdiff_t index = current == candidates.end() ? (direction > 0 ? -1 : 0) :
|
||||
std::distance(candidates.begin(), current);
|
||||
index = (index + direction + static_cast<std::ptrdiff_t>(candidates.size())) %
|
||||
static_cast<std::ptrdiff_t>(candidates.size());
|
||||
candidates[static_cast<std::size_t>(index)].Element->Focus();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ApplyLocalization(DocumentState& state) {
|
||||
if (state.Document == nullptr || state.Localization.empty()) return;
|
||||
const auto findText = [&](std::string_view key) -> std::optional<std::string> {
|
||||
for (const std::string* locale : {&Locale_, &state.FallbackLocale, &state.DefaultLocale}) {
|
||||
const auto localeValues = state.Localization.find(*locale);
|
||||
if (localeValues == state.Localization.end()) continue;
|
||||
const auto value = localeValues->second.find(std::string(key));
|
||||
if (value != localeValues->second.end()) return value->second;
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
Rml::ElementList elements;
|
||||
state.Document->QuerySelectorAll(elements, "[data-metacore-loc]");
|
||||
for (Rml::Element* element : elements) {
|
||||
const std::string key = element->GetAttribute<Rml::String>("data-metacore-loc", "");
|
||||
if (const auto value = findText(key); value.has_value()) element->SetInnerRML(EscapeRmlText(*value));
|
||||
else Diagnostics_.push_back({state.Entry.DocumentId, element->GetId(), key,
|
||||
"Localization key cannot be resolved for locale " + Locale_, true});
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyCanvasLayouts(int framebufferWidth, int framebufferHeight) {
|
||||
if (Window_ == nullptr) return;
|
||||
const auto [windowWidth, windowHeight] = Window_->GetWindowSize();
|
||||
const float dpiScaleX = static_cast<float>(framebufferWidth) / static_cast<float>(std::max(1, windowWidth));
|
||||
const float dpiScaleY = static_cast<float>(framebufferHeight) / static_cast<float>(std::max(1, windowHeight));
|
||||
for (DocumentState& state : Documents_) {
|
||||
if (state.Document == nullptr) continue;
|
||||
const float width = state.Entry.RenderMode == 1U && state.Entry.TargetWidth > 0
|
||||
? static_cast<float>(state.Entry.TargetWidth) : static_cast<float>(framebufferWidth);
|
||||
const float height = state.Entry.RenderMode == 1U && state.Entry.TargetHeight > 0
|
||||
? static_cast<float>(state.Entry.TargetHeight) : static_cast<float>(framebufferHeight);
|
||||
const MetaCoreUiCanvasLayout layout = MetaCoreComputeUiCanvasLayout(state.CanvasSettings, {
|
||||
width, height, 96.0F * std::max(dpiScaleX, dpiScaleY), 0.0F, 0.0F, 0.0F, 0.0F
|
||||
});
|
||||
Rml::Element* body = state.Document->QuerySelector("body");
|
||||
if (body == nullptr) continue;
|
||||
body->SetProperty("position", "absolute");
|
||||
body->SetProperty("left", std::to_string(layout.ViewportLeft) + "px");
|
||||
body->SetProperty("top", std::to_string(layout.ViewportTop) + "px");
|
||||
body->SetProperty("width", std::to_string(layout.LogicalWidth) + "px");
|
||||
body->SetProperty("height", std::to_string(layout.LogicalHeight) + "px");
|
||||
body->SetProperty("transform-origin", "0 0");
|
||||
body->SetProperty("transform", "scale(" + std::to_string(layout.Scale) + ")");
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyBindings(const std::string* onlyPath = nullptr) {
|
||||
bool focusInvalidated = false;
|
||||
for (DocumentState& state : Documents_) {
|
||||
if (state.Document == nullptr) continue;
|
||||
for (const std::string target : {"text", "value", "visible", "enabled", "selected-index"}) {
|
||||
@ -667,14 +1090,39 @@ public:
|
||||
else element->SetAttribute(target == "enabled" ? "disabled" : target, fallback);
|
||||
continue;
|
||||
}
|
||||
const std::string converter = element->GetAttribute<Rml::String>(
|
||||
"data-metacore-converter-" + target, "");
|
||||
const auto converted = ConvertUiValue(value->second, converter);
|
||||
const bool targetTypeValid = converted.has_value() &&
|
||||
((target != "visible" && target != "enabled") ||
|
||||
converted->Type == MetaCoreRuntimeUiValueType::Bool) &&
|
||||
(target != "selected-index" ||
|
||||
converted->Type == MetaCoreRuntimeUiValueType::Int64);
|
||||
if (!targetTypeValid) {
|
||||
const std::string message = "Binding conversion or target type is invalid";
|
||||
if (std::none_of(Diagnostics_.begin(), Diagnostics_.end(), [&](const MetaCoreUiDiagnostic& diagnostic) {
|
||||
return diagnostic.Document == state.Entry.DocumentId &&
|
||||
diagnostic.NodeId == element->GetId() && diagnostic.BindingPath == path &&
|
||||
diagnostic.Message == message;
|
||||
})) Diagnostics_.push_back({state.Entry.DocumentId, element->GetId(), path, message, true});
|
||||
continue;
|
||||
}
|
||||
const std::string format = element->GetAttribute<Rml::String>("data-metacore-format-" + target, "");
|
||||
const std::string text = FormatUiValue(value->second, format);
|
||||
const std::string text = FormatUiValue(*converted, 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");
|
||||
if (converted->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");
|
||||
if (converted->BoolValue) {
|
||||
element->RemoveAttribute("disabled");
|
||||
} else {
|
||||
element->SetAttribute("disabled", "true");
|
||||
if (state.Context != nullptr && state.Context->GetFocusElement() == element) {
|
||||
element->Blur();
|
||||
focusInvalidated = true;
|
||||
}
|
||||
}
|
||||
} else element->SetAttribute("data-metacore-selected-index", text);
|
||||
}
|
||||
}
|
||||
@ -689,6 +1137,7 @@ public:
|
||||
element->GetAttribute<Rml::String>("data-metacore-format", ""))));
|
||||
}
|
||||
}
|
||||
if (focusInvalidated) (void)NavigateFocus(1);
|
||||
}
|
||||
|
||||
bool LoadEntry(const MetaCoreRuntimeUiDocumentEntry& entry) {
|
||||
@ -701,16 +1150,98 @@ public:
|
||||
Errors_.push_back("Invalid UI manifest entry: " + entry.DocumentId);
|
||||
return false;
|
||||
}
|
||||
Rml::ElementDocument* document = Context_->LoadDocument(absolutePath.string());
|
||||
DocumentState state;
|
||||
state.Entry = entry;
|
||||
state.Context = Context_;
|
||||
if (entry.RenderMode == 1U) {
|
||||
state.ContextName = "MetaCoreWorldUi-" + std::to_string(entry.InstanceId);
|
||||
state.Context = Rml::CreateContext(state.ContextName, {
|
||||
std::max(1, entry.TargetWidth), std::max(1, entry.TargetHeight)
|
||||
});
|
||||
if (state.Context == nullptr) {
|
||||
Errors_.push_back("Failed to create World Space RmlUi context: " + entry.DocumentId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const std::filesystem::path documentDirectory = absolutePath.parent_path();
|
||||
const std::filesystem::path localizationPath = documentDirectory / "localization.json";
|
||||
if (std::filesystem::is_regular_file(localizationPath)) {
|
||||
try {
|
||||
std::ifstream input(localizationPath);
|
||||
nlohmann::json root;
|
||||
input >> root;
|
||||
state.DefaultLocale = root.value("default", "en-US");
|
||||
state.FallbackLocale = root.value("fallback", state.DefaultLocale);
|
||||
if (root.contains("locales") && root["locales"].is_object()) {
|
||||
for (auto locale = root["locales"].begin(); locale != root["locales"].end(); ++locale) {
|
||||
if (!locale.value().is_object()) continue;
|
||||
auto& values = state.Localization[locale.key()];
|
||||
for (auto value = locale.value().begin(); value != locale.value().end(); ++value)
|
||||
if (value.value().is_string()) values[value.key()] = value.value().get<std::string>();
|
||||
}
|
||||
}
|
||||
} catch (...) {
|
||||
Diagnostics_.push_back({entry.DocumentId, {}, {}, "Compiled localization data is invalid", true});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const std::filesystem::path fontDirectory = documentDirectory / "Assets";
|
||||
std::error_code fontError;
|
||||
if (std::filesystem::is_directory(fontDirectory, fontError)) {
|
||||
std::vector<std::filesystem::path> fonts;
|
||||
for (std::filesystem::directory_iterator iterator(fontDirectory, fontError), end;
|
||||
!fontError && iterator != end; iterator.increment(fontError)) {
|
||||
if (iterator->is_regular_file() && iterator->path().extension() == ".font") fonts.push_back(iterator->path());
|
||||
}
|
||||
std::sort(fonts.begin(), fonts.end());
|
||||
bool fallback = false;
|
||||
for (const auto& font : fonts) {
|
||||
if (!Rml::LoadFontFace(font.string(), fallback)) {
|
||||
Diagnostics_.push_back({entry.DocumentId, {}, {}, "Failed to load font: " + font.filename().string(), true});
|
||||
}
|
||||
fallback = true;
|
||||
}
|
||||
}
|
||||
Rml::ElementDocument* document = state.Context->LoadDocument(absolutePath.string());
|
||||
if (document == nullptr) {
|
||||
Errors_.push_back("Failed to load RML: " + relativePath.generic_string());
|
||||
if (!state.ContextName.empty()) (void)Rml::RemoveContext(state.ContextName);
|
||||
return false;
|
||||
}
|
||||
state.Document = document;
|
||||
for (const char* target : {"text", "value", "visible", "enabled", "items", "selected-index"}) {
|
||||
Rml::ElementList boundElements;
|
||||
document->QuerySelectorAll(boundElements, std::string("[data-metacore-bind-") + target + "]");
|
||||
for (Rml::Element* element : boundElements) {
|
||||
const std::string path = element->GetAttribute<Rml::String>(
|
||||
std::string("data-metacore-bind-") + target, "");
|
||||
if (!path.empty()) ProviderPaths_.insert(path);
|
||||
}
|
||||
}
|
||||
if (Rml::Element* body = document->QuerySelector("body"); body != nullptr) {
|
||||
const std::string reference = body->GetAttribute<Rml::String>("data-metacore-reference", "1920x1080");
|
||||
const std::size_t separator = reference.find('x');
|
||||
if (separator != std::string::npos) {
|
||||
state.CanvasSettings.ReferenceWidth = std::max(1, std::atoi(reference.substr(0, separator).c_str()));
|
||||
state.CanvasSettings.ReferenceHeight = std::max(1, std::atoi(reference.substr(separator + 1).c_str()));
|
||||
}
|
||||
state.CanvasSettings.ScaleMode = static_cast<MetaCoreUiCanvasScaleMode>(
|
||||
body->GetAttribute<int>("data-metacore-scale-mode", 1));
|
||||
state.CanvasSettings.MatchWidthOrHeight = body->GetAttribute<float>("data-metacore-match", 0.5F);
|
||||
state.CanvasSettings.ReferenceDpi = body->GetAttribute<float>("data-metacore-reference-dpi", 96.0F);
|
||||
state.CanvasSettings.MinScale = body->GetAttribute<float>("data-metacore-min-scale", 0.5F);
|
||||
state.CanvasSettings.MaxScale = body->GetAttribute<float>("data-metacore-max-scale", 4.0F);
|
||||
state.CanvasSettings.MinAspectRatio = body->GetAttribute<float>("data-metacore-min-aspect", 0.0F);
|
||||
state.CanvasSettings.MaxAspectRatio = body->GetAttribute<float>("data-metacore-max-aspect", 0.0F);
|
||||
state.CanvasSettings.UseSafeArea =
|
||||
body->GetAttribute<Rml::String>("data-metacore-safe-area", "true") == "true";
|
||||
}
|
||||
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});
|
||||
Documents_.push_back(std::move(state));
|
||||
ApplyLocalization(Documents_.back());
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@ -761,12 +1292,32 @@ void MetaCoreRuntimeUiSystem::AttachToViewportRenderer(MetaCoreEditorViewportRen
|
||||
viewportRenderer.SetRuntimeOverlayRenderCallback([this](filament::Engine& engine, filament::Renderer& renderer, uint32_t width, uint32_t height) {
|
||||
if (Impl_ && Impl_->Initialized_) Impl_->RenderInterface_.Render(engine, renderer, width, height);
|
||||
});
|
||||
viewportRenderer.SetRuntimeWorldUiRenderCallback([this, &viewportRenderer](
|
||||
filament::Engine& engine, filament::Renderer& renderer, filament::Scene& scene) {
|
||||
if (!Impl_ || !Impl_->Initialized_) return;
|
||||
std::vector<MetaCoreRmlFilamentRenderInterface::WorldSurface> surfaces;
|
||||
for (const auto& state : Impl_->Documents_) {
|
||||
if (state.Entry.RenderMode != 1U || !state.Entry.Visible || state.Document == nullptr) continue;
|
||||
surfaces.push_back({
|
||||
state.Entry.InstanceId,
|
||||
state.Entry.SceneObjectId,
|
||||
std::max(1, state.Entry.TargetWidth),
|
||||
std::max(1, state.Entry.TargetHeight),
|
||||
state.Entry.WorldSize
|
||||
});
|
||||
}
|
||||
Impl_->RenderInterface_.RenderWorldSurfaces(engine, renderer, scene, surfaces,
|
||||
[&viewportRenderer](std::uint64_t objectId, glm::mat4& matrix) {
|
||||
return viewportRenderer.TryGetObjectWorldMatrix(objectId, matrix);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void MetaCoreRuntimeUiSystem::Shutdown() {
|
||||
if (!Impl_) return;
|
||||
if (Impl_->ViewportRenderer_ != nullptr) {
|
||||
Impl_->ViewportRenderer_->SetRuntimeOverlayRenderCallback({});
|
||||
Impl_->ViewportRenderer_->SetRuntimeWorldUiRenderCallback({});
|
||||
Impl_->ViewportRenderer_ = nullptr;
|
||||
}
|
||||
if (Impl_->Initialized_) Rml::Shutdown();
|
||||
@ -778,16 +1329,32 @@ void MetaCoreRuntimeUiSystem::Shutdown() {
|
||||
Impl_->SystemInterface_.reset();
|
||||
Impl_->Values_.clear();
|
||||
Impl_->Lists_.clear();
|
||||
Impl_->Providers_.clear();
|
||||
Impl_->ProviderPaths_.clear();
|
||||
Impl_->Diagnostics_.clear();
|
||||
Impl_->Initialized_ = false;
|
||||
}
|
||||
|
||||
void MetaCoreRuntimeUiSystem::Update(float) {
|
||||
if (!Impl_->Initialized_ || Impl_->Context_ == nullptr || Impl_->Window_ == nullptr) return;
|
||||
Impl_->RefreshProviders();
|
||||
const auto [width, height] = Impl_->Window_->GetFramebufferSize();
|
||||
Impl_->Context_->SetDimensions({width, height});
|
||||
Impl_->ApplyCanvasLayouts(width, height);
|
||||
Impl_->RenderInterface_.BeginFrame();
|
||||
Impl_->Context_->Update();
|
||||
for (auto& worldState : Impl_->Documents_) {
|
||||
if (worldState.Entry.RenderMode != 1U || !worldState.Entry.Visible ||
|
||||
worldState.Document == nullptr || worldState.Context == nullptr) continue;
|
||||
worldState.Context->SetDimensions({
|
||||
std::max(1, worldState.Entry.TargetWidth),
|
||||
std::max(1, worldState.Entry.TargetHeight)
|
||||
});
|
||||
worldState.Context->Update();
|
||||
Impl_->RenderInterface_.BeginWorld(worldState.Entry.InstanceId);
|
||||
worldState.Context->Render();
|
||||
Impl_->RenderInterface_.EndWorld();
|
||||
}
|
||||
Impl_->Context_->Render();
|
||||
}
|
||||
|
||||
@ -795,30 +1362,99 @@ MetaCoreInputConsumption MetaCoreRuntimeUiSystem::ProcessInput() {
|
||||
MetaCoreInputConsumption consumed;
|
||||
if (!Impl_->Initialized_ || Impl_->Context_ == nullptr || Impl_->Window_ == nullptr || !Impl_->HasInputEnabledDocument()) return consumed;
|
||||
MetaCoreInput& input = Impl_->Window_->GetInput();
|
||||
Rml::Context* keyboardContext = Impl_->Context_;
|
||||
for (const auto& state : Impl_->Documents_)
|
||||
if (state.Entry.RenderMode == 1U && state.Context != nullptr &&
|
||||
state.Context->GetFocusElement() != nullptr) keyboardContext = state.Context;
|
||||
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);
|
||||
const bool hasScreenUi = std::any_of(Impl_->Documents_.begin(), Impl_->Documents_.end(), [](const auto& state) {
|
||||
return state.Entry.RenderMode == 0U && state.Entry.Visible && state.Entry.InputEnabled && state.Document != nullptr;
|
||||
});
|
||||
if (hasScreenUi) 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);
|
||||
if (input.WasMouseButtonPressed(mouseButton)) consumed.Mouse = !Impl_->Context_->ProcessMouseButtonDown(button, 0) || consumed.Mouse;
|
||||
if (input.WasMouseButtonReleased(mouseButton)) consumed.Mouse = !Impl_->Context_->ProcessMouseButtonUp(button, 0) || consumed.Mouse;
|
||||
if (hasScreenUi && input.WasMouseButtonPressed(mouseButton)) consumed.Mouse = !Impl_->Context_->ProcessMouseButtonDown(button, 0) || consumed.Mouse;
|
||||
if (hasScreenUi && input.WasMouseButtonReleased(mouseButton)) consumed.Mouse = !Impl_->Context_->ProcessMouseButtonUp(button, 0) || consumed.Mouse;
|
||||
}
|
||||
if (std::abs(input.GetMouseWheelDelta()) > 0.001F) consumed.Mouse = !Impl_->Context_->ProcessMouseWheel(input.GetMouseWheelDelta(), 0) || consumed.Mouse;
|
||||
if (hasScreenUi && 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;
|
||||
if (event.Pressed && event.Key == GLFW_KEY_TAB) {
|
||||
consumed.Keyboard = Impl_->NavigateFocus((event.Modifiers & GLFW_MOD_SHIFT) != 0 ? -1 : 1, keyboardContext) || consumed.Keyboard;
|
||||
continue;
|
||||
}
|
||||
if (event.Pressed && event.Key == GLFW_KEY_ESCAPE && keyboardContext->GetFocusElement() != nullptr) {
|
||||
keyboardContext->GetFocusElement()->Blur();
|
||||
consumed.Keyboard = true;
|
||||
continue;
|
||||
}
|
||||
if (event.Pressed && (event.Key == GLFW_KEY_LEFT || event.Key == GLFW_KEY_UP ||
|
||||
event.Key == GLFW_KEY_RIGHT || event.Key == GLFW_KEY_DOWN)) {
|
||||
Rml::Element* focus = keyboardContext->GetFocusElement();
|
||||
const std::string kind = focus != nullptr
|
||||
? focus->GetAttribute<Rml::String>("data-metacore-kind", "") : "";
|
||||
if (kind != "input") {
|
||||
const int direction = event.Key == GLFW_KEY_LEFT || event.Key == GLFW_KEY_UP ? -1 : 1;
|
||||
consumed.Keyboard = Impl_->NavigateFocus(direction, keyboardContext) || consumed.Keyboard;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
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);
|
||||
const bool propagated = event.Pressed ? keyboardContext->ProcessKeyDown(key, modifiers) : keyboardContext->ProcessKeyUp(key, modifiers);
|
||||
consumed.Keyboard = !propagated || 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;
|
||||
consumed.Keyboard = !keyboardContext->ProcessTextInput(static_cast<Rml::Character>(codePoint)) || consumed.Keyboard;
|
||||
}
|
||||
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Programmatic;
|
||||
return consumed;
|
||||
}
|
||||
|
||||
bool MetaCoreRuntimeUiSystem::ProcessWorldPointer(
|
||||
std::uint64_t sceneObjectId,
|
||||
glm::vec2 surfaceUv,
|
||||
MetaCoreRuntimeUiWorldPointerType type,
|
||||
std::uint32_t button,
|
||||
float scrollDelta
|
||||
) {
|
||||
if (!Impl_->Initialized_ || Impl_->Context_ == nullptr || Impl_->Window_ == nullptr ||
|
||||
sceneObjectId == 0 || surfaceUv.x < 0.0F || surfaceUv.y < 0.0F ||
|
||||
surfaceUv.x > 1.0F || surfaceUv.y > 1.0F) return false;
|
||||
const auto state = std::max_element(Impl_->Documents_.begin(), Impl_->Documents_.end(),
|
||||
[&](const auto& lhs, const auto& rhs) {
|
||||
const bool lhsMatch = lhs.Entry.RenderMode == 1U && lhs.Entry.SceneObjectId == sceneObjectId &&
|
||||
lhs.Entry.Visible && lhs.Entry.InputEnabled && lhs.Document != nullptr;
|
||||
const bool rhsMatch = rhs.Entry.RenderMode == 1U && rhs.Entry.SceneObjectId == sceneObjectId &&
|
||||
rhs.Entry.Visible && rhs.Entry.InputEnabled && rhs.Document != nullptr;
|
||||
if (lhsMatch != rhsMatch) return !lhsMatch;
|
||||
return lhs.Entry.Layer < rhs.Entry.Layer;
|
||||
});
|
||||
if (state == Impl_->Documents_.end() || state->Entry.RenderMode != 1U ||
|
||||
state->Entry.SceneObjectId != sceneObjectId || !state->Entry.Visible ||
|
||||
!state->Entry.InputEnabled || state->Document == nullptr) return false;
|
||||
const int width = std::max(1, state->Entry.TargetWidth);
|
||||
const int height = std::max(1, state->Entry.TargetHeight);
|
||||
const int x = std::clamp(static_cast<int>(surfaceUv.x * static_cast<float>(width)), 0, width - 1);
|
||||
const int y = std::clamp(static_cast<int>(surfaceUv.y * static_cast<float>(height)), 0, height - 1);
|
||||
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::WorldPointer;
|
||||
if (state->Context == nullptr) return false;
|
||||
state->Context->SetDimensions({width, height});
|
||||
bool propagated = state->Context->ProcessMouseMove(x, y, 0);
|
||||
if (type == MetaCoreRuntimeUiWorldPointerType::Down)
|
||||
propagated = state->Context->ProcessMouseButtonDown(static_cast<int>(button), 0) && propagated;
|
||||
else if (type == MetaCoreRuntimeUiWorldPointerType::Up)
|
||||
propagated = state->Context->ProcessMouseButtonUp(static_cast<int>(button), 0) && propagated;
|
||||
else if (type == MetaCoreRuntimeUiWorldPointerType::Scroll)
|
||||
propagated = state->Context->ProcessMouseWheel(scrollDelta, 0) && propagated;
|
||||
else if (type == MetaCoreRuntimeUiWorldPointerType::Cancel)
|
||||
propagated = state->Context->ProcessMouseLeave() && propagated;
|
||||
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Programmatic;
|
||||
return !propagated;
|
||||
}
|
||||
|
||||
void MetaCoreRuntimeUiSystem::ApplyDataUpdates(const std::vector<MetaCoreRuntimeDataUpdate>& updates) {
|
||||
if (!Impl_->Initialized_) return;
|
||||
for (const MetaCoreRuntimeDataUpdate& update : updates) {
|
||||
@ -850,25 +1486,56 @@ bool MetaCoreRuntimeUiSystem::SetListRows(const std::string& sourcePath, std::ve
|
||||
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;
|
||||
auto* select = dynamic_cast<Rml::ElementFormControlSelect*>(element);
|
||||
if (select == nullptr) continue;
|
||||
select->RemoveAll();
|
||||
std::size_t index = 0;
|
||||
for (const auto& row : Impl_->Lists_[sourcePath]) {
|
||||
markup << "<div class=\"mcui-list-row\" data-index=\"" << index++ << "\">";
|
||||
const auto value = row.find("value");
|
||||
const auto label = row.contains("label") ? row.find("label") :
|
||||
(row.contains("text") ? row.find("text") : row.end());
|
||||
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>";
|
||||
std::ostringstream markup;
|
||||
if (label != row.end()) {
|
||||
markup << EscapeRmlText(FormatUiValue(label->second, {}));
|
||||
} else {
|
||||
bool first = true;
|
||||
for (const std::string& key : keys) {
|
||||
if (!first) markup << " | ";
|
||||
first = false;
|
||||
markup << EscapeRmlText(FormatUiValue(row.at(key), {}));
|
||||
}
|
||||
}
|
||||
(void)select->Add(markup.str(), value == row.end() ? std::to_string(index) :
|
||||
FormatUiValue(value->second, {}));
|
||||
++index;
|
||||
}
|
||||
element->SetInnerRML(markup.str());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MetaCoreRuntimeUiSystem::RegisterProvider(
|
||||
std::string providerName,
|
||||
ProviderReadCallback reader,
|
||||
ProviderWriteCallback writer
|
||||
) {
|
||||
if (providerName.empty() || providerName.find('.') != std::string::npos || !reader) return false;
|
||||
Impl_->Providers_.insert_or_assign(std::move(providerName), Impl::Provider{std::move(reader), std::move(writer)});
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MetaCoreRuntimeUiSystem::UnregisterProvider(std::string_view providerName) {
|
||||
return Impl_->Providers_.erase(std::string(providerName)) > 0;
|
||||
}
|
||||
|
||||
void MetaCoreRuntimeUiSystem::RefreshProviders() {
|
||||
if (Impl_->Initialized_) Impl_->RefreshProviders();
|
||||
}
|
||||
|
||||
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;
|
||||
@ -893,16 +1560,28 @@ bool MetaCoreRuntimeUiSystem::DestroyInstance(std::uint64_t instanceId) {
|
||||
return value.Entry.InstanceId == instanceId;
|
||||
});
|
||||
if (found == Impl_->Documents_.end()) return false;
|
||||
const bool ownedFocus = found->Context != nullptr && found->Context->GetFocusElement() != nullptr &&
|
||||
found->Context->GetFocusElement()->GetOwnerDocument() == found->Document;
|
||||
if (ownedFocus) found->Context->GetFocusElement()->Blur();
|
||||
if (found->Document != nullptr) found->Document->Close();
|
||||
if (!found->ContextName.empty()) (void)Rml::RemoveContext(found->ContextName);
|
||||
Impl_->Documents_.erase(found);
|
||||
if (ownedFocus) (void)Impl_->NavigateFocus(1);
|
||||
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;
|
||||
if (visible) state.Document->Show(); else state.Document->Hide();
|
||||
const bool ownedFocus = state.Context != nullptr && state.Context->GetFocusElement() != nullptr &&
|
||||
state.Context->GetFocusElement()->GetOwnerDocument() == state.Document;
|
||||
if (visible) state.Document->Show();
|
||||
else {
|
||||
if (ownedFocus) state.Context->GetFocusElement()->Blur();
|
||||
state.Document->Hide();
|
||||
}
|
||||
state.Entry.Visible = visible;
|
||||
if (!visible && ownedFocus) (void)Impl_->NavigateFocus(1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -911,7 +1590,11 @@ bool MetaCoreRuntimeUiSystem::SetDocumentVisible(const std::string& documentId,
|
||||
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_);
|
||||
for (auto& state : Impl_->Documents_) {
|
||||
if (state.Document == nullptr) continue;
|
||||
state.Document->SetAttribute("data-metacore-locale", Impl_->Locale_);
|
||||
Impl_->ApplyLocalization(state);
|
||||
}
|
||||
}
|
||||
|
||||
void MetaCoreRuntimeUiSystem::SetTheme(std::string theme) {
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
#include "MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <glm/gtc/matrix_inverse.hpp>
|
||||
|
||||
namespace MetaCore {
|
||||
namespace {
|
||||
@ -27,6 +29,31 @@ std::string Escape(std::string_view value) {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string EscapeJson(std::string_view value) {
|
||||
std::string result;
|
||||
result.reserve(value.size());
|
||||
for (const unsigned char c : value) {
|
||||
switch (c) {
|
||||
case '\\': result += "\\\\"; break;
|
||||
case '"': result += "\\\""; break;
|
||||
case '\n': result += "\\n"; break;
|
||||
case '\r': result += "\\r"; break;
|
||||
case '\t': result += "\\t"; break;
|
||||
default:
|
||||
if (c < 0x20U) {
|
||||
constexpr char digits[] = "0123456789abcdef";
|
||||
result += "\\u00";
|
||||
result += digits[c >> 4U];
|
||||
result += digits[c & 0x0fU];
|
||||
} else {
|
||||
result += static_cast<char>(c);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const char* NodeClass(MetaCoreUiNodeType type) {
|
||||
switch (type) {
|
||||
case MetaCoreUiNodeType::Panel: return "panel";
|
||||
@ -65,8 +92,265 @@ bool IsBindingPathValid(std::string_view path) {
|
||||
return path.starts_with("runtime.") || path.starts_with("script.") || path.starts_with("scene.");
|
||||
}
|
||||
|
||||
const char* EasingName(MetaCoreUiEasing easing) {
|
||||
switch (easing) {
|
||||
case MetaCoreUiEasing::Linear: return "linear";
|
||||
case MetaCoreUiEasing::EaseIn: return "ease-in";
|
||||
case MetaCoreUiEasing::EaseOut: return "ease-out";
|
||||
case MetaCoreUiEasing::EaseInOut: return "ease-in-out";
|
||||
}
|
||||
return "linear";
|
||||
}
|
||||
|
||||
const char* ThemeStateSelector(MetaCoreUiVisualState state) {
|
||||
switch (state) {
|
||||
case MetaCoreUiVisualState::Normal: return "";
|
||||
case MetaCoreUiVisualState::Hover: return ":hover";
|
||||
case MetaCoreUiVisualState::Pressed: return ":active";
|
||||
case MetaCoreUiVisualState::Focused: return ":focus";
|
||||
case MetaCoreUiVisualState::Disabled: return ":disabled";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string AnimationValue(MetaCoreUiAnimationProperty property, const glm::vec3& value) {
|
||||
std::ostringstream out;
|
||||
switch (property) {
|
||||
case MetaCoreUiAnimationProperty::Opacity:
|
||||
out << std::clamp(value.x, 0.0F, 1.0F);
|
||||
break;
|
||||
case MetaCoreUiAnimationProperty::Translation:
|
||||
out << "translate(" << value.x << "px," << value.y << "px)";
|
||||
break;
|
||||
case MetaCoreUiAnimationProperty::Scale:
|
||||
out << "scale(" << value.x << ',' << value.y << ')';
|
||||
break;
|
||||
case MetaCoreUiAnimationProperty::Color:
|
||||
return Color(value);
|
||||
}
|
||||
return out.str();
|
||||
}
|
||||
|
||||
const char* AnimationPropertyName(MetaCoreUiAnimationProperty property) {
|
||||
switch (property) {
|
||||
case MetaCoreUiAnimationProperty::Opacity: return "opacity";
|
||||
case MetaCoreUiAnimationProperty::Translation:
|
||||
case MetaCoreUiAnimationProperty::Scale: return "transform";
|
||||
case MetaCoreUiAnimationProperty::Color: return "color";
|
||||
}
|
||||
return "opacity";
|
||||
}
|
||||
|
||||
std::string Trim(std::string_view value) {
|
||||
std::size_t first = 0;
|
||||
while (first < value.size() && std::isspace(static_cast<unsigned char>(value[first]))) ++first;
|
||||
std::size_t last = value.size();
|
||||
while (last > first && std::isspace(static_cast<unsigned char>(value[last - 1]))) --last;
|
||||
return std::string(value.substr(first, last - first));
|
||||
}
|
||||
|
||||
std::string Lower(std::string_view value) {
|
||||
std::string result(value);
|
||||
std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string DecodeLegacyText(std::string_view value) {
|
||||
std::string result(value);
|
||||
const std::pair<std::string_view, std::string_view> entities[] = {
|
||||
{"<", "<"}, {">", ">"}, {""", "\""}, {"'", "'"}, {"&", "&"}
|
||||
};
|
||||
for (const auto& [encoded, decoded] : entities) {
|
||||
std::size_t position = 0;
|
||||
while ((position = result.find(encoded, position)) != std::string::npos) {
|
||||
result.replace(position, encoded.size(), decoded);
|
||||
position += decoded.size();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string LegacyAttribute(std::string_view tag, std::string_view requestedName) {
|
||||
const std::string name = Lower(requestedName);
|
||||
std::size_t position = 0;
|
||||
while (position < tag.size()) {
|
||||
while (position < tag.size() &&
|
||||
(std::isspace(static_cast<unsigned char>(tag[position])) || tag[position] == '<')) ++position;
|
||||
const std::size_t keyStart = position;
|
||||
while (position < tag.size() &&
|
||||
(std::isalnum(static_cast<unsigned char>(tag[position])) || tag[position] == '-' ||
|
||||
tag[position] == '_' || tag[position] == ':')) ++position;
|
||||
if (position == keyStart) {
|
||||
++position;
|
||||
continue;
|
||||
}
|
||||
const std::string key = Lower(tag.substr(keyStart, position - keyStart));
|
||||
while (position < tag.size() && std::isspace(static_cast<unsigned char>(tag[position]))) ++position;
|
||||
if (position >= tag.size() || tag[position] != '=') continue;
|
||||
++position;
|
||||
while (position < tag.size() && std::isspace(static_cast<unsigned char>(tag[position]))) ++position;
|
||||
if (position >= tag.size()) break;
|
||||
const char quote = tag[position];
|
||||
if (quote != '"' && quote != '\'') continue;
|
||||
const std::size_t valueStart = ++position;
|
||||
const std::size_t valueEnd = tag.find(quote, valueStart);
|
||||
if (valueEnd == std::string_view::npos) break;
|
||||
if (key == name) return DecodeLegacyText(tag.substr(valueStart, valueEnd - valueStart));
|
||||
position = valueEnd + 1;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::optional<MetaCoreUiNodeType> LegacyNodeType(std::string_view tag, std::string_view fullTag) {
|
||||
const std::string explicitType = Lower(LegacyAttribute(fullTag, "data-metacore-type"));
|
||||
const std::string name = explicitType.empty() ? Lower(tag) : explicitType;
|
||||
if (name == "div" || name == "section" || name == "panel") return MetaCoreUiNodeType::Panel;
|
||||
if (name == "p" || name == "span" || name == "label" || name == "text") return MetaCoreUiNodeType::Text;
|
||||
if (name == "img" || name == "image") return MetaCoreUiNodeType::Image;
|
||||
if (name == "button") return MetaCoreUiNodeType::Button;
|
||||
if (name == "input" || name == "textarea") return MetaCoreUiNodeType::Input;
|
||||
if (name == "select" || name == "ul" || name == "ol" || name == "list") return MetaCoreUiNodeType::List;
|
||||
if (name == "scroll") return MetaCoreUiNodeType::Scroll;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MetaCoreLegacyUiImportResult MetaCoreImportLegacyUiDocument(
|
||||
std::string_view rml,
|
||||
std::string_view rcss,
|
||||
std::string_view documentName
|
||||
) {
|
||||
MetaCoreLegacyUiImportResult result;
|
||||
result.Document.Name = Trim(documentName).empty() ? "ImportedUi" : Trim(documentName);
|
||||
result.Document.SchemaVersion = METACORE_UI_SCHEMA_VERSION;
|
||||
result.Document.GeneratorVersion = METACORE_UI_GENERATOR_VERSION;
|
||||
|
||||
MetaCoreUiNodeDocument root;
|
||||
root.Id = "LegacyRoot";
|
||||
root.Name = "LegacyRoot";
|
||||
root.Type = MetaCoreUiNodeType::Panel;
|
||||
root.RectTransform.AnchorMax = {1.0F, 1.0F, 0.0F};
|
||||
root.RectTransform.Size = {0.0F, 0.0F, 0.0F};
|
||||
result.Document.Nodes.push_back(root);
|
||||
result.Document.RootNodeIds.push_back(root.Id);
|
||||
|
||||
struct OpenNode {
|
||||
std::string Tag{};
|
||||
std::string NodeId{};
|
||||
};
|
||||
std::vector<OpenNode> stack{{"__root", root.Id}};
|
||||
std::unordered_set<std::string> ids{root.Id};
|
||||
std::size_t generatedId = 0;
|
||||
auto diagnostic = [&](std::string node, std::string message, bool error = false) {
|
||||
result.Diagnostics.push_back({result.Document.Name, std::move(node), {}, std::move(message), error});
|
||||
};
|
||||
auto appendText = [&](std::string_view rawText) {
|
||||
const std::string text = Trim(DecodeLegacyText(rawText));
|
||||
if (text.empty() || stack.size() <= 1) return;
|
||||
const std::string& nodeId = stack.back().NodeId;
|
||||
const auto node = std::find_if(result.Document.Nodes.begin(), result.Document.Nodes.end(),
|
||||
[&](const MetaCoreUiNodeDocument& candidate) { return candidate.Id == nodeId; });
|
||||
if (node != result.Document.Nodes.end() &&
|
||||
(node->Type == MetaCoreUiNodeType::Text || node->Type == MetaCoreUiNodeType::Button ||
|
||||
node->Type == MetaCoreUiNodeType::Input)) {
|
||||
if (!node->Text.empty()) node->Text += ' ';
|
||||
node->Text += text;
|
||||
}
|
||||
};
|
||||
|
||||
std::size_t position = 0;
|
||||
while (position < rml.size()) {
|
||||
const std::size_t tagStart = rml.find('<', position);
|
||||
if (tagStart == std::string_view::npos) {
|
||||
appendText(rml.substr(position));
|
||||
break;
|
||||
}
|
||||
appendText(rml.substr(position, tagStart - position));
|
||||
if (rml.substr(tagStart).starts_with("<!--")) {
|
||||
const std::size_t commentEnd = rml.find("-->", tagStart + 4);
|
||||
if (commentEnd == std::string_view::npos) {
|
||||
diagnostic({}, "Legacy RML contains an unterminated comment", true);
|
||||
break;
|
||||
}
|
||||
position = commentEnd + 3;
|
||||
continue;
|
||||
}
|
||||
const std::size_t tagEnd = rml.find('>', tagStart + 1);
|
||||
if (tagEnd == std::string_view::npos) {
|
||||
diagnostic({}, "Legacy RML contains an unterminated tag", true);
|
||||
break;
|
||||
}
|
||||
const std::string_view fullTag = rml.substr(tagStart, tagEnd - tagStart + 1);
|
||||
std::string_view body = fullTag.substr(1, fullTag.size() - 2);
|
||||
const bool closing = !body.empty() && body.front() == '/';
|
||||
if (closing) body.remove_prefix(1);
|
||||
body = std::string_view(body.data(), body.size());
|
||||
std::size_t nameLength = 0;
|
||||
while (nameLength < body.size() &&
|
||||
(std::isalnum(static_cast<unsigned char>(body[nameLength])) || body[nameLength] == '-')) ++nameLength;
|
||||
const std::string tagName = Lower(body.substr(0, nameLength));
|
||||
const bool selfClosing = (!body.empty() && body.back() == '/') ||
|
||||
tagName == "input" || tagName == "img" || tagName == "br" || tagName == "link" || tagName == "meta";
|
||||
position = tagEnd + 1;
|
||||
if (tagName.empty() || tagName == "!doctype" || tagName == "rml" || tagName == "html" ||
|
||||
tagName == "head" || tagName == "body" || tagName == "title" || tagName == "style" ||
|
||||
tagName == "link" || tagName == "meta") {
|
||||
continue;
|
||||
}
|
||||
if (closing) {
|
||||
const auto open = std::find_if(stack.rbegin(), stack.rend(),
|
||||
[&](const OpenNode& candidate) { return candidate.Tag == tagName; });
|
||||
if (open == stack.rend()) {
|
||||
diagnostic({}, "Ignored unmatched closing tag </" + tagName + ">");
|
||||
} else {
|
||||
stack.erase(open.base() - 1, stack.end());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const std::optional<MetaCoreUiNodeType> type = LegacyNodeType(tagName, fullTag);
|
||||
if (!type.has_value()) {
|
||||
diagnostic({}, "Unsupported legacy tag <" + tagName + "> was skipped");
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string id = LegacyAttribute(fullTag, "id");
|
||||
if (id.empty() || ids.contains(id)) {
|
||||
if (!id.empty()) diagnostic(id, "Duplicate legacy id was replaced with a deterministic generated id");
|
||||
do id = "LegacyNode" + std::to_string(++generatedId); while (ids.contains(id));
|
||||
}
|
||||
ids.insert(id);
|
||||
MetaCoreUiNodeDocument node;
|
||||
node.Id = id;
|
||||
node.Name = LegacyAttribute(fullTag, "name");
|
||||
if (node.Name.empty()) node.Name = id;
|
||||
node.ParentId = stack.back().NodeId;
|
||||
node.Type = *type;
|
||||
node.Placeholder = LegacyAttribute(fullTag, "placeholder");
|
||||
node.Interactable = *type == MetaCoreUiNodeType::Button || *type == MetaCoreUiNodeType::Input ||
|
||||
*type == MetaCoreUiNodeType::List || *type == MetaCoreUiNodeType::Scroll;
|
||||
if (*type == MetaCoreUiNodeType::Image) {
|
||||
const std::string source = LegacyAttribute(fullTag, "src");
|
||||
if (!source.empty()) diagnostic(id, "Image src '" + source + "' requires manual Asset GUID assignment");
|
||||
}
|
||||
const auto parent = std::find_if(result.Document.Nodes.begin(), result.Document.Nodes.end(),
|
||||
[&](const MetaCoreUiNodeDocument& candidate) { return candidate.Id == node.ParentId; });
|
||||
if (parent != result.Document.Nodes.end()) parent->Children.push_back(id);
|
||||
result.Document.Nodes.push_back(std::move(node));
|
||||
if (!selfClosing) stack.push_back({tagName, id});
|
||||
}
|
||||
if (!rcss.empty()) {
|
||||
diagnostic({}, "Legacy RCSS was diagnosed but not copied; migrate declarations to UiTheme tokens and node styles");
|
||||
}
|
||||
const std::vector<MetaCoreUiDiagnostic> validation = MetaCoreValidateUiDocument(result.Document);
|
||||
result.Diagnostics.insert(result.Diagnostics.end(), validation.begin(), validation.end());
|
||||
result.Succeeded = std::none_of(result.Diagnostics.begin(), result.Diagnostics.end(),
|
||||
[](const MetaCoreUiDiagnostic& item) { return item.IsError; });
|
||||
return result;
|
||||
}
|
||||
|
||||
MetaCoreUiDocument MetaCoreMigrateUiDocument(MetaCoreUiDocument document) {
|
||||
if (document.SchemaVersion >= METACORE_UI_SCHEMA_VERSION) {
|
||||
return document;
|
||||
@ -100,6 +384,10 @@ std::vector<MetaCoreUiDiagnostic> MetaCoreValidateUiDocument(const MetaCoreUiDoc
|
||||
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");
|
||||
if (document.MinAspectRatio < 0.0F || document.MaxAspectRatio < 0.0F ||
|
||||
(document.MinAspectRatio > 0.0F && document.MaxAspectRatio > 0.0F &&
|
||||
document.MaxAspectRatio < document.MinAspectRatio))
|
||||
issue({}, {}, "Canvas aspect ratio limits are invalid");
|
||||
|
||||
std::unordered_map<std::string, const MetaCoreUiNodeDocument*> nodes;
|
||||
for (const MetaCoreUiNodeDocument& node : document.Nodes) {
|
||||
@ -110,8 +398,28 @@ std::vector<MetaCoreUiDiagnostic> MetaCoreValidateUiDocument(const MetaCoreUiDoc
|
||||
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");
|
||||
if (node.Type != MetaCoreUiNodeType::List && (!node.StaticItems.empty() || !node.ItemTemplateNodeId.empty()))
|
||||
issue(node.Id, {}, "Static items and row templates are only valid on List nodes");
|
||||
std::unordered_set<std::string> listItemIds;
|
||||
for (const MetaCoreUiListItemDocument& item : node.StaticItems) {
|
||||
if (item.Id.empty()) issue(node.Id, {}, "Static list item requires a stable ID");
|
||||
else if (!listItemIds.insert(item.Id).second) issue(node.Id, {}, "Duplicate static list item ID");
|
||||
}
|
||||
std::unordered_set<std::string> animationIds;
|
||||
for (const MetaCoreUiAnimationDocument& animation : node.Animations) {
|
||||
if (animation.Id.empty()) issue(node.Id, {}, "Animation requires a stable ID");
|
||||
else if (!animationIds.insert(animation.Id).second) issue(node.Id, {}, "Duplicate animation ID");
|
||||
if (!std::isfinite(animation.DurationSeconds) || animation.DurationSeconds < 0.0F ||
|
||||
!std::isfinite(animation.DelaySeconds) || animation.DelaySeconds < 0.0F)
|
||||
issue(node.Id, {}, "Animation duration and delay must be finite and non-negative");
|
||||
}
|
||||
for (const MetaCoreUiBindingDocument& binding : node.Bindings) {
|
||||
if (!IsBindingPathValid(binding.SourcePath)) issue(node.Id, binding.SourcePath, "Binding path must use runtime., script. or scene.");
|
||||
static const std::unordered_set<std::string> converters{
|
||||
"", "not", "to_string", "uppercase", "lowercase", "to_bool", "to_int", "to_double"
|
||||
};
|
||||
if (!converters.contains(binding.Converter))
|
||||
issue(node.Id, binding.SourcePath, "Binding converter is not supported: " + binding.Converter);
|
||||
if (binding.Mode == MetaCoreUiBindingMode::TwoWay &&
|
||||
binding.Target != MetaCoreUiBindingTarget::Value &&
|
||||
binding.Target != MetaCoreUiBindingTarget::SelectedIndex)
|
||||
@ -122,6 +430,9 @@ std::vector<MetaCoreUiDiagnostic> MetaCoreValidateUiDocument(const MetaCoreUiDoc
|
||||
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 auto& node : document.Nodes)
|
||||
if (!node.ItemTemplateNodeId.empty() && !nodes.contains(node.ItemTemplateNodeId))
|
||||
issue(node.Id, {}, "List row template node does not exist");
|
||||
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;
|
||||
@ -152,9 +463,48 @@ std::vector<MetaCoreUiDiagnostic> MetaCoreValidateUiDocument(const MetaCoreUiDoc
|
||||
}
|
||||
|
||||
MetaCoreCompiledUiDocument MetaCoreCompileUiDocument(const MetaCoreUiDocument& source) {
|
||||
return MetaCoreCompileUiDocument(source, {});
|
||||
}
|
||||
|
||||
MetaCoreCompiledUiDocument MetaCoreCompileUiDocument(
|
||||
const MetaCoreUiDocument& source,
|
||||
const MetaCoreUiCompileResources& resources
|
||||
) {
|
||||
MetaCoreCompiledUiDocument result;
|
||||
const MetaCoreUiDocument document = MetaCoreMigrateUiDocument(source);
|
||||
result.Diagnostics = MetaCoreValidateUiDocument(document);
|
||||
const auto resourceIssue = [&](std::string node, std::string message) {
|
||||
result.Diagnostics.push_back({document.Name, std::move(node), {}, std::move(message), true});
|
||||
};
|
||||
if (resources.Theme.has_value()) {
|
||||
std::unordered_set<std::string> stateKeys;
|
||||
for (const auto& style : resources.Theme->Styles) {
|
||||
const std::string key = style.Token + ':' + std::to_string(static_cast<int>(style.State));
|
||||
if (style.Token.empty()) resourceIssue({}, "Theme style requires a token");
|
||||
else if (!stateKeys.insert(key).second) resourceIssue({}, "Duplicate theme token state: " + key);
|
||||
}
|
||||
}
|
||||
if (resources.FontFamily.has_value()) {
|
||||
for (const auto& face : resources.FontFamily->Faces)
|
||||
if (!face.FontAssetGuid.IsValid()) resourceIssue({}, "Font face requires an asset GUID");
|
||||
}
|
||||
if (resources.Localization.has_value()) {
|
||||
std::unordered_set<std::string> localeIds;
|
||||
for (const auto& locale : resources.Localization->Locales) {
|
||||
if (locale.Locale.empty() || !localeIds.insert(locale.Locale).second)
|
||||
resourceIssue({}, "Localization locale is empty or duplicated");
|
||||
std::unordered_set<std::string> keys;
|
||||
for (const auto& entry : locale.Entries)
|
||||
if (entry.Key.empty() || !keys.insert(entry.Key).second)
|
||||
resourceIssue({}, "Localization key is empty or duplicated in " + locale.Locale);
|
||||
}
|
||||
for (const auto& node : document.Nodes) {
|
||||
if (!node.LocalizationKey.empty() &&
|
||||
!MetaCoreResolveUiLocalizedText(*resources.Localization, node.LocalizationKey,
|
||||
document.DefaultLocale, document.FallbackLocale).has_value())
|
||||
resourceIssue(node.Id, "Localization key cannot be resolved: " + node.LocalizationKey);
|
||||
}
|
||||
}
|
||||
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;
|
||||
@ -164,6 +514,9 @@ MetaCoreCompiledUiDocument MetaCoreCompileUiDocument(const MetaCoreUiDocument& s
|
||||
}
|
||||
for (MetaCoreAssetGuid guid : {document.ThemeAssetGuid, document.FontFamilyAssetGuid, document.LocalizationAssetGuid})
|
||||
if (guid.IsValid()) result.Dependencies.push_back(guid);
|
||||
if (resources.FontFamily.has_value())
|
||||
for (const auto& face : resources.FontFamily->Faces)
|
||||
if (face.FontAssetGuid.IsValid()) result.Dependencies.push_back(face.FontAssetGuid);
|
||||
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());
|
||||
|
||||
@ -174,20 +527,44 @@ MetaCoreCompiledUiDocument MetaCoreCompileUiDocument(const MetaCoreUiDocument& s
|
||||
<< " 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";
|
||||
<< " data-metacore-min-scale=\"" << document.MinScale << "\""
|
||||
<< " data-metacore-max-scale=\"" << document.MaxScale << "\""
|
||||
<< " data-metacore-min-aspect=\"" << document.MinAspectRatio << "\""
|
||||
<< " data-metacore-max-aspect=\"" << document.MaxAspectRatio << "\""
|
||||
<< " data-metacore-safe-area=\"" << (document.UseSafeArea ? "true" : "false") << "\"";
|
||||
if (resources.Theme.has_value()) rml << " data-metacore-theme=\"" << Escape(resources.Theme->Name) << "\"";
|
||||
rml << ">\n";
|
||||
if (resources.FontFamily.has_value()) {
|
||||
std::string requiredCharacters;
|
||||
for (const auto& face : resources.FontFamily->Faces) requiredCharacters += face.RequiredCharacters;
|
||||
if (!requiredCharacters.empty())
|
||||
rml << " <span id=\"mcui-font-prewarm\" aria-hidden=\"true\">" << Escape(requiredCharacters) << "</span>\n";
|
||||
}
|
||||
|
||||
std::int32_t documentOrder = 0;
|
||||
std::function<void(const std::string&, int)> emit = [&](const std::string& id, int depth) {
|
||||
const MetaCoreUiNodeDocument& node = *nodes.at(id);
|
||||
const std::int32_t currentDocumentOrder = documentOrder++;
|
||||
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";
|
||||
else if (node.Type == MetaCoreUiNodeType::List) tag = "select";
|
||||
else if (node.Type == MetaCoreUiNodeType::Image) {
|
||||
const bool isAtlased = std::any_of(resources.AtlasEntries.begin(), resources.AtlasEntries.end(),
|
||||
[&](const MetaCoreUiCompileResources::AtlasEntry& entry) {
|
||||
return entry.AssetGuid == node.Style.ImageAssetGuid;
|
||||
});
|
||||
tag = isAtlased ? "div" : "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 << "\"";
|
||||
else if (node.Type == MetaCoreUiNodeType::Button || node.Type == MetaCoreUiNodeType::Input ||
|
||||
node.Type == MetaCoreUiNodeType::List)
|
||||
rml << " tabindex=\"" << (100000 + currentDocumentOrder) << "\"";
|
||||
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) << "\"";
|
||||
@ -196,6 +573,7 @@ MetaCoreCompiledUiDocument MetaCoreCompileUiDocument(const MetaCoreUiDocument& s
|
||||
<< " 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.Converter.empty()) rml << " data-metacore-converter-" << BindingTargetName(binding.Target) << "=\"" << Escape(binding.Converter) << "\"";
|
||||
if (!binding.Fallback.empty()) rml << " data-metacore-fallback-" << BindingTargetName(binding.Target) << "=\"" << Escape(binding.Fallback) << "\"";
|
||||
}
|
||||
if (node.Bindings.empty() && !node.DataBinding.empty()) {
|
||||
@ -207,14 +585,47 @@ MetaCoreCompiledUiDocument MetaCoreCompileUiDocument(const MetaCoreUiDocument& s
|
||||
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";
|
||||
rml << " data-metacore-input-type=\"" << static_cast<int>(node.InputValueType) << "\""
|
||||
<< " type=\"" << (node.InputValueType == MetaCoreUiInputValueType::Text ? "text" : "number") << "\"";
|
||||
if (node.InputValueType == MetaCoreUiInputValueType::Integer) rml << " step=\"1\"";
|
||||
else if (node.InputValueType == MetaCoreUiInputValueType::Number) rml << " step=\"any\"";
|
||||
rml << "/>\n";
|
||||
return;
|
||||
}
|
||||
if (node.Type == MetaCoreUiNodeType::Image && node.Style.ImageAssetGuid.IsValid())
|
||||
if (node.Type == MetaCoreUiNodeType::Image && node.Style.ImageAssetGuid.IsValid() &&
|
||||
std::none_of(resources.AtlasEntries.begin(), resources.AtlasEntries.end(),
|
||||
[&](const MetaCoreUiCompileResources::AtlasEntry& entry) {
|
||||
return entry.AssetGuid == node.Style.ImageAssetGuid;
|
||||
}))
|
||||
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.Type == MetaCoreUiNodeType::Text || node.Type == MetaCoreUiNodeType::Button) {
|
||||
std::string text = node.Text;
|
||||
if (resources.Localization.has_value() && !node.LocalizationKey.empty()) {
|
||||
text = MetaCoreResolveUiLocalizedText(
|
||||
*resources.Localization,
|
||||
node.LocalizationKey,
|
||||
document.DefaultLocale,
|
||||
document.FallbackLocale
|
||||
).value_or(node.Text);
|
||||
}
|
||||
rml << Escape(text);
|
||||
}
|
||||
if (node.Type == MetaCoreUiNodeType::List) {
|
||||
for (std::size_t index = 0; index < node.StaticItems.size(); ++index) {
|
||||
const auto& item = node.StaticItems[index];
|
||||
std::string text = item.Text;
|
||||
if (resources.Localization.has_value() && !item.LocalizationKey.empty())
|
||||
text = MetaCoreResolveUiLocalizedText(*resources.Localization, item.LocalizationKey,
|
||||
document.DefaultLocale, document.FallbackLocale).value_or(item.Text);
|
||||
rml << "<option id=\"" << Escape(node.Id + "-item-" + item.Id) << "\" value=\""
|
||||
<< Escape(item.Value) << "\" data-metacore-index=\"" << index << "\"";
|
||||
if (static_cast<std::int32_t>(index) == node.SelectedIndex) rml << " selected=\"true\"";
|
||||
if (!item.LocalizationKey.empty()) rml << " data-metacore-loc=\"" << Escape(item.LocalizationKey) << "\"";
|
||||
rml << '>' << Escape(text) << "</option>";
|
||||
}
|
||||
}
|
||||
if (!node.Children.empty()) rml << '\n';
|
||||
for (const std::string& child : node.Children) emit(child, depth + 2);
|
||||
if (!node.Children.empty()) rml << indent;
|
||||
@ -224,10 +635,28 @@ MetaCoreCompiledUiDocument MetaCoreCompileUiDocument(const MetaCoreUiDocument& s
|
||||
rml << "</body></rml>\n";
|
||||
|
||||
std::ostringstream css;
|
||||
if (!resources.AtlasFilename.empty() && !resources.AtlasEntries.empty()) {
|
||||
css << "@spritesheet metacore-ui-atlas { src:url(\"Assets/" << Escape(resources.AtlasFilename) << "\");";
|
||||
for (const auto& entry : resources.AtlasEntries) {
|
||||
css << " sprite:" << entry.AssetGuid.ToString() << ' ' << entry.X << "px " << entry.Y << "px "
|
||||
<< entry.Width << "px " << entry.Height << "px;";
|
||||
}
|
||||
css << " }\n";
|
||||
}
|
||||
if (resources.FontFamily.has_value()) {
|
||||
const auto& family = *resources.FontFamily;
|
||||
for (const auto& face : family.Faces) {
|
||||
if (!face.FontAssetGuid.IsValid()) continue;
|
||||
css << "@font-face { font-family:\"" << Escape(family.FamilyName) << "\"; src:url(\"Assets/"
|
||||
<< face.FontAssetGuid.ToString() << ".font\"); font-weight:" << face.Weight
|
||||
<< "; font-style:" << (face.Italic ? "italic" : "normal") << "; }\n";
|
||||
}
|
||||
}
|
||||
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-font-prewarm { position:absolute; left:-10000px; top:-10000px; opacity:0; }\n"
|
||||
<< ".mcui-scroll { overflow:auto; }\n.mcui-list { overflow:auto; }\n";
|
||||
for (const auto& node : document.Nodes) {
|
||||
const auto& rt = node.RectTransform;
|
||||
@ -244,7 +673,74 @@ MetaCoreCompiledUiDocument MetaCoreCompileUiDocument(const MetaCoreUiDocument& s
|
||||
<< 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";
|
||||
<< ';';
|
||||
if (const auto atlas = std::find_if(resources.AtlasEntries.begin(), resources.AtlasEntries.end(),
|
||||
[&](const MetaCoreUiCompileResources::AtlasEntry& entry) {
|
||||
return node.Type == MetaCoreUiNodeType::Image && entry.AssetGuid == node.Style.ImageAssetGuid;
|
||||
}); atlas != resources.AtlasEntries.end())
|
||||
css << " decorator:image(" << atlas->AssetGuid.ToString() << ");";
|
||||
css << " }\n";
|
||||
for (const MetaCoreUiAnimationDocument& animation : node.Animations) {
|
||||
const std::string animationName = "mcui-" + node.Id + '-' + animation.Id;
|
||||
css << "@keyframes " << animationName << " { from { " << AnimationPropertyName(animation.Property)
|
||||
<< ':' << AnimationValue(animation.Property, animation.From) << "; } to { "
|
||||
<< AnimationPropertyName(animation.Property) << ':' << AnimationValue(animation.Property, animation.To)
|
||||
<< "; } }\n";
|
||||
std::string selector = "#" + node.Id;
|
||||
switch (animation.Trigger) {
|
||||
case MetaCoreUiAnimationTrigger::Show: selector += ":not([hidden])"; break;
|
||||
case MetaCoreUiAnimationTrigger::Hide: selector += "[hidden]"; break;
|
||||
case MetaCoreUiAnimationTrigger::Hover: selector += ":hover"; break;
|
||||
case MetaCoreUiAnimationTrigger::Pressed: selector += ":active"; break;
|
||||
case MetaCoreUiAnimationTrigger::Focus: selector += ":focus"; break;
|
||||
case MetaCoreUiAnimationTrigger::Custom: selector += ".animate-" + animation.Id; break;
|
||||
}
|
||||
css << selector << " { animation:" << animationName << ' ' << animation.DurationSeconds << "s "
|
||||
<< EasingName(animation.Easing) << ' ' << animation.DelaySeconds << "s both; }\n";
|
||||
}
|
||||
}
|
||||
if (resources.Theme.has_value()) {
|
||||
const auto& theme = *resources.Theme;
|
||||
for (const auto& style : theme.Styles) {
|
||||
if (style.Token.empty()) continue;
|
||||
css << "body[data-metacore-theme=\"" << Escape(theme.Name) << "\"] .theme-" << style.Token
|
||||
<< ThemeStateSelector(style.State) << " { background-color:" << Color(style.BackgroundColor, style.Opacity)
|
||||
<< "; color:" << Color(style.TextColor) << "; border-radius:" << style.BorderRadius
|
||||
<< "px; font-size:" << style.FontSize << "px; }\n";
|
||||
}
|
||||
}
|
||||
if (resources.FontFamily.has_value()) {
|
||||
css << "body { font-family:\"" << Escape(resources.FontFamily->FamilyName) << "\"";
|
||||
for (const std::string& fallback : resources.FontFamily->FallbackFamilies)
|
||||
css << ",\"" << Escape(fallback) << "\"";
|
||||
css << "; }\n";
|
||||
}
|
||||
if (resources.Localization.has_value()) {
|
||||
std::vector<const MetaCoreUiLocalizationLocaleDocument*> locales;
|
||||
for (const auto& locale : resources.Localization->Locales) locales.push_back(&locale);
|
||||
std::sort(locales.begin(), locales.end(), [](const auto* lhs, const auto* rhs) { return lhs->Locale < rhs->Locale; });
|
||||
std::ostringstream localization;
|
||||
localization << "{\"default\":\"" << EscapeJson(resources.Localization->DefaultLocale)
|
||||
<< "\",\"fallback\":\"" << EscapeJson(resources.Localization->FallbackLocale)
|
||||
<< "\",\"locales\":{";
|
||||
bool firstLocale = true;
|
||||
for (const auto* locale : locales) {
|
||||
if (!firstLocale) localization << ',';
|
||||
firstLocale = false;
|
||||
localization << '"' << EscapeJson(locale->Locale) << "\":{";
|
||||
std::vector<const MetaCoreUiLocalizationEntryDocument*> entries;
|
||||
for (const auto& entry : locale->Entries) entries.push_back(&entry);
|
||||
std::sort(entries.begin(), entries.end(), [](const auto* lhs, const auto* rhs) { return lhs->Key < rhs->Key; });
|
||||
bool firstEntry = true;
|
||||
for (const auto* entry : entries) {
|
||||
if (!firstEntry) localization << ',';
|
||||
firstEntry = false;
|
||||
localization << '"' << EscapeJson(entry->Key) << "\":\"" << EscapeJson(entry->Value) << '"';
|
||||
}
|
||||
localization << '}';
|
||||
}
|
||||
localization << "}}";
|
||||
result.LocalizationJson = localization.str();
|
||||
}
|
||||
result.Rml = rml.str();
|
||||
result.Rcss = css.str();
|
||||
@ -252,4 +748,106 @@ MetaCoreCompiledUiDocument MetaCoreCompileUiDocument(const MetaCoreUiDocument& s
|
||||
return result;
|
||||
}
|
||||
|
||||
MetaCoreUiCanvasLayout MetaCoreComputeUiCanvasLayout(
|
||||
const MetaCoreUiDocument& document,
|
||||
const MetaCoreUiCanvasEnvironment& environment
|
||||
) {
|
||||
MetaCoreUiCanvasLayout result;
|
||||
const float fullWidth = std::max(1.0F, environment.Width);
|
||||
const float fullHeight = std::max(1.0F, environment.Height);
|
||||
result.ViewportLeft = document.UseSafeArea ? std::max(0.0F, environment.SafeInsetLeft) : 0.0F;
|
||||
result.ViewportTop = document.UseSafeArea ? std::max(0.0F, environment.SafeInsetTop) : 0.0F;
|
||||
result.ViewportWidth = std::max(1.0F, fullWidth - result.ViewportLeft -
|
||||
(document.UseSafeArea ? std::max(0.0F, environment.SafeInsetRight) : 0.0F));
|
||||
result.ViewportHeight = std::max(1.0F, fullHeight - result.ViewportTop -
|
||||
(document.UseSafeArea ? std::max(0.0F, environment.SafeInsetBottom) : 0.0F));
|
||||
|
||||
float aspect = result.ViewportWidth / result.ViewportHeight;
|
||||
if (document.MinAspectRatio > 0.0F && aspect < document.MinAspectRatio) {
|
||||
const float constrainedHeight = result.ViewportWidth / document.MinAspectRatio;
|
||||
result.ViewportTop += (result.ViewportHeight - constrainedHeight) * 0.5F;
|
||||
result.ViewportHeight = constrainedHeight;
|
||||
} else if (document.MaxAspectRatio > 0.0F && aspect > document.MaxAspectRatio) {
|
||||
const float constrainedWidth = result.ViewportHeight * document.MaxAspectRatio;
|
||||
result.ViewportLeft += (result.ViewportWidth - constrainedWidth) * 0.5F;
|
||||
result.ViewportWidth = constrainedWidth;
|
||||
}
|
||||
|
||||
switch (document.ScaleMode) {
|
||||
case MetaCoreUiCanvasScaleMode::ConstantPixelSize:
|
||||
result.Scale = 1.0F;
|
||||
break;
|
||||
case MetaCoreUiCanvasScaleMode::ScaleWithScreenSize: {
|
||||
const float widthScale = result.ViewportWidth / static_cast<float>(std::max(1, document.ReferenceWidth));
|
||||
const float heightScale = result.ViewportHeight / static_cast<float>(std::max(1, document.ReferenceHeight));
|
||||
const float match = std::clamp(document.MatchWidthOrHeight, 0.0F, 1.0F);
|
||||
result.Scale = std::exp2(std::log2(std::max(widthScale, 0.0001F)) * (1.0F - match) +
|
||||
std::log2(std::max(heightScale, 0.0001F)) * match);
|
||||
break;
|
||||
}
|
||||
case MetaCoreUiCanvasScaleMode::ConstantPhysicalSize:
|
||||
result.Scale = std::max(1.0F, environment.Dpi) / std::max(1.0F, document.ReferenceDpi);
|
||||
break;
|
||||
}
|
||||
result.Scale = std::clamp(result.Scale, std::max(0.0001F, document.MinScale), std::max(document.MinScale, document.MaxScale));
|
||||
result.LogicalWidth = result.ViewportWidth / result.Scale;
|
||||
result.LogicalHeight = result.ViewportHeight / result.Scale;
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<std::string> MetaCoreResolveUiLocalizedText(
|
||||
const MetaCoreUiLocalizationTableDocument& table,
|
||||
std::string_view key,
|
||||
std::string_view locale,
|
||||
std::string_view explicitFallbackLocale
|
||||
) {
|
||||
const auto lookup = [&](std::string_view requested) -> std::optional<std::string> {
|
||||
if (requested.empty()) return std::nullopt;
|
||||
const auto foundLocale = std::find_if(table.Locales.begin(), table.Locales.end(), [&](const auto& value) {
|
||||
return value.Locale == requested;
|
||||
});
|
||||
if (foundLocale == table.Locales.end()) return std::nullopt;
|
||||
const auto foundEntry = std::find_if(foundLocale->Entries.begin(), foundLocale->Entries.end(), [&](const auto& value) {
|
||||
return value.Key == key;
|
||||
});
|
||||
return foundEntry == foundLocale->Entries.end() ? std::nullopt : std::optional<std::string>(foundEntry->Value);
|
||||
};
|
||||
std::vector<std::string_view> chain{locale, explicitFallbackLocale, table.FallbackLocale, table.DefaultLocale};
|
||||
for (const std::string_view candidate : chain)
|
||||
if (const auto value = lookup(candidate); value.has_value()) return value;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
MetaCoreUiWorldSurfaceLayout MetaCoreComputeUiWorldSurfaceLayout(
|
||||
glm::vec3 worldSize,
|
||||
float pixelsPerUnit,
|
||||
std::int32_t maximumTextureSize
|
||||
) {
|
||||
MetaCoreUiWorldSurfaceLayout result;
|
||||
result.PixelsPerUnit = std::max(1.0F, pixelsPerUnit);
|
||||
const auto maximum = std::max(1, maximumTextureSize);
|
||||
result.TargetWidth = static_cast<std::int32_t>(std::clamp(
|
||||
std::round(std::max(0.001F, worldSize.x) * result.PixelsPerUnit), 1.0F, static_cast<float>(maximum)));
|
||||
result.TargetHeight = static_cast<std::int32_t>(std::clamp(
|
||||
std::round(std::max(0.001F, worldSize.y) * result.PixelsPerUnit), 1.0F, static_cast<float>(maximum)));
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<glm::vec2> MetaCoreMapWorldUiHitToUv(
|
||||
const glm::mat4& worldMatrix,
|
||||
glm::vec3 worldSize,
|
||||
glm::vec3 worldHitPosition
|
||||
) {
|
||||
if (worldSize.x <= 0.0F || worldSize.y <= 0.0F) return std::nullopt;
|
||||
const glm::vec4 local = glm::inverse(worldMatrix) * glm::vec4(worldHitPosition, 1.0F);
|
||||
const glm::vec2 uv{
|
||||
local.x / worldSize.x + 0.5F,
|
||||
0.5F - local.y / worldSize.y
|
||||
};
|
||||
constexpr float tolerance = 0.0001F;
|
||||
if (uv.x < -tolerance || uv.y < -tolerance || uv.x > 1.0F + tolerance || uv.y > 1.0F + tolerance)
|
||||
return std::nullopt;
|
||||
return glm::clamp(uv, glm::vec2(0.0F), glm::vec2(1.0F));
|
||||
}
|
||||
|
||||
} // namespace MetaCore
|
||||
|
||||
@ -10,8 +10,10 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <glm/mat4x4.hpp>
|
||||
|
||||
namespace MetaCore {
|
||||
|
||||
@ -41,6 +43,14 @@ enum class MetaCoreRuntimeUiInputSource : std::uint32_t {
|
||||
WorldPointer
|
||||
};
|
||||
|
||||
enum class MetaCoreRuntimeUiWorldPointerType : std::uint32_t {
|
||||
Move = 0,
|
||||
Down,
|
||||
Up,
|
||||
Scroll,
|
||||
Cancel
|
||||
};
|
||||
|
||||
enum class MetaCoreRuntimeUiValueType : std::uint32_t {
|
||||
Bool = 0,
|
||||
Int64,
|
||||
@ -74,6 +84,8 @@ struct MetaCoreRuntimeUiEvent {
|
||||
std::string Type{};
|
||||
std::string Value{};
|
||||
std::string OldValue{};
|
||||
MetaCoreRuntimeUiValue NewTypedValue{};
|
||||
MetaCoreRuntimeUiValue OldTypedValue{};
|
||||
MetaCoreRuntimeUiEventType EventType = MetaCoreRuntimeUiEventType::Click;
|
||||
MetaCoreRuntimeUiInputSource InputSource = MetaCoreRuntimeUiInputSource::Programmatic;
|
||||
bool Consumed = false;
|
||||
@ -90,18 +102,99 @@ struct MetaCoreUiDiagnostic {
|
||||
struct MetaCoreCompiledUiDocument {
|
||||
std::string Rml{};
|
||||
std::string Rcss{};
|
||||
std::string LocalizationJson{};
|
||||
std::vector<MetaCoreAssetGuid> Dependencies{};
|
||||
std::vector<MetaCoreUiDiagnostic> Diagnostics{};
|
||||
bool Succeeded = false;
|
||||
};
|
||||
|
||||
struct MetaCoreUiCompileResources {
|
||||
struct AtlasEntry {
|
||||
MetaCoreAssetGuid AssetGuid{};
|
||||
std::int32_t X = 0;
|
||||
std::int32_t Y = 0;
|
||||
std::int32_t Width = 0;
|
||||
std::int32_t Height = 0;
|
||||
};
|
||||
std::optional<MetaCoreUiThemeDocument> Theme{};
|
||||
std::optional<MetaCoreUiFontFamilyDocument> FontFamily{};
|
||||
std::optional<MetaCoreUiLocalizationTableDocument> Localization{};
|
||||
std::string AtlasFilename{};
|
||||
std::int32_t AtlasWidth = 0;
|
||||
std::int32_t AtlasHeight = 0;
|
||||
std::vector<AtlasEntry> AtlasEntries{};
|
||||
};
|
||||
|
||||
struct MetaCoreLegacyUiImportResult {
|
||||
MetaCoreUiDocument Document{};
|
||||
std::vector<MetaCoreUiDiagnostic> Diagnostics{};
|
||||
bool Succeeded = false;
|
||||
};
|
||||
|
||||
struct MetaCoreUiCanvasEnvironment {
|
||||
float Width = 1920.0F;
|
||||
float Height = 1080.0F;
|
||||
float Dpi = 96.0F;
|
||||
float SafeInsetLeft = 0.0F;
|
||||
float SafeInsetTop = 0.0F;
|
||||
float SafeInsetRight = 0.0F;
|
||||
float SafeInsetBottom = 0.0F;
|
||||
};
|
||||
|
||||
struct MetaCoreUiCanvasLayout {
|
||||
float Scale = 1.0F;
|
||||
float LogicalWidth = 1920.0F;
|
||||
float LogicalHeight = 1080.0F;
|
||||
float ViewportLeft = 0.0F;
|
||||
float ViewportTop = 0.0F;
|
||||
float ViewportWidth = 1920.0F;
|
||||
float ViewportHeight = 1080.0F;
|
||||
};
|
||||
|
||||
struct MetaCoreUiWorldSurfaceLayout {
|
||||
std::int32_t TargetWidth = 1;
|
||||
std::int32_t TargetHeight = 1;
|
||||
float PixelsPerUnit = 1.0F;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::vector<MetaCoreUiDiagnostic> MetaCoreValidateUiDocument(const MetaCoreUiDocument& document);
|
||||
[[nodiscard]] MetaCoreCompiledUiDocument MetaCoreCompileUiDocument(const MetaCoreUiDocument& document);
|
||||
[[nodiscard]] MetaCoreCompiledUiDocument MetaCoreCompileUiDocument(
|
||||
const MetaCoreUiDocument& document,
|
||||
const MetaCoreUiCompileResources& resources
|
||||
);
|
||||
[[nodiscard]] MetaCoreUiDocument MetaCoreMigrateUiDocument(MetaCoreUiDocument document);
|
||||
[[nodiscard]] MetaCoreLegacyUiImportResult MetaCoreImportLegacyUiDocument(
|
||||
std::string_view rml,
|
||||
std::string_view rcss,
|
||||
std::string_view documentName = "ImportedUi"
|
||||
);
|
||||
[[nodiscard]] MetaCoreUiCanvasLayout MetaCoreComputeUiCanvasLayout(
|
||||
const MetaCoreUiDocument& document,
|
||||
const MetaCoreUiCanvasEnvironment& environment
|
||||
);
|
||||
[[nodiscard]] std::optional<std::string> MetaCoreResolveUiLocalizedText(
|
||||
const MetaCoreUiLocalizationTableDocument& table,
|
||||
std::string_view key,
|
||||
std::string_view locale,
|
||||
std::string_view explicitFallbackLocale = {}
|
||||
);
|
||||
[[nodiscard]] MetaCoreUiWorldSurfaceLayout MetaCoreComputeUiWorldSurfaceLayout(
|
||||
glm::vec3 worldSize,
|
||||
float pixelsPerUnit,
|
||||
std::int32_t maximumTextureSize = 8192
|
||||
);
|
||||
[[nodiscard]] std::optional<glm::vec2> MetaCoreMapWorldUiHitToUv(
|
||||
const glm::mat4& worldMatrix,
|
||||
glm::vec3 worldSize,
|
||||
glm::vec3 worldHitPosition
|
||||
);
|
||||
|
||||
class MetaCoreRuntimeUiSystem {
|
||||
public:
|
||||
using EventCallback = std::function<void(const MetaCoreRuntimeUiEvent&)>;
|
||||
using ProviderReadCallback = std::function<std::optional<MetaCoreRuntimeUiValue>(std::string_view)>;
|
||||
using ProviderWriteCallback = std::function<bool(std::string_view, const MetaCoreRuntimeUiValue&)>;
|
||||
|
||||
MetaCoreRuntimeUiSystem();
|
||||
~MetaCoreRuntimeUiSystem();
|
||||
@ -118,10 +211,24 @@ public:
|
||||
|
||||
void Update(float deltaSeconds);
|
||||
[[nodiscard]] MetaCoreInputConsumption ProcessInput();
|
||||
[[nodiscard]] bool ProcessWorldPointer(
|
||||
std::uint64_t sceneObjectId,
|
||||
glm::vec2 surfaceUv,
|
||||
MetaCoreRuntimeUiWorldPointerType type,
|
||||
std::uint32_t button = 0,
|
||||
float scrollDelta = 0.0F
|
||||
);
|
||||
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 RegisterProvider(
|
||||
std::string providerName,
|
||||
ProviderReadCallback reader,
|
||||
ProviderWriteCallback writer = {}
|
||||
);
|
||||
[[nodiscard]] bool UnregisterProvider(std::string_view providerName);
|
||||
void RefreshProviders();
|
||||
[[nodiscard]] bool RequestFocus(std::uint64_t instanceId, const std::string& elementId);
|
||||
[[nodiscard]] bool CreateInstance(const MetaCoreRuntimeUiDocumentEntry& entry);
|
||||
[[nodiscard]] bool DestroyInstance(std::uint64_t instanceId);
|
||||
|
||||
@ -935,6 +935,46 @@ static MetaCoreUiBindingDocument JsonToUiBinding(const json& j) {
|
||||
return binding;
|
||||
}
|
||||
|
||||
static json UiAnimationToJson(const MetaCoreUiAnimationDocument& animation) {
|
||||
return json{
|
||||
{"Id", animation.Id},
|
||||
{"Trigger", static_cast<int>(animation.Trigger)},
|
||||
{"Property", static_cast<int>(animation.Property)},
|
||||
{"From", Vec3ToJson(animation.From)},
|
||||
{"To", Vec3ToJson(animation.To)},
|
||||
{"DurationSeconds", animation.DurationSeconds},
|
||||
{"DelaySeconds", animation.DelaySeconds},
|
||||
{"Easing", static_cast<int>(animation.Easing)}
|
||||
};
|
||||
}
|
||||
|
||||
static MetaCoreUiAnimationDocument JsonToUiAnimation(const json& j) {
|
||||
MetaCoreUiAnimationDocument animation;
|
||||
if (j.contains("Id")) animation.Id = j["Id"].get<std::string>();
|
||||
if (j.contains("Trigger")) animation.Trigger = static_cast<MetaCoreUiAnimationTrigger>(j["Trigger"].get<int>());
|
||||
if (j.contains("Property")) animation.Property = static_cast<MetaCoreUiAnimationProperty>(j["Property"].get<int>());
|
||||
if (j.contains("From")) animation.From = JsonToVec3(j["From"]);
|
||||
if (j.contains("To")) animation.To = JsonToVec3(j["To"]);
|
||||
if (j.contains("DurationSeconds")) animation.DurationSeconds = j["DurationSeconds"].get<float>();
|
||||
if (j.contains("DelaySeconds")) animation.DelaySeconds = j["DelaySeconds"].get<float>();
|
||||
if (j.contains("Easing")) animation.Easing = static_cast<MetaCoreUiEasing>(j["Easing"].get<int>());
|
||||
return animation;
|
||||
}
|
||||
|
||||
static json UiListItemToJson(const MetaCoreUiListItemDocument& item) {
|
||||
return json{{"Id", item.Id}, {"Text", item.Text}, {"Value", item.Value},
|
||||
{"LocalizationKey", item.LocalizationKey}};
|
||||
}
|
||||
|
||||
static MetaCoreUiListItemDocument JsonToUiListItem(const json& j) {
|
||||
MetaCoreUiListItemDocument item;
|
||||
if (j.contains("Id")) item.Id = j["Id"].get<std::string>();
|
||||
if (j.contains("Text")) item.Text = j["Text"].get<std::string>();
|
||||
if (j.contains("Value")) item.Value = j["Value"].get<std::string>();
|
||||
if (j.contains("LocalizationKey")) item.LocalizationKey = j["LocalizationKey"].get<std::string>();
|
||||
return item;
|
||||
}
|
||||
|
||||
static json UiNodeToJson(const MetaCoreUiNodeDocument& node) {
|
||||
json j = json::object();
|
||||
j["Id"] = node.Id;
|
||||
@ -958,10 +998,14 @@ static json UiNodeToJson(const MetaCoreUiNodeDocument& node) {
|
||||
j["ReadOnly"] = node.ReadOnly;
|
||||
j["MaxLength"] = node.MaxLength;
|
||||
j["SelectedIndex"] = node.SelectedIndex;
|
||||
j["StaticItems"] = json::array();
|
||||
for (const auto& item : node.StaticItems) j["StaticItems"].push_back(UiListItemToJson(item));
|
||||
j["ItemTemplateNodeId"] = node.ItemTemplateNodeId;
|
||||
j["LocalizationKey"] = node.LocalizationKey;
|
||||
j["ShowAnimation"] = node.ShowAnimation;
|
||||
j["HideAnimation"] = node.HideAnimation;
|
||||
j["Animations"] = json::array();
|
||||
for (const auto& animation : node.Animations) j["Animations"].push_back(UiAnimationToJson(animation));
|
||||
return j;
|
||||
}
|
||||
|
||||
@ -988,10 +1032,14 @@ static MetaCoreUiNodeDocument JsonToUiNode(const json& j) {
|
||||
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("StaticItems") && j["StaticItems"].is_array())
|
||||
for (const auto& item : j["StaticItems"]) node.StaticItems.push_back(JsonToUiListItem(item));
|
||||
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>();
|
||||
if (j.contains("Animations") && j["Animations"].is_array())
|
||||
for (const auto& animation : j["Animations"]) node.Animations.push_back(JsonToUiAnimation(animation));
|
||||
return node;
|
||||
}
|
||||
|
||||
@ -1014,6 +1062,8 @@ bool MetaCoreSceneSerializer::SaveUiToJson(
|
||||
uiJson["MinScale"] = uiDocument.MinScale;
|
||||
uiJson["MaxScale"] = uiDocument.MaxScale;
|
||||
uiJson["UseSafeArea"] = uiDocument.UseSafeArea;
|
||||
uiJson["MinAspectRatio"] = uiDocument.MinAspectRatio;
|
||||
uiJson["MaxAspectRatio"] = uiDocument.MaxAspectRatio;
|
||||
uiJson["ThemeAssetGuid"] = GuidToString(uiDocument.ThemeAssetGuid);
|
||||
uiJson["FontFamilyAssetGuid"] = GuidToString(uiDocument.FontFamilyAssetGuid);
|
||||
uiJson["LocalizationAssetGuid"] = GuidToString(uiDocument.LocalizationAssetGuid);
|
||||
@ -1065,6 +1115,8 @@ std::optional<MetaCoreUiDocument> MetaCoreSceneSerializer::LoadUiFromJson(
|
||||
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("MinAspectRatio")) doc.MinAspectRatio = uiJson["MinAspectRatio"].get<float>();
|
||||
if (uiJson.contains("MaxAspectRatio")) doc.MaxAspectRatio = uiJson["MaxAspectRatio"].get<float>();
|
||||
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>());
|
||||
@ -1085,4 +1137,171 @@ std::optional<MetaCoreUiDocument> MetaCoreSceneSerializer::LoadUiFromJson(
|
||||
}
|
||||
}
|
||||
|
||||
bool MetaCoreSceneSerializer::SaveUiThemeToJson(
|
||||
const std::filesystem::path& absolutePath,
|
||||
const MetaCoreUiThemeDocument& document
|
||||
) {
|
||||
try {
|
||||
json root{{"SchemaVersion", document.SchemaVersion}, {"Name", document.Name}};
|
||||
root["Styles"] = json::array();
|
||||
for (const auto& style : document.Styles) {
|
||||
root["Styles"].push_back({
|
||||
{"Token", style.Token},
|
||||
{"State", static_cast<int>(style.State)},
|
||||
{"BackgroundColor", Vec3ToJson(style.BackgroundColor)},
|
||||
{"TextColor", Vec3ToJson(style.TextColor)},
|
||||
{"Opacity", style.Opacity},
|
||||
{"BorderRadius", style.BorderRadius},
|
||||
{"FontSize", style.FontSize}
|
||||
});
|
||||
}
|
||||
std::ofstream output(absolutePath);
|
||||
return output.is_open() && static_cast<bool>(output << root.dump(4));
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<MetaCoreUiThemeDocument> MetaCoreSceneSerializer::LoadUiThemeFromJson(
|
||||
const std::filesystem::path& absolutePath
|
||||
) {
|
||||
try {
|
||||
std::ifstream input(absolutePath);
|
||||
if (!input.is_open()) return std::nullopt;
|
||||
json root;
|
||||
input >> root;
|
||||
MetaCoreUiThemeDocument result;
|
||||
if (root.contains("SchemaVersion")) result.SchemaVersion = root["SchemaVersion"].get<std::uint32_t>();
|
||||
if (root.contains("Name")) result.Name = root["Name"].get<std::string>();
|
||||
if (root.contains("Styles") && root["Styles"].is_array()) {
|
||||
for (const auto& value : root["Styles"]) {
|
||||
MetaCoreUiThemeStyleDocument style;
|
||||
if (value.contains("Token")) style.Token = value["Token"].get<std::string>();
|
||||
if (value.contains("State")) style.State = static_cast<MetaCoreUiVisualState>(value["State"].get<int>());
|
||||
if (value.contains("BackgroundColor")) style.BackgroundColor = JsonToVec3(value["BackgroundColor"]);
|
||||
if (value.contains("TextColor")) style.TextColor = JsonToVec3(value["TextColor"]);
|
||||
if (value.contains("Opacity")) style.Opacity = value["Opacity"].get<float>();
|
||||
if (value.contains("BorderRadius")) style.BorderRadius = value["BorderRadius"].get<float>();
|
||||
if (value.contains("FontSize")) style.FontSize = value["FontSize"].get<float>();
|
||||
result.Styles.push_back(std::move(style));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (...) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
bool MetaCoreSceneSerializer::SaveUiFontFamilyToJson(
|
||||
const std::filesystem::path& absolutePath,
|
||||
const MetaCoreUiFontFamilyDocument& document
|
||||
) {
|
||||
try {
|
||||
json root{
|
||||
{"SchemaVersion", document.SchemaVersion},
|
||||
{"FamilyName", document.FamilyName},
|
||||
{"FallbackFamilies", document.FallbackFamilies}
|
||||
};
|
||||
root["Faces"] = json::array();
|
||||
for (const auto& face : document.Faces) {
|
||||
root["Faces"].push_back({
|
||||
{"FontAssetGuid", GuidToString(face.FontAssetGuid)},
|
||||
{"Weight", face.Weight},
|
||||
{"Italic", face.Italic},
|
||||
{"RequiredCharacters", face.RequiredCharacters}
|
||||
});
|
||||
}
|
||||
std::ofstream output(absolutePath);
|
||||
return output.is_open() && static_cast<bool>(output << root.dump(4));
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<MetaCoreUiFontFamilyDocument> MetaCoreSceneSerializer::LoadUiFontFamilyFromJson(
|
||||
const std::filesystem::path& absolutePath
|
||||
) {
|
||||
try {
|
||||
std::ifstream input(absolutePath);
|
||||
if (!input.is_open()) return std::nullopt;
|
||||
json root;
|
||||
input >> root;
|
||||
MetaCoreUiFontFamilyDocument result;
|
||||
if (root.contains("SchemaVersion")) result.SchemaVersion = root["SchemaVersion"].get<std::uint32_t>();
|
||||
if (root.contains("FamilyName")) result.FamilyName = root["FamilyName"].get<std::string>();
|
||||
if (root.contains("FallbackFamilies")) result.FallbackFamilies = root["FallbackFamilies"].get<std::vector<std::string>>();
|
||||
if (root.contains("Faces") && root["Faces"].is_array()) {
|
||||
for (const auto& value : root["Faces"]) {
|
||||
MetaCoreUiFontFaceDocument face;
|
||||
if (value.contains("FontAssetGuid")) face.FontAssetGuid = StringToGuid(value["FontAssetGuid"].get<std::string>());
|
||||
if (value.contains("Weight")) face.Weight = value["Weight"].get<std::int32_t>();
|
||||
if (value.contains("Italic")) face.Italic = value["Italic"].get<bool>();
|
||||
if (value.contains("RequiredCharacters")) face.RequiredCharacters = value["RequiredCharacters"].get<std::string>();
|
||||
result.Faces.push_back(std::move(face));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (...) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
bool MetaCoreSceneSerializer::SaveUiLocalizationTableToJson(
|
||||
const std::filesystem::path& absolutePath,
|
||||
const MetaCoreUiLocalizationTableDocument& document
|
||||
) {
|
||||
try {
|
||||
json root{
|
||||
{"SchemaVersion", document.SchemaVersion},
|
||||
{"DefaultLocale", document.DefaultLocale},
|
||||
{"FallbackLocale", document.FallbackLocale}
|
||||
};
|
||||
root["Locales"] = json::array();
|
||||
for (const auto& locale : document.Locales) {
|
||||
json localeJson{{"Locale", locale.Locale}};
|
||||
localeJson["Entries"] = json::array();
|
||||
for (const auto& entry : locale.Entries)
|
||||
localeJson["Entries"].push_back({{"Key", entry.Key}, {"Value", entry.Value}});
|
||||
root["Locales"].push_back(std::move(localeJson));
|
||||
}
|
||||
std::ofstream output(absolutePath);
|
||||
return output.is_open() && static_cast<bool>(output << root.dump(4));
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<MetaCoreUiLocalizationTableDocument> MetaCoreSceneSerializer::LoadUiLocalizationTableFromJson(
|
||||
const std::filesystem::path& absolutePath
|
||||
) {
|
||||
try {
|
||||
std::ifstream input(absolutePath);
|
||||
if (!input.is_open()) return std::nullopt;
|
||||
json root;
|
||||
input >> root;
|
||||
MetaCoreUiLocalizationTableDocument result;
|
||||
if (root.contains("SchemaVersion")) result.SchemaVersion = root["SchemaVersion"].get<std::uint32_t>();
|
||||
if (root.contains("DefaultLocale")) result.DefaultLocale = root["DefaultLocale"].get<std::string>();
|
||||
if (root.contains("FallbackLocale")) result.FallbackLocale = root["FallbackLocale"].get<std::string>();
|
||||
if (root.contains("Locales") && root["Locales"].is_array()) {
|
||||
for (const auto& value : root["Locales"]) {
|
||||
MetaCoreUiLocalizationLocaleDocument locale;
|
||||
if (value.contains("Locale")) locale.Locale = value["Locale"].get<std::string>();
|
||||
if (value.contains("Entries") && value["Entries"].is_array()) {
|
||||
for (const auto& entryValue : value["Entries"]) {
|
||||
MetaCoreUiLocalizationEntryDocument entry;
|
||||
if (entryValue.contains("Key")) entry.Key = entryValue["Key"].get<std::string>();
|
||||
if (entryValue.contains("Value")) entry.Value = entryValue["Value"].get<std::string>();
|
||||
locale.Entries.push_back(std::move(entry));
|
||||
}
|
||||
}
|
||||
result.Locales.push_back(std::move(locale));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (...) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace MetaCore
|
||||
|
||||
@ -134,6 +134,41 @@ enum class MetaCoreUiInputValueType {
|
||||
Number
|
||||
};
|
||||
|
||||
MC_ENUM()
|
||||
enum class MetaCoreUiVisualState {
|
||||
Normal = 0,
|
||||
Hover,
|
||||
Pressed,
|
||||
Focused,
|
||||
Disabled
|
||||
};
|
||||
|
||||
MC_ENUM()
|
||||
enum class MetaCoreUiAnimationTrigger {
|
||||
Show = 0,
|
||||
Hide,
|
||||
Hover,
|
||||
Pressed,
|
||||
Focus,
|
||||
Custom
|
||||
};
|
||||
|
||||
MC_ENUM()
|
||||
enum class MetaCoreUiAnimationProperty {
|
||||
Opacity = 0,
|
||||
Translation,
|
||||
Scale,
|
||||
Color
|
||||
};
|
||||
|
||||
MC_ENUM()
|
||||
enum class MetaCoreUiEasing {
|
||||
Linear = 0,
|
||||
EaseIn,
|
||||
EaseOut,
|
||||
EaseInOut
|
||||
};
|
||||
|
||||
MC_ENUM()
|
||||
enum class MetaCoreUiHorizontalAlignment {
|
||||
Left = 0,
|
||||
@ -243,6 +278,52 @@ struct MetaCoreUiBindingDocument {
|
||||
std::string Fallback{};
|
||||
};
|
||||
|
||||
MC_STRUCT()
|
||||
struct MetaCoreUiAnimationDocument {
|
||||
MC_GENERATED_BODY()
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string Id{};
|
||||
|
||||
MC_PROPERTY()
|
||||
MetaCoreUiAnimationTrigger Trigger = MetaCoreUiAnimationTrigger::Show;
|
||||
|
||||
MC_PROPERTY()
|
||||
MetaCoreUiAnimationProperty Property = MetaCoreUiAnimationProperty::Opacity;
|
||||
|
||||
MC_PROPERTY()
|
||||
glm::vec3 From{0.0F};
|
||||
|
||||
MC_PROPERTY()
|
||||
glm::vec3 To{1.0F};
|
||||
|
||||
MC_PROPERTY()
|
||||
float DurationSeconds = 0.2F;
|
||||
|
||||
MC_PROPERTY()
|
||||
float DelaySeconds = 0.0F;
|
||||
|
||||
MC_PROPERTY()
|
||||
MetaCoreUiEasing Easing = MetaCoreUiEasing::EaseOut;
|
||||
};
|
||||
|
||||
MC_STRUCT()
|
||||
struct MetaCoreUiListItemDocument {
|
||||
MC_GENERATED_BODY()
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string Id{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string Text{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string Value{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string LocalizationKey{};
|
||||
};
|
||||
|
||||
MC_STRUCT()
|
||||
struct MetaCoreUiNodeDocument {
|
||||
MC_GENERATED_BODY()
|
||||
@ -307,6 +388,9 @@ struct MetaCoreUiNodeDocument {
|
||||
MC_PROPERTY()
|
||||
std::int32_t SelectedIndex = -1;
|
||||
|
||||
MC_PROPERTY()
|
||||
std::vector<MetaCoreUiListItemDocument> StaticItems{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string ItemTemplateNodeId{};
|
||||
|
||||
@ -318,6 +402,122 @@ struct MetaCoreUiNodeDocument {
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string HideAnimation{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::vector<MetaCoreUiAnimationDocument> Animations{};
|
||||
};
|
||||
|
||||
MC_STRUCT()
|
||||
struct MetaCoreUiThemeStyleDocument {
|
||||
MC_GENERATED_BODY()
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string Token{};
|
||||
|
||||
MC_PROPERTY()
|
||||
MetaCoreUiVisualState State = MetaCoreUiVisualState::Normal;
|
||||
|
||||
MC_PROPERTY()
|
||||
glm::vec3 BackgroundColor{0.15F};
|
||||
|
||||
MC_PROPERTY()
|
||||
glm::vec3 TextColor{1.0F};
|
||||
|
||||
MC_PROPERTY()
|
||||
float Opacity = 1.0F;
|
||||
|
||||
MC_PROPERTY()
|
||||
float BorderRadius = 0.0F;
|
||||
|
||||
MC_PROPERTY()
|
||||
float FontSize = 16.0F;
|
||||
};
|
||||
|
||||
MC_STRUCT()
|
||||
struct MetaCoreUiThemeDocument {
|
||||
MC_GENERATED_BODY()
|
||||
|
||||
MC_PROPERTY()
|
||||
std::uint32_t SchemaVersion = 1;
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string Name{"Default"};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::vector<MetaCoreUiThemeStyleDocument> Styles{};
|
||||
};
|
||||
|
||||
MC_STRUCT()
|
||||
struct MetaCoreUiFontFaceDocument {
|
||||
MC_GENERATED_BODY()
|
||||
|
||||
MC_PROPERTY()
|
||||
MetaCoreAssetGuid FontAssetGuid{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::int32_t Weight = 400;
|
||||
|
||||
MC_PROPERTY()
|
||||
bool Italic = false;
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string RequiredCharacters{};
|
||||
};
|
||||
|
||||
MC_STRUCT()
|
||||
struct MetaCoreUiFontFamilyDocument {
|
||||
MC_GENERATED_BODY()
|
||||
|
||||
MC_PROPERTY()
|
||||
std::uint32_t SchemaVersion = 1;
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string FamilyName{"MetaCore UI"};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::vector<std::string> FallbackFamilies{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::vector<MetaCoreUiFontFaceDocument> Faces{};
|
||||
};
|
||||
|
||||
MC_STRUCT()
|
||||
struct MetaCoreUiLocalizationEntryDocument {
|
||||
MC_GENERATED_BODY()
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string Key{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string Value{};
|
||||
};
|
||||
|
||||
MC_STRUCT()
|
||||
struct MetaCoreUiLocalizationLocaleDocument {
|
||||
MC_GENERATED_BODY()
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string Locale{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::vector<MetaCoreUiLocalizationEntryDocument> Entries{};
|
||||
};
|
||||
|
||||
MC_STRUCT()
|
||||
struct MetaCoreUiLocalizationTableDocument {
|
||||
MC_GENERATED_BODY()
|
||||
|
||||
MC_PROPERTY()
|
||||
std::uint32_t SchemaVersion = 1;
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string DefaultLocale{"en-US"};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::string FallbackLocale{"en-US"};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::vector<MetaCoreUiLocalizationLocaleDocument> Locales{};
|
||||
};
|
||||
|
||||
MC_STRUCT()
|
||||
@ -357,6 +557,12 @@ struct MetaCoreUiDocument {
|
||||
MC_PROPERTY()
|
||||
bool UseSafeArea = true;
|
||||
|
||||
MC_PROPERTY()
|
||||
float MinAspectRatio = 0.0F;
|
||||
|
||||
MC_PROPERTY()
|
||||
float MaxAspectRatio = 0.0F;
|
||||
|
||||
MC_PROPERTY()
|
||||
MetaCoreAssetGuid ThemeAssetGuid{};
|
||||
|
||||
|
||||
@ -57,6 +57,30 @@ public:
|
||||
const std::filesystem::path& absolutePath,
|
||||
const MetaCoreTypeRegistry& registry
|
||||
);
|
||||
|
||||
static bool SaveUiThemeToJson(
|
||||
const std::filesystem::path& absolutePath,
|
||||
const MetaCoreUiThemeDocument& document
|
||||
);
|
||||
static std::optional<MetaCoreUiThemeDocument> LoadUiThemeFromJson(
|
||||
const std::filesystem::path& absolutePath
|
||||
);
|
||||
|
||||
static bool SaveUiFontFamilyToJson(
|
||||
const std::filesystem::path& absolutePath,
|
||||
const MetaCoreUiFontFamilyDocument& document
|
||||
);
|
||||
static std::optional<MetaCoreUiFontFamilyDocument> LoadUiFontFamilyFromJson(
|
||||
const std::filesystem::path& absolutePath
|
||||
);
|
||||
|
||||
static bool SaveUiLocalizationTableToJson(
|
||||
const std::filesystem::path& absolutePath,
|
||||
const MetaCoreUiLocalizationTableDocument& document
|
||||
);
|
||||
static std::optional<MetaCoreUiLocalizationTableDocument> LoadUiLocalizationTableFromJson(
|
||||
const std::filesystem::path& absolutePath
|
||||
);
|
||||
};
|
||||
|
||||
} // namespace MetaCore
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <charconv>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
@ -591,6 +592,78 @@ MetaCoreRuntimeInput* MetaCoreScriptRuntime::GetRuntimeInput() const { return Im
|
||||
MetaCorePhysicsWorld* MetaCoreScriptRuntime::GetPhysicsWorld() const { return Impl_->Physics; }
|
||||
MetaCoreAnimationRuntime* MetaCoreScriptRuntime::GetAnimationRuntime() const { return Impl_->Animation; }
|
||||
MetaCoreRuntimeUiSystem* MetaCoreScriptRuntime::GetRuntimeUi() const { return Impl_->RuntimeUi; }
|
||||
std::optional<MetaCoreRuntimeUiValue> MetaCoreScriptRuntime::ReadSceneUiBinding(std::string_view relativePath) const {
|
||||
const auto objectSeparator = relativePath.find('.');
|
||||
const auto componentSeparator = objectSeparator == std::string_view::npos
|
||||
? std::string_view::npos : relativePath.find('.', objectSeparator + 1);
|
||||
if (objectSeparator == std::string_view::npos || componentSeparator == std::string_view::npos) return std::nullopt;
|
||||
std::uint64_t objectId = 0;
|
||||
const auto idText = relativePath.substr(0, objectSeparator);
|
||||
const auto [end, error] = std::from_chars(idText.data(), idText.data() + idText.size(), objectId);
|
||||
if (error != std::errc{} || end != idText.data() + idText.size() || objectId == 0) return std::nullopt;
|
||||
MetaCoreScriptValueV1 source{};
|
||||
if (!GetProperty(
|
||||
{WorldGeneration_, objectId},
|
||||
relativePath.substr(objectSeparator + 1, componentSeparator - objectSeparator - 1),
|
||||
relativePath.substr(componentSeparator + 1), source)) return std::nullopt;
|
||||
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_String: value.Type = MetaCoreRuntimeUiValueType::String; value.StringValue = source.Text; break;
|
||||
case MetaCoreScriptAbiValue_Vec3:
|
||||
value.Type = MetaCoreRuntimeUiValueType::Vec3;
|
||||
value.Vec3Value = {static_cast<float>(source.Vec3[0]), static_cast<float>(source.Vec3[1]), static_cast<float>(source.Vec3[2])};
|
||||
break;
|
||||
case MetaCoreScriptAbiValue_AssetRef:
|
||||
value.Type = MetaCoreRuntimeUiValueType::AssetRef;
|
||||
value.AssetValue = MetaCoreAssetGuid::Parse(source.Text).value_or(MetaCoreAssetGuid{});
|
||||
break;
|
||||
case MetaCoreScriptAbiValue_GameObjectRef:
|
||||
value.Type = MetaCoreRuntimeUiValueType::GameObjectRef;
|
||||
value.ObjectValue = source.ObjectValue.ObjectId;
|
||||
break;
|
||||
default: return std::nullopt;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
bool MetaCoreScriptRuntime::WriteSceneUiBinding(std::string_view relativePath, const MetaCoreRuntimeUiValue& value) {
|
||||
const auto objectSeparator = relativePath.find('.');
|
||||
const auto componentSeparator = objectSeparator == std::string_view::npos
|
||||
? std::string_view::npos : relativePath.find('.', objectSeparator + 1);
|
||||
if (objectSeparator == std::string_view::npos || componentSeparator == std::string_view::npos) return false;
|
||||
std::uint64_t objectId = 0;
|
||||
const auto idText = relativePath.substr(0, objectSeparator);
|
||||
const auto [end, error] = std::from_chars(idText.data(), idText.data() + idText.size(), objectId);
|
||||
if (error != std::errc{} || end != idText.data() + idText.size() || objectId == 0) return false;
|
||||
MetaCoreScriptValueV1 target{};
|
||||
switch (value.Type) {
|
||||
case MetaCoreRuntimeUiValueType::Bool: target.Type = MetaCoreScriptAbiValue_Bool; target.IntValue = value.BoolValue ? 1 : 0; break;
|
||||
case MetaCoreRuntimeUiValueType::Int64: target.Type = MetaCoreScriptAbiValue_Int; target.IntValue = value.Int64Value; break;
|
||||
case MetaCoreRuntimeUiValueType::Double: target.Type = MetaCoreScriptAbiValue_Float; target.NumberValue = value.DoubleValue; break;
|
||||
case MetaCoreRuntimeUiValueType::String:
|
||||
target.Type = MetaCoreScriptAbiValue_String;
|
||||
std::snprintf(target.Text, sizeof(target.Text), "%s", value.StringValue.c_str());
|
||||
break;
|
||||
case MetaCoreRuntimeUiValueType::Vec3:
|
||||
target.Type = MetaCoreScriptAbiValue_Vec3;
|
||||
target.Vec3[0] = value.Vec3Value.x; target.Vec3[1] = value.Vec3Value.y; target.Vec3[2] = value.Vec3Value.z;
|
||||
break;
|
||||
case MetaCoreRuntimeUiValueType::AssetRef:
|
||||
target.Type = MetaCoreScriptAbiValue_AssetRef;
|
||||
std::snprintf(target.Text, sizeof(target.Text), "%s", value.AssetValue.ToString().c_str());
|
||||
break;
|
||||
case MetaCoreRuntimeUiValueType::GameObjectRef:
|
||||
target.Type = MetaCoreScriptAbiValue_GameObjectRef;
|
||||
target.ObjectValue = {WorldGeneration_, value.ObjectValue};
|
||||
break;
|
||||
}
|
||||
return SetProperty(
|
||||
{WorldGeneration_, objectId},
|
||||
relativePath.substr(objectSeparator + 1, componentSeparator - objectSeparator - 1),
|
||||
relativePath.substr(componentSeparator + 1), target);
|
||||
}
|
||||
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)); }
|
||||
@ -601,6 +674,9 @@ bool MetaCoreScriptRuntime::HasComponent(MetaCoreScriptObjectHandleV1 handle, st
|
||||
if (component == "Script") return object.HasComponent<MetaCoreScriptComponent>();
|
||||
if (component == "Camera") return object.HasComponent<MetaCoreCameraComponent>();
|
||||
if (component == "Light") return object.HasComponent<MetaCoreLightComponent>();
|
||||
if (component == "Active") return object.HasComponent<MetaCoreActiveComponent>();
|
||||
if (component == "Rotator") return object.HasComponent<MetaCoreRotatorComponent>();
|
||||
if (component == "UiRenderer") return object.HasComponent<MetaCoreUiRendererComponent>();
|
||||
if (component == "MeshRenderer") return object.HasComponent<MetaCoreMeshRendererComponent>();
|
||||
if (component == "Animator") return object.HasComponent<MetaCoreAnimatorComponent>();
|
||||
return false;
|
||||
@ -608,16 +684,100 @@ bool MetaCoreScriptRuntime::HasComponent(MetaCoreScriptObjectHandleV1 handle, st
|
||||
bool MetaCoreScriptRuntime::GetProperty(MetaCoreScriptObjectHandleV1 handle, std::string_view component, std::string_view property, MetaCoreScriptValueV1& value) const {
|
||||
if (!IsHandleValid(handle)) return false; auto object = Impl_->Scene.FindGameObject(handle.ObjectId); value = {};
|
||||
if (component == "Name" && property == "Value") { value.Type = MetaCoreScriptAbiValue_String; std::snprintf(value.Text, sizeof(value.Text), "%s", object.GetName().c_str()); return true; }
|
||||
if (component != "Transform") return false; const auto& transform = object.GetComponent<MetaCoreTransformComponent>(); const glm::vec3* source = nullptr;
|
||||
if (property == "Position") source = &transform.Position; else if (property == "Rotation") source = &transform.RotationEulerDegrees; else if (property == "Scale") source = &transform.Scale; else return false;
|
||||
value.Type = MetaCoreScriptAbiValue_Vec3; value.Vec3[0] = source->x; value.Vec3[1] = source->y; value.Vec3[2] = source->z; return true;
|
||||
const auto boolean = [&](bool source) { value.Type = MetaCoreScriptAbiValue_Bool; value.IntValue = source ? 1 : 0; return true; };
|
||||
const auto number = [&](double source) { value.Type = MetaCoreScriptAbiValue_Float; value.NumberValue = source; return true; };
|
||||
const auto vector = [&](glm::vec3 source) { value.Type = MetaCoreScriptAbiValue_Vec3; value.Vec3[0]=source.x; value.Vec3[1]=source.y; value.Vec3[2]=source.z; return true; };
|
||||
if (component == "Transform") {
|
||||
const auto& transform = object.GetComponent<MetaCoreTransformComponent>();
|
||||
if (property == "Position") return vector(transform.Position);
|
||||
if (property == "Rotation") return vector(transform.RotationEulerDegrees);
|
||||
if (property == "Scale") return vector(transform.Scale);
|
||||
return false;
|
||||
}
|
||||
if (component == "Active" && object.HasComponent<MetaCoreActiveComponent>() && property == "Active")
|
||||
return boolean(object.GetComponent<MetaCoreActiveComponent>().Active);
|
||||
if (component == "Rotator" && object.HasComponent<MetaCoreRotatorComponent>()) {
|
||||
const auto& rotator = object.GetComponent<MetaCoreRotatorComponent>();
|
||||
if (property == "Enabled") return boolean(rotator.Enabled);
|
||||
if (property == "DegreesPerSecond") return number(rotator.DegreesPerSecond);
|
||||
return false;
|
||||
}
|
||||
if (component == "Camera" && object.HasComponent<MetaCoreCameraComponent>()) {
|
||||
const auto& camera = object.GetComponent<MetaCoreCameraComponent>();
|
||||
if (property == "Enabled") return boolean(camera.Enabled);
|
||||
if (property == "IsPrimary") return boolean(camera.IsPrimary);
|
||||
if (property == "FieldOfViewDegrees") return number(camera.FieldOfViewDegrees);
|
||||
if (property == "NearClip") return number(camera.NearClip);
|
||||
if (property == "FarClip") return number(camera.FarClip);
|
||||
if (property == "OrthographicSize") return number(camera.OrthographicSize);
|
||||
if (property == "Depth") return number(camera.Depth);
|
||||
if (property == "BackgroundColor") return vector(camera.BackgroundColor);
|
||||
return false;
|
||||
}
|
||||
if (component == "Light" && object.HasComponent<MetaCoreLightComponent>()) {
|
||||
const auto& light = object.GetComponent<MetaCoreLightComponent>();
|
||||
if (property == "Enabled") return boolean(light.Enabled);
|
||||
if (property == "CastShadows") return boolean(light.CastShadows);
|
||||
if (property == "Intensity") return number(light.Intensity);
|
||||
if (property == "Range") return number(light.Range);
|
||||
if (property == "ColorTemperatureKelvin") return number(light.ColorTemperatureKelvin);
|
||||
if (property == "Color") return vector(light.Color);
|
||||
return false;
|
||||
}
|
||||
if (component == "UiRenderer" && object.HasComponent<MetaCoreUiRendererComponent>()) {
|
||||
const auto& ui = object.GetComponent<MetaCoreUiRendererComponent>();
|
||||
if (property == "Visible") return boolean(ui.Visible);
|
||||
if (property == "InputEnabled") return boolean(ui.InputEnabled);
|
||||
if (property == "PixelsPerUnit") return number(ui.PixelsPerUnit);
|
||||
if (property == "WorldSize") return vector(ui.WorldSize);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool MetaCoreScriptRuntime::SetProperty(MetaCoreScriptObjectHandleV1 handle, std::string_view component, std::string_view property, const MetaCoreScriptValueV1& value) {
|
||||
if (!IsHandleValid(handle)) return false; auto object = Impl_->Scene.FindGameObject(handle.ObjectId);
|
||||
if (component == "Name" && property == "Value" && value.Type == MetaCoreScriptAbiValue_String) { object.GetName() = value.Text; Impl_->Scene.IncrementRevision(); return true; }
|
||||
if (component != "Transform" || value.Type != MetaCoreScriptAbiValue_Vec3) return false; auto& transform = object.GetComponent<MetaCoreTransformComponent>(); glm::vec3* target = nullptr;
|
||||
if (property == "Position") target = &transform.Position; else if (property == "Rotation") target = &transform.RotationEulerDegrees; else if (property == "Scale") target = &transform.Scale; else return false;
|
||||
*target = {static_cast<float>(value.Vec3[0]), static_cast<float>(value.Vec3[1]), static_cast<float>(value.Vec3[2])}; Impl_->Scene.IncrementRevision(); return true;
|
||||
const auto changed = [&] { Impl_->Scene.IncrementRevision(); return true; };
|
||||
if (component == "Transform" && value.Type == MetaCoreScriptAbiValue_Vec3) {
|
||||
auto& transform = object.GetComponent<MetaCoreTransformComponent>(); glm::vec3* target = nullptr;
|
||||
if (property == "Position") target = &transform.Position; else if (property == "Rotation") target = &transform.RotationEulerDegrees; else if (property == "Scale") target = &transform.Scale; else return false;
|
||||
*target = {static_cast<float>(value.Vec3[0]), static_cast<float>(value.Vec3[1]), static_cast<float>(value.Vec3[2])}; return changed();
|
||||
}
|
||||
if (component == "Active" && object.HasComponent<MetaCoreActiveComponent>() &&
|
||||
property == "Active" && value.Type == MetaCoreScriptAbiValue_Bool) {
|
||||
object.GetComponent<MetaCoreActiveComponent>().Active = value.IntValue != 0; return changed();
|
||||
}
|
||||
if (component == "Rotator" && object.HasComponent<MetaCoreRotatorComponent>()) {
|
||||
auto& rotator = object.GetComponent<MetaCoreRotatorComponent>();
|
||||
if (property == "Enabled" && value.Type == MetaCoreScriptAbiValue_Bool) { rotator.Enabled=value.IntValue!=0; return changed(); }
|
||||
if (property == "DegreesPerSecond" && value.Type == MetaCoreScriptAbiValue_Float) { rotator.DegreesPerSecond=static_cast<float>(value.NumberValue); return changed(); }
|
||||
}
|
||||
if (component == "Camera" && object.HasComponent<MetaCoreCameraComponent>()) {
|
||||
auto& camera = object.GetComponent<MetaCoreCameraComponent>();
|
||||
if (value.Type == MetaCoreScriptAbiValue_Bool && property == "Enabled") { camera.Enabled=value.IntValue!=0; return changed(); }
|
||||
if (value.Type == MetaCoreScriptAbiValue_Bool && property == "IsPrimary") { camera.IsPrimary=value.IntValue!=0; return changed(); }
|
||||
if (value.Type == MetaCoreScriptAbiValue_Float && property == "FieldOfViewDegrees") { camera.FieldOfViewDegrees=static_cast<float>(value.NumberValue); return changed(); }
|
||||
if (value.Type == MetaCoreScriptAbiValue_Float && property == "NearClip") { camera.NearClip=static_cast<float>(value.NumberValue); return changed(); }
|
||||
if (value.Type == MetaCoreScriptAbiValue_Float && property == "FarClip") { camera.FarClip=static_cast<float>(value.NumberValue); return changed(); }
|
||||
if (value.Type == MetaCoreScriptAbiValue_Float && property == "OrthographicSize") { camera.OrthographicSize=static_cast<float>(value.NumberValue); return changed(); }
|
||||
if (value.Type == MetaCoreScriptAbiValue_Float && property == "Depth") { camera.Depth=static_cast<float>(value.NumberValue); return changed(); }
|
||||
if (value.Type == MetaCoreScriptAbiValue_Vec3 && property == "BackgroundColor") { camera.BackgroundColor={static_cast<float>(value.Vec3[0]),static_cast<float>(value.Vec3[1]),static_cast<float>(value.Vec3[2])}; return changed(); }
|
||||
}
|
||||
if (component == "Light" && object.HasComponent<MetaCoreLightComponent>()) {
|
||||
auto& light = object.GetComponent<MetaCoreLightComponent>();
|
||||
if (value.Type == MetaCoreScriptAbiValue_Bool && property == "Enabled") { light.Enabled=value.IntValue!=0; return changed(); }
|
||||
if (value.Type == MetaCoreScriptAbiValue_Bool && property == "CastShadows") { light.CastShadows=value.IntValue!=0; return changed(); }
|
||||
if (value.Type == MetaCoreScriptAbiValue_Float && property == "Intensity") { light.Intensity=static_cast<float>(value.NumberValue); return changed(); }
|
||||
if (value.Type == MetaCoreScriptAbiValue_Float && property == "Range") { light.Range=static_cast<float>(value.NumberValue); return changed(); }
|
||||
if (value.Type == MetaCoreScriptAbiValue_Vec3 && property == "Color") { light.Color={static_cast<float>(value.Vec3[0]),static_cast<float>(value.Vec3[1]),static_cast<float>(value.Vec3[2])}; return changed(); }
|
||||
}
|
||||
if (component == "UiRenderer" && object.HasComponent<MetaCoreUiRendererComponent>()) {
|
||||
auto& ui = object.GetComponent<MetaCoreUiRendererComponent>();
|
||||
if (value.Type == MetaCoreScriptAbiValue_Bool && property == "Visible") { ui.Visible=value.IntValue!=0; return changed(); }
|
||||
if (value.Type == MetaCoreScriptAbiValue_Bool && property == "InputEnabled") { ui.InputEnabled=value.IntValue!=0; return changed(); }
|
||||
if (value.Type == MetaCoreScriptAbiValue_Float && property == "PixelsPerUnit" && value.NumberValue > 0.0) { ui.PixelsPerUnit=static_cast<float>(value.NumberValue); return changed(); }
|
||||
if (value.Type == MetaCoreScriptAbiValue_Vec3 && property == "WorldSize" && value.Vec3[0] > 0.0 && value.Vec3[1] > 0.0) { ui.WorldSize={static_cast<float>(value.Vec3[0]),static_cast<float>(value.Vec3[1]),static_cast<float>(value.Vec3[2])}; return changed(); }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool MetaCoreScriptRuntime::GetField(MetaCoreScriptObjectHandleV1 handle, std::uint64_t instance, std::string_view field, MetaCoreScriptValueV1& value) const {
|
||||
if (!IsHandleValid(handle)) return false;
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
#include "MetaCoreScripting/MetaCoreScriptAbi.h"
|
||||
#include "MetaCoreScene/MetaCoreScene.h"
|
||||
#include "MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
@ -13,7 +14,6 @@
|
||||
|
||||
namespace MetaCore {
|
||||
class MetaCoreAnimationRuntime;
|
||||
class MetaCoreRuntimeUiSystem;
|
||||
|
||||
enum class MetaCoreScriptFieldType {
|
||||
Bool = 0,
|
||||
@ -143,6 +143,8 @@ public:
|
||||
[[nodiscard]] MetaCorePhysicsWorld* GetPhysicsWorld() const;
|
||||
[[nodiscard]] MetaCoreAnimationRuntime* GetAnimationRuntime() const;
|
||||
[[nodiscard]] MetaCoreRuntimeUiSystem* GetRuntimeUi() const;
|
||||
[[nodiscard]] std::optional<MetaCoreRuntimeUiValue> ReadSceneUiBinding(std::string_view relativePath) const;
|
||||
[[nodiscard]] bool WriteSceneUiBinding(std::string_view relativePath, const MetaCoreRuntimeUiValue& value);
|
||||
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,27 +334,28 @@ R8.1–R8.5 已落地的工程边界:
|
||||
|
||||
### 9. UI 系统
|
||||
|
||||
当前状态:**核心资产、编译、运行时、Cook 和脚本接口已迁移到 v2,但图形签收门禁尚未满足,问题 9 保持打开。**
|
||||
当前状态:**计划内工程实现已完成;当前执行环境没有图形会话,Linux x86_64 图形参考机签收尚未执行,因此问题 9 保持打开。**
|
||||
|
||||
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** 通过。
|
||||
- 新增独立校验与确定性编译器,拒绝重复 ID、损坏层级、循环/不可达节点、非法绑定命名空间、错误双向目标和不兼容版本,并生成版本化 RML/RCSS 与 GUID 依赖集合。Editor 导出、Cook 和 Runtime 使用同一编译入口;旧直接 RML/清单编辑面板已隐藏。一次性旧 RML 导入器会确定性重建受支持控件层级和稳定 ID,并对不支持标签、重复 ID、图片路径及需迁移到 Theme Token 的 RCSS 输出诊断。
|
||||
- RectTransform 已覆盖 AnchorMin/AnchorMax、Pivot、Offset/Size、最小/最大尺寸和裁剪;Canvas 的三种缩放模式、参考分辨率、宽高匹配、DPI 回退/上下限、安全区与宽高比约束均由 Editor/Player 共用的逐实例布局函数求值。
|
||||
- Runtime UI 清单升级为 v2 UI Asset GUID 实例清单,包含 Instance/Scene Object、RenderMode、Layer、`PixelsPerUnit`、World Size 和目标纹理分辨率;旧 `DocumentId + RmlPath` 继续只读兼容。Cook 从启动场景 UI 组件生成 v2 清单与 `Ui/Compiled/<guid>` 产物,并收集、验证和复制 UI 依赖。
|
||||
- `UiTheme`、`UiFontFamily`、`UiLocalizationTable` 已具有版本化 JSON 格式和确定性编译路径;主题 Token 与控件状态样式、本地化显式 fallback、字体 fallback/所需字符预热、确定性图片图集和 Show/Hide/Hover/Pressed/Focus/Custom 基础动效均进入正式 RML/RCSS 产物。
|
||||
- 运行时提供类型化 Bool/Int64/Double/String/Vec3/AssetRef/GameObjectRef 值、列表行复用、增量路径刷新、严格转换、fallback、实例创建销毁、焦点、Locale、Theme 和诊断接口。`runtime.*`、`script.*`、`scene.*` Provider 已统一注册;场景 Provider 使用稳定对象 ID,覆盖 Name、Transform、Camera、Light、Active、Rotator 和 UiRenderer 的严格类型读写。
|
||||
- 输入顺序固定为高层 Screen Space、World Space 命中 UI、3D 场景;仅在命中、焦点、滚动或捕获时消费。事件涵盖 Pointer Enter/Leave、Click、Submit、Value/Selection Changed、Scroll、Focus/Blur 和不可写 Provider 的 WriteRequest;显式 NavigationOrder、文档顺序、方向键与焦点失效转移使用确定性实现。
|
||||
- World Space 每个实例使用独立离屏 RmlUi Context 和持久 Filament Render Target,再以场景平面参与深度遮挡;尺寸由 `PixelsPerUnit` 和 World Size 共同确定,物理射线命中点转换为稳定 UV 后路由 Move/Down/Up/Scroll/Cancel,且被 UI 消费后不再触发 3D 交互。
|
||||
- 脚本 ABI Minor 升至 5,新增 `METACORE_SCRIPT_CAP_UI`,提供文档显示、节点值、列表、焦点、Locale 和 Theme 接口;UI 事件同时发布到全局 `MetaCore.UI` 和清单指定的场景对象,旧插件未声明能力时继续加载。
|
||||
- UI Designer 已补齐新控件、Canvas、Anchor/Pivot、导航、输入/列表/滚动、动效、主题/字体/本地化和类型化绑定 Inspector,并支持拖放重挂、复制删除、排序、资产级 Undo/Redo 和未保存切换保护。正式一致性判断由 Game View 与 Player 的共用编译/运行/渲染链路承担,不再使用旧 ImGui 近似绘制。
|
||||
- 新增独立 `MetaCoreRuntimeUiTests`,覆盖七类节点、文档校验/迁移、确定性资源编译、Anchor/DPI/安全区/宽高比、World Size/UV、类型转换、静态与动态列表、本地化 fallback、主题/字体/图集/动效及 JSON/二进制往返;Runtime Input 和 Scripting Tests 覆盖 World Space 输入消费与 `scene.*` 严格读写。
|
||||
- 2,000 节点、500 动态绑定和 100 行列表编译基准已加入回归测试,本轮独立复测 CPU p95 为 **11.93–29.61 ms**,低于当前 2,000 ms 防退化上限;Linux Development 的 Editor、Player 和相关测试目标构建成功,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 像素及交互签收尚未执行。
|
||||
- 当前环境在 `glfwInit` 处没有可用图形会话,无法生成可信的 Screen/World Space GPU 黄金图;仍需在固定 Linux x86_64 图形参考机执行 4:3、16:9、21:9,DPI 1.0/1.5/2.0 及中英文 Locale 矩阵,并证明 Editor Play Mode 与 Player 的 SSIM ≥ 0.995。
|
||||
- CPU 编译压力测试已经自动化;运行时 2,000 节点、500 动态绑定、100 行滚动列表的 CPU/GPU p95、显存和常驻内存基线仍需在同一图形参考机签收,并建立不超过签收基线 120% 且整体帧时间不超过 16.67 ms 的持续门禁。
|
||||
- 鼠标、滚轮、纯键盘、文本输入、重叠文档、World Space 遮挡/射线命中和“UI 消费后不触发 3D”已有代码路径与非 GPU 自动化覆盖;最终 Development 发布包的不同窗口尺寸、DPI 和真实显示/输入设备交互仍须在参考机人工签收。
|
||||
|
||||
### 10. Scene 与 Prefab
|
||||
|
||||
|
||||
@ -107,6 +107,28 @@ int main() {
|
||||
pointerInput.BeginFrame(); interaction.Update(scene, pickBackend, pointerInput, viewport, true);
|
||||
Expect(pickCallbacks.empty(), "UI-consumed mouse should not submit picking");
|
||||
|
||||
interaction.Cancel();
|
||||
pointerEvents.clear();
|
||||
pickCallbacks.clear();
|
||||
auto worldUiObject = scene.CreateGameObject("World UI");
|
||||
MetaCore::MetaCoreUiRendererComponent worldUi;
|
||||
worldUi.RenderMode = MetaCore::MetaCoreUiRenderMode::WorldSpace;
|
||||
worldUi.WorldSize = {2.0F, 1.0F, 0.0F};
|
||||
worldUiObject.AddComponent<MetaCore::MetaCoreUiRendererComponent>(worldUi);
|
||||
pointerInput.BeginFrame();
|
||||
pointerInput.SetCursorPosition({50.0F, 50.0F});
|
||||
interaction.Update(scene, pickBackend, pointerInput, viewport, false);
|
||||
auto worldHover = std::move(pickCallbacks.back());
|
||||
pickCallbacks.clear();
|
||||
worldHover({worldUiObject.GetId(), {0.5F, 0.25F, 0.0F}});
|
||||
pointerInput.BeginFrame();
|
||||
interaction.Update(scene, pickBackend, pointerInput, viewport, false);
|
||||
Expect(!pointerEvents.empty() && pointerEvents.back().WorldSpaceUi,
|
||||
"World Space UI should participate in the pointer priority pipeline");
|
||||
Expect(std::abs(pointerEvents.back().SurfaceUv.x - 0.75F) < 0.001F &&
|
||||
std::abs(pointerEvents.back().SurfaceUv.y - 0.25F) < 0.001F,
|
||||
"World Space UI ray hit should map to deterministic surface UV");
|
||||
|
||||
std::filesystem::remove(mapPath); std::filesystem::remove(overridePath);
|
||||
std::cout << "MetaCoreRuntimeInputTests passed\n";
|
||||
return 0;
|
||||
|
||||
@ -3,9 +3,13 @@
|
||||
#include "MetaCoreScene/MetaCoreSceneSerializer.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
@ -58,6 +62,22 @@ MetaCore::MetaCoreUiDocument BuildDocument() {
|
||||
if (types[index] == MetaCoreUiNodeType::List) {
|
||||
node.Bindings.push_back({"script.rows", MetaCoreUiBindingTarget::Items,
|
||||
MetaCoreUiBindingMode::OneWay, {}, {}, {}});
|
||||
node.StaticItems = {{"first", "First", "1", {}}, {"second", "Second", "2", {}}};
|
||||
node.SelectedIndex = 1;
|
||||
}
|
||||
if (types[index] == MetaCoreUiNodeType::Button) {
|
||||
node.Style.ThemeToken = "primary";
|
||||
node.LocalizationKey = "button.ok";
|
||||
node.Animations.push_back({
|
||||
"hover-opacity",
|
||||
MetaCoreUiAnimationTrigger::Hover,
|
||||
MetaCoreUiAnimationProperty::Opacity,
|
||||
{0.7F, 0.0F, 0.0F},
|
||||
{1.0F, 0.0F, 0.0F},
|
||||
0.15F,
|
||||
0.0F,
|
||||
MetaCoreUiEasing::EaseOut
|
||||
});
|
||||
}
|
||||
root.Children.push_back(node.Id);
|
||||
document.Nodes.push_back(std::move(node));
|
||||
@ -71,6 +91,33 @@ MetaCore::MetaCoreUiDocument BuildDocument() {
|
||||
|
||||
int main() {
|
||||
using namespace MetaCore;
|
||||
const MetaCoreLegacyUiImportResult legacyImport = MetaCoreImportLegacyUiDocument(
|
||||
R"(<rml><head><link type="text/rcss" href="legacy.rcss"/></head><body>
|
||||
<div id="menu"><button id="play">Play & Go</button><input id="name" placeholder="Name"/>
|
||||
<img id="logo" src="logo.png"/><custom id="ignored"/></div></body></rml>)",
|
||||
"#menu { width: 400px; }",
|
||||
"LegacyMenu"
|
||||
);
|
||||
Expect(legacyImport.Succeeded, "legacy RML structural import failed");
|
||||
Expect(legacyImport.Document.Nodes.size() == 5, "legacy importer produced unexpected node count");
|
||||
const auto importedButton = std::find_if(
|
||||
legacyImport.Document.Nodes.begin(),
|
||||
legacyImport.Document.Nodes.end(),
|
||||
[](const MetaCoreUiNodeDocument& node) { return node.Id == "play"; }
|
||||
);
|
||||
Expect(importedButton != legacyImport.Document.Nodes.end() &&
|
||||
importedButton->Type == MetaCoreUiNodeType::Button &&
|
||||
importedButton->Text == "Play & Go", "legacy button semantics were not imported");
|
||||
Expect(std::any_of(
|
||||
legacyImport.Diagnostics.begin(),
|
||||
legacyImport.Diagnostics.end(),
|
||||
[](const MetaCoreUiDiagnostic& item) {
|
||||
return !item.IsError && item.Message.find("UiTheme") != std::string::npos;
|
||||
}
|
||||
), "legacy RCSS migration diagnostic was not emitted");
|
||||
const MetaCoreLegacyUiImportResult brokenLegacy = MetaCoreImportLegacyUiDocument("<rml><body><div", {});
|
||||
Expect(!brokenLegacy.Succeeded, "malformed legacy RML import was accepted");
|
||||
|
||||
const MetaCoreUiDocument document = BuildDocument();
|
||||
Expect(MetaCoreValidateUiDocument(document).empty(), "valid document was rejected");
|
||||
const MetaCoreCompiledUiDocument first = MetaCoreCompileUiDocument(document);
|
||||
@ -79,10 +126,114 @@ int main() {
|
||||
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("<option id=\"Node4-item-first\"") != std::string::npos,
|
||||
"static List items were 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");
|
||||
Expect(first.Rcss.find("@keyframes mcui-Node2-hover-opacity") != std::string::npos, "basic animation was not compiled");
|
||||
|
||||
MetaCoreUiCompileResources resources;
|
||||
MetaCoreUiThemeDocument theme;
|
||||
theme.Name = "Dark";
|
||||
MetaCoreUiThemeStyleDocument primaryStyle;
|
||||
primaryStyle.Token = "primary";
|
||||
primaryStyle.BackgroundColor = {0.1F, 0.2F, 0.3F};
|
||||
primaryStyle.BorderRadius = 5.0F;
|
||||
primaryStyle.FontSize = 18.0F;
|
||||
theme.Styles.push_back(primaryStyle);
|
||||
resources.Theme = theme;
|
||||
MetaCoreUiLocalizationTableDocument localization;
|
||||
localization.DefaultLocale = "en-US";
|
||||
localization.FallbackLocale = "en-US";
|
||||
localization.Locales = {
|
||||
{"en-US", {{"button.ok", "OK"}}},
|
||||
{"zh-CN", {{"button.ok", "确定"}}}
|
||||
};
|
||||
resources.Localization = localization;
|
||||
MetaCoreUiFontFamilyDocument compileFont;
|
||||
compileFont.FamilyName = "UI";
|
||||
compileFont.FallbackFamilies = {"sans-serif"};
|
||||
compileFont.Faces.push_back({MetaCoreAssetGuid::Generate(), 400, false, "中文ABC"});
|
||||
resources.FontFamily = compileFont;
|
||||
const MetaCoreCompiledUiDocument localized = MetaCoreCompileUiDocument(document, resources);
|
||||
Expect(localized.Succeeded, "resource-aware compilation failed");
|
||||
Expect(localized.Rml.find("data-metacore-theme=\"Dark\"") != std::string::npos, "theme name was not compiled");
|
||||
Expect(localized.Rcss.find("theme-primary") != std::string::npos, "theme token state was not compiled");
|
||||
Expect(localized.Rml.find(">OK</button>") != std::string::npos, "default locale text was not compiled");
|
||||
Expect(localized.LocalizationJson.find("确定") != std::string::npos, "localization sidecar was not compiled");
|
||||
Expect(localized.Rml.find("mcui-font-prewarm") != std::string::npos &&
|
||||
localized.Rml.find("中文ABC") != std::string::npos, "required font characters were not prewarmed for glyph atlas Cook");
|
||||
Expect(MetaCoreResolveUiLocalizedText(localization, "button.ok", "zh-CN").value_or("") == "确定",
|
||||
"locale lookup failed");
|
||||
MetaCoreUiDocument atlasDocument = document;
|
||||
const MetaCoreAssetGuid atlasImage = MetaCoreAssetGuid::Generate();
|
||||
atlasDocument.Nodes[2].Style.ImageAssetGuid = atlasImage;
|
||||
resources.AtlasFilename = "atlas.tga";
|
||||
resources.AtlasWidth = 64;
|
||||
resources.AtlasHeight = 64;
|
||||
resources.AtlasEntries.push_back({atlasImage, 1, 1, 32, 32});
|
||||
const MetaCoreCompiledUiDocument atlased = MetaCoreCompileUiDocument(atlasDocument, resources);
|
||||
Expect(atlased.Succeeded && atlased.Rcss.find("@spritesheet metacore-ui-atlas") != std::string::npos,
|
||||
"image atlas spritesheet was not compiled");
|
||||
Expect(atlased.Rcss.find("decorator:image(" + atlasImage.ToString()) != std::string::npos,
|
||||
"atlased image did not reference its deterministic sprite");
|
||||
Expect(atlased.Rml.find("Assets/" + atlasImage.ToString() + ".png") == std::string::npos,
|
||||
"atlased image should not load a loose texture");
|
||||
|
||||
MetaCoreUiCanvasEnvironment environment{2560.0F, 1440.0F, 144.0F, 100.0F, 20.0F, 100.0F, 20.0F};
|
||||
MetaCoreUiCanvasLayout canvas = MetaCoreComputeUiCanvasLayout(document, environment);
|
||||
Expect(canvas.Scale > 1.0F && canvas.ViewportLeft == 100.0F, "safe-area screen scaling failed");
|
||||
MetaCoreUiDocument physical = document;
|
||||
physical.ScaleMode = MetaCoreUiCanvasScaleMode::ConstantPhysicalSize;
|
||||
canvas = MetaCoreComputeUiCanvasLayout(physical, environment);
|
||||
Expect(std::abs(canvas.Scale - 1.5F) < 0.001F, "physical DPI scaling failed");
|
||||
const auto worldSurface = MetaCoreComputeUiWorldSurfaceLayout({1.92F, 1.08F, 0.0F}, 1000.0F);
|
||||
Expect(worldSurface.TargetWidth == 1920 && worldSurface.TargetHeight == 1080,
|
||||
"World Space PixelsPerUnit sizing failed");
|
||||
const auto centerUv = MetaCoreMapWorldUiHitToUv(glm::mat4(1.0F), {2.0F, 1.0F, 0.0F}, {0.0F, 0.0F, 0.0F});
|
||||
Expect(centerUv.has_value() && std::abs(centerUv->x - 0.5F) < 0.001F &&
|
||||
std::abs(centerUv->y - 0.5F) < 0.001F, "World Space ray hit UV mapping failed");
|
||||
Expect(!MetaCoreMapWorldUiHitToUv(glm::mat4(1.0F), {2.0F, 1.0F, 0.0F}, {2.0F, 0.0F, 0.0F}).has_value(),
|
||||
"World Space hit outside the UI plane should be rejected");
|
||||
|
||||
MetaCoreUiDocument benchmark;
|
||||
benchmark.Name = "UiSteadyStateBenchmark";
|
||||
benchmark.ReferenceWidth = 1920;
|
||||
benchmark.ReferenceHeight = 1080;
|
||||
MetaCoreUiNodeDocument benchmarkRoot;
|
||||
benchmarkRoot.Id = "root";
|
||||
benchmarkRoot.Type = MetaCoreUiNodeType::Panel;
|
||||
benchmark.RootNodeIds.push_back(benchmarkRoot.Id);
|
||||
benchmark.Nodes.push_back(benchmarkRoot);
|
||||
for (int index = 1; index < 2000; ++index) {
|
||||
MetaCoreUiNodeDocument node;
|
||||
node.Id = "node-" + std::to_string(index);
|
||||
node.ParentId = "root";
|
||||
node.Type = index == 1 ? MetaCoreUiNodeType::List : MetaCoreUiNodeType::Text;
|
||||
node.Text = "Row " + std::to_string(index);
|
||||
if (index <= 500) node.Bindings.push_back({"runtime.perf." + std::to_string(index),
|
||||
MetaCoreUiBindingTarget::Text, MetaCoreUiBindingMode::OneWay, {}, {}, "0"});
|
||||
benchmark.Nodes.front().Children.push_back(node.Id);
|
||||
benchmark.Nodes.push_back(std::move(node));
|
||||
}
|
||||
for (int row = 0; row < 100; ++row)
|
||||
benchmark.Nodes[1].StaticItems.push_back({"row-" + std::to_string(row),
|
||||
"Row " + std::to_string(row), std::to_string(row), {}});
|
||||
std::vector<double> compileSamples;
|
||||
for (int sample = 0; sample < 5; ++sample) {
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
const auto result = MetaCoreCompileUiDocument(benchmark);
|
||||
const auto finish = std::chrono::steady_clock::now();
|
||||
Expect(result.Succeeded, "2,000-node benchmark document failed to compile");
|
||||
compileSamples.push_back(std::chrono::duration<double, std::milli>(finish - start).count());
|
||||
}
|
||||
std::sort(compileSamples.begin(), compileSamples.end());
|
||||
const double compileP95 = compileSamples.back();
|
||||
Expect(compileP95 < 2000.0, "2,000-node deterministic compiler p95 exceeded the regression guard");
|
||||
std::cout << "MetaCoreRuntimeUi benchmark compile_p95_ms=" << compileP95
|
||||
<< " nodes=2000 bindings=500 list_rows=100\n";
|
||||
|
||||
MetaCoreUiDocument invalid = document;
|
||||
invalid.Nodes.back().Id = invalid.Nodes.front().Id;
|
||||
@ -91,6 +242,10 @@ int main() {
|
||||
invalid.Nodes[1].Bindings.push_back({"unknown.path", MetaCoreUiBindingTarget::Text,
|
||||
MetaCoreUiBindingMode::OneWay, {}, {}, {}});
|
||||
Expect(!MetaCoreValidateUiDocument(invalid).empty(), "invalid binding namespace was accepted");
|
||||
invalid = document;
|
||||
invalid.Nodes[1].Bindings.push_back({"runtime.value", MetaCoreUiBindingTarget::Text,
|
||||
MetaCoreUiBindingMode::OneWay, {}, "arbitrary_expression()", {}});
|
||||
Expect(!MetaCoreValidateUiDocument(invalid).empty(), "arbitrary binding converter was accepted");
|
||||
|
||||
MetaCoreUiDocument legacy = document;
|
||||
legacy.SchemaVersion = 1;
|
||||
@ -110,8 +265,27 @@ int main() {
|
||||
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");
|
||||
Expect(loaded->Nodes[5].StaticItems.size() == 2, "List static items did not round-trip");
|
||||
std::filesystem::remove(jsonPath);
|
||||
|
||||
const std::filesystem::path themePath = std::filesystem::temp_directory_path() / "MetaCoreUiTheme.mcuitheme.json";
|
||||
const std::filesystem::path fontPath = std::filesystem::temp_directory_path() / "MetaCoreUiFont.mcuifont.json";
|
||||
const std::filesystem::path localizationPath = std::filesystem::temp_directory_path() / "MetaCoreUiLoc.mculoc.json";
|
||||
MetaCoreUiFontFamilyDocument font;
|
||||
font.FamilyName = "UI";
|
||||
font.FallbackFamilies = {"sans-serif"};
|
||||
font.Faces.push_back({MetaCoreAssetGuid::Generate(), 400, false, "中文ABC"});
|
||||
Expect(MetaCoreSceneSerializer::SaveUiThemeToJson(themePath, theme), "theme JSON write failed");
|
||||
Expect(MetaCoreSceneSerializer::SaveUiFontFamilyToJson(fontPath, font), "font family JSON write failed");
|
||||
Expect(MetaCoreSceneSerializer::SaveUiLocalizationTableToJson(localizationPath, localization), "localization JSON write failed");
|
||||
Expect(MetaCoreSceneSerializer::LoadUiThemeFromJson(themePath)->Styles.size() == 1, "theme JSON round-trip failed");
|
||||
Expect(MetaCoreSceneSerializer::LoadUiFontFamilyFromJson(fontPath)->Faces.size() == 1, "font JSON round-trip failed");
|
||||
Expect(MetaCoreSceneSerializer::LoadUiLocalizationTableFromJson(localizationPath)->Locales.size() == 2,
|
||||
"localization JSON round-trip failed");
|
||||
std::filesystem::remove(themePath);
|
||||
std::filesystem::remove(fontPath);
|
||||
std::filesystem::remove(localizationPath);
|
||||
|
||||
std::cout << "MetaCoreRuntimeUiTests passed\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -88,6 +88,24 @@ void TestRuntimeLifecycleFaultsHandlesEventsAndTimers() {
|
||||
Expect(counters->Start == 1 && counters->Fixed == 1 && counters->Update == 1 && counters->Late == 1, "runtime should dispatch lifecycle in all phases");
|
||||
Expect(runtime.GetFaultedInstanceCount() == 1 && !logs.empty(), "one throwing behaviour should be isolated and diagnosed");
|
||||
|
||||
auto scenePosition = runtime.ReadSceneUiBinding(std::to_string(objectId) + ".Transform.Position");
|
||||
Expect(scenePosition.has_value() && scenePosition->Type == MetaCore::MetaCoreRuntimeUiValueType::Vec3,
|
||||
"scene UI provider should read reflected transform values through a stable object handle");
|
||||
MetaCore::MetaCoreRuntimeUiValue changedPosition;
|
||||
changedPosition.Type = MetaCore::MetaCoreRuntimeUiValueType::Vec3;
|
||||
changedPosition.Vec3Value = {4.0F, 5.0F, 6.0F};
|
||||
Expect(runtime.WriteSceneUiBinding(std::to_string(objectId) + ".Transform.Position", changedPosition),
|
||||
"scene UI provider should write compatible reflected transform values");
|
||||
Expect(object.GetComponent<MetaCore::MetaCoreTransformComponent>().Position == changedPosition.Vec3Value,
|
||||
"scene UI provider write should update the scene component");
|
||||
Expect(!runtime.ReadSceneUiBinding("invalid.Transform.Position").has_value(),
|
||||
"scene UI provider should reject malformed handles");
|
||||
MetaCore::MetaCoreRuntimeUiValue incompatible;
|
||||
incompatible.Type = MetaCore::MetaCoreRuntimeUiValueType::String;
|
||||
incompatible.StringValue = "invalid";
|
||||
Expect(!runtime.WriteSceneUiBinding(std::to_string(objectId) + ".Transform.Position", incompatible),
|
||||
"scene UI provider should reject incompatible typed writes");
|
||||
|
||||
std::vector<std::string> eventOrder;
|
||||
runtime.Subscribe(objectId, 11, "first", [&](std::string_view, const auto&) { eventOrder.push_back("first"); runtime.Publish("second"); });
|
||||
runtime.Subscribe(objectId, 11, "second", [&](std::string_view, const auto&) { eventOrder.push_back("second"); });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user