MetaCore/Source/MetaCoreRuntimeUi/Private/MetaCoreRuntimeUiSystem.cpp
2026-07-27 15:01:03 +08:00

1616 lines
81 KiB
C++

#include "MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h"
#include "MetaCorePlatform/MetaCoreInput.h"
#include "MetaCorePlatform/MetaCoreWindow.h"
#include "MetaCoreRender/MetaCoreEditorViewportRenderer.h"
#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>
#include <filament/IndexBuffer.h>
#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>
#include <filament/TextureSampler.h>
#include <filament/TransformManager.h>
#include <filament/VertexBuffer.h>
#include <filament/View.h>
#include <filament/Viewport.h>
#include <utils/EntityManager.h>
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <limits>
#include <sstream>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
namespace MetaCore {
namespace {
[[nodiscard]] bool IsPathInsideRoot(const std::filesystem::path& root, const std::filesystem::path& candidate) {
std::error_code error;
const std::filesystem::path canonicalRoot = std::filesystem::weakly_canonical(root, error);
if (error) return false;
const std::filesystem::path canonicalCandidate = std::filesystem::weakly_canonical(candidate, error);
if (error) return false;
const std::filesystem::path relative = canonicalCandidate.lexically_relative(canonicalRoot);
return !relative.empty() && !relative.is_absolute() && *relative.begin() != "..";
}
[[nodiscard]] std::string EscapeRmlText(std::string_view value) {
std::string result;
result.reserve(value.size());
for (char character : value) {
switch (character) {
case '&': result += "&amp;"; break;
case '<': result += "&lt;"; break;
case '>': result += "&gt;"; break;
case '"': result += "&quot;"; break;
default: result += character; break;
}
}
return result;
}
[[nodiscard]] std::string FormatUiValue(const MetaCoreRuntimeUiValue& value, const std::string& format) {
std::ostringstream stream;
if (!format.empty()) stream << std::fixed << std::setprecision(std::max(0, std::atoi(format.c_str())));
switch (value.Type) {
case MetaCoreRuntimeUiValueType::Bool: return value.BoolValue ? "true" : "false";
case MetaCoreRuntimeUiValueType::Int64: stream << value.Int64Value; break;
case MetaCoreRuntimeUiValueType::Double: stream << value.DoubleValue; break;
case MetaCoreRuntimeUiValueType::String: return value.StringValue;
case MetaCoreRuntimeUiValueType::Vec3: stream << value.Vec3Value.x << ", " << value.Vec3Value.y << ", " << value.Vec3Value.z; break;
case MetaCoreRuntimeUiValueType::AssetRef: return value.AssetValue.ToString();
case MetaCoreRuntimeUiValueType::GameObjectRef: stream << value.ObjectValue; break;
}
return stream.str();
}
[[nodiscard]] 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);
result.BoolValue = value.BoolValue;
result.Int64Value = value.Int64Value;
result.DoubleValue = value.DoubleValue;
result.StringValue = value.StringValue;
result.Vec3Value = value.Vec3Value;
return result;
}
[[nodiscard]] MetaCoreRuntimeUiEventType EventTypeFromRml(std::string_view type) {
if (type == "mouseover") return MetaCoreRuntimeUiEventType::PointerEnter;
if (type == "mouseout") return MetaCoreRuntimeUiEventType::PointerLeave;
if (type == "submit") return MetaCoreRuntimeUiEventType::Submit;
if (type == "change") return MetaCoreRuntimeUiEventType::ValueChanged;
if (type == "scroll") return MetaCoreRuntimeUiEventType::Scroll;
if (type == "focus") return MetaCoreRuntimeUiEventType::Focus;
if (type == "blur") return MetaCoreRuntimeUiEventType::Blur;
return MetaCoreRuntimeUiEventType::Click;
}
[[nodiscard]] Rml::Input::KeyIdentifier MetaCoreMapGlfwKeyToRml(int key) {
if (key >= GLFW_KEY_0 && key <= GLFW_KEY_9) {
return static_cast<Rml::Input::KeyIdentifier>(Rml::Input::KI_0 + (key - GLFW_KEY_0));
}
if (key >= GLFW_KEY_A && key <= GLFW_KEY_Z) {
return static_cast<Rml::Input::KeyIdentifier>(Rml::Input::KI_A + (key - GLFW_KEY_A));
}
switch (key) {
case GLFW_KEY_SPACE: return Rml::Input::KI_SPACE;
case GLFW_KEY_BACKSPACE: return Rml::Input::KI_BACK;
case GLFW_KEY_TAB: return Rml::Input::KI_TAB;
case GLFW_KEY_ENTER: return Rml::Input::KI_RETURN;
case GLFW_KEY_ESCAPE: return Rml::Input::KI_ESCAPE;
case GLFW_KEY_LEFT: return Rml::Input::KI_LEFT;
case GLFW_KEY_UP: return Rml::Input::KI_UP;
case GLFW_KEY_RIGHT: return Rml::Input::KI_RIGHT;
case GLFW_KEY_DOWN: return Rml::Input::KI_DOWN;
case GLFW_KEY_HOME: return Rml::Input::KI_HOME;
case GLFW_KEY_END: return Rml::Input::KI_END;
case GLFW_KEY_PAGE_UP: return Rml::Input::KI_PRIOR;
case GLFW_KEY_PAGE_DOWN: return Rml::Input::KI_NEXT;
case GLFW_KEY_INSERT: return Rml::Input::KI_INSERT;
case GLFW_KEY_DELETE: return Rml::Input::KI_DELETE;
case GLFW_KEY_LEFT_SHIFT: return Rml::Input::KI_LSHIFT;
case GLFW_KEY_RIGHT_SHIFT: return Rml::Input::KI_RSHIFT;
case GLFW_KEY_LEFT_CONTROL: return Rml::Input::KI_LCONTROL;
case GLFW_KEY_RIGHT_CONTROL: return Rml::Input::KI_RCONTROL;
case GLFW_KEY_LEFT_ALT: return Rml::Input::KI_LMENU;
case GLFW_KEY_RIGHT_ALT: return Rml::Input::KI_RMENU;
default: return Rml::Input::KI_UNKNOWN;
}
}
[[nodiscard]] int MetaCoreMapGlfwModifiersToRml(int modifiers) {
int result = 0;
if ((modifiers & GLFW_MOD_CONTROL) != 0) result |= Rml::Input::KM_CTRL;
if ((modifiers & GLFW_MOD_SHIFT) != 0) result |= Rml::Input::KM_SHIFT;
if ((modifiers & GLFW_MOD_ALT) != 0) result |= Rml::Input::KM_ALT;
if ((modifiers & GLFW_MOD_SUPER) != 0) result |= Rml::Input::KM_META;
if ((modifiers & GLFW_MOD_CAPS_LOCK) != 0) result |= Rml::Input::KM_CAPSLOCK;
if ((modifiers & GLFW_MOD_NUM_LOCK) != 0) result |= Rml::Input::KM_NUMLOCK;
return result;
}
[[nodiscard]] std::vector<char> MetaCoreLoadRuntimeUiMaterialPackage() {
for (const std::filesystem::path& candidate : {
std::filesystem::path("rml_ui.filamat"),
std::filesystem::path("Engine/Materials/rml_ui.filamat"),
std::filesystem::path("../rml_ui.filamat"),
std::filesystem::path("uiBlit.filamat"),
std::filesystem::path("Engine/Materials/uiBlit.filamat"),
std::filesystem::path("../uiBlit.filamat")
}) {
std::ifstream input(candidate, std::ios::binary);
if (!input.is_open()) {
continue;
}
std::vector<char> package((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
if (!package.empty()) {
return package;
}
}
return {};
}
class MetaCoreRmlSystemInterface final : public Rml::SystemInterface {
public:
explicit MetaCoreRmlSystemInterface(MetaCoreWindow& window) : Window_(window) {}
void SetClipboardText(const Rml::String& text) override { Window_.SetClipboardText(text); }
void GetClipboardText(Rml::String& text) override { text = Window_.GetClipboardText(); }
private:
MetaCoreWindow& Window_;
};
class MetaCoreRmlFilamentRenderInterface final : public Rml::RenderInterface {
public:
struct DrawCommand {
Rml::CompiledGeometryHandle Geometry = 0;
Rml::Vector2f Translation{};
Rml::TextureHandle Texture = 0;
bool ScissorEnabled = false;
Rml::Rectanglei Scissor{};
};
Rml::CompiledGeometryHandle CompileGeometry(Rml::Span<const Rml::Vertex> vertices, Rml::Span<const int> indices) override {
if (vertices.empty() || indices.empty() || vertices.size() > std::numeric_limits<std::uint32_t>::max()) {
return 0;
}
Geometry geometry;
geometry.Vertices.reserve(vertices.size());
for (const Rml::Vertex& vertex : vertices) {
geometry.Vertices.push_back({
vertex.position.x,
vertex.position.y,
vertex.tex_coord.x,
vertex.tex_coord.y,
{vertex.colour.red, vertex.colour.green, vertex.colour.blue, vertex.colour.alpha}
});
}
geometry.Indices.reserve(indices.size());
for (const int index : indices) {
if (index < 0 || static_cast<std::size_t>(index) >= vertices.size()) return 0;
geometry.Indices.push_back(static_cast<std::uint32_t>(index));
}
const Rml::CompiledGeometryHandle handle = NextGeometryHandle_++;
Geometries_.emplace(handle, std::move(geometry));
return handle;
}
void RenderGeometry(Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation, Rml::TextureHandle texture) override {
if (!Geometries_.contains(geometry)) return;
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); }
Rml::TextureHandle LoadTexture(Rml::Vector2i& dimensions, const Rml::String& source) override {
std::filesystem::path path(source);
if (path.is_relative()) path = UiRoot_ / path;
if (!IsPathInsideRoot(UiRoot_, path)) return 0;
int width = 0;
int height = 0;
int channels = 0;
stbi_uc* pixels = stbi_load(path.string().c_str(), &width, &height, &channels, STBI_rgb_alpha);
if (pixels == nullptr || width <= 0 || height <= 0) {
stbi_image_free(pixels);
return 0;
}
Texture texture;
texture.Width = width;
texture.Height = height;
texture.Pixels.assign(pixels, pixels + static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4U);
stbi_image_free(pixels);
dimensions = {width, height};
return StoreTexture(std::move(texture));
}
Rml::TextureHandle GenerateTexture(Rml::Span<const Rml::byte> source, Rml::Vector2i dimensions) override {
if (dimensions.x <= 0 || dimensions.y <= 0) return 0;
const std::size_t byteCount = static_cast<std::size_t>(dimensions.x) * static_cast<std::size_t>(dimensions.y) * 4U;
if (source.size() != byteCount) return 0;
Texture texture;
texture.Width = dimensions.x;
texture.Height = dimensions.y;
texture.Pixels.assign(source.begin(), source.end());
return StoreTexture(std::move(texture));
}
void ReleaseTexture(Rml::TextureHandle texture) override {
const auto found = Textures_.find(texture);
if (found == Textures_.end()) return;
if (Engine_ != nullptr && found->second.FilamentTexture != nullptr) {
Engine_->destroy(found->second.FilamentTexture);
}
Textures_.erase(found);
}
void EnableScissorRegion(bool enable) override { ScissorEnabled_ = enable; }
void SetScissorRegion(Rml::Rectanglei region) override { Scissor_ = region; }
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,
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);
std::vector<DrawCommand> commands;
commands.reserve(DrawCommands_.size());
for (const DrawCommand& command : DrawCommands_) {
if (Geometries_.contains(command.Geometry)) commands.push_back(command);
}
if (commands.empty()) return;
FrameRenderable_ = utils::EntityManager::get().create();
filament::RenderableManager::Builder builder(static_cast<uint32_t>(commands.size()));
builder.boundingBox({{0.0f, 0.0f, -1.0f}, {static_cast<float>(width), static_cast<float>(height), 1.0f}})
.culling(false)
.castShadows(false)
.receiveShadows(false);
for (std::size_t primitiveIndex = 0; primitiveIndex < commands.size(); ++primitiveIndex) {
const DrawCommand& command = commands[primitiveIndex];
const Geometry& geometry = Geometries_.at(command.Geometry);
if (!CreateFrameGeometry(engine, geometry, command.Translation)) continue;
filament::MaterialInstance* materialInstance = Material_->createInstance();
FrameMaterialInstances_.push_back(materialInstance);
const filament::Texture* texture = ResolveTexture(engine, command.Texture);
if (texture != nullptr) {
materialInstance->setParameter("albedo", texture, filament::TextureSampler(
filament::TextureSampler::MinFilter::LINEAR,
filament::TextureSampler::MagFilter::LINEAR
));
}
if (command.ScissorEnabled) {
const uint32_t left = static_cast<uint32_t>(std::max(0, command.Scissor.Left()));
const uint32_t bottom = static_cast<uint32_t>(std::max(0, static_cast<int>(height) - command.Scissor.Bottom()));
const uint32_t scissorWidth = static_cast<uint32_t>(std::max(0, command.Scissor.Width()));
const uint32_t scissorHeight = static_cast<uint32_t>(std::max(0, command.Scissor.Height()));
materialInstance->setScissor(left, bottom, scissorWidth, scissorHeight);
} else {
materialInstance->unsetScissor();
}
builder.geometry(static_cast<uint8_t>(primitiveIndex), filament::RenderableManager::PrimitiveType::TRIANGLES,
FrameVertexBuffers_.back(), FrameIndexBuffers_.back(), 0, static_cast<uint32_t>(geometry.Indices.size()))
.material(static_cast<uint8_t>(primitiveIndex), materialInstance)
.blendOrder(static_cast<uint16_t>(primitiveIndex), static_cast<uint16_t>(primitiveIndex));
}
if (FrameVertexBuffers_.empty()) {
utils::EntityManager::get().destroy(FrameRenderable_);
FrameRenderable_ = {};
return;
}
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);
}
Textures_.clear();
if (WhiteTexture_ != nullptr) Engine_->destroy(WhiteTexture_);
if (Material_ != nullptr) Engine_->destroy(Material_);
if (View_ != nullptr) Engine_->destroy(View_);
if (Camera_ != nullptr) {
const utils::Entity cameraEntity = Camera_->getEntity();
Engine_->destroyCameraComponent(cameraEntity);
utils::EntityManager::get().destroy(cameraEntity);
}
if (Scene_ != nullptr) Engine_->destroy(Scene_);
Material_ = nullptr;
WhiteTexture_ = nullptr;
View_ = nullptr;
Camera_ = nullptr;
Scene_ = nullptr;
Engine_ = nullptr;
}
void SetUiRoot(std::filesystem::path root) { UiRoot_ = std::move(root); }
private:
struct GpuVertex {
float Position[2];
float Uv[2];
std::array<std::uint8_t, 4> Color{};
};
struct Geometry {
std::vector<GpuVertex> Vertices;
std::vector<std::uint32_t> Indices;
};
struct Texture {
int Width = 0;
int Height = 0;
std::vector<std::uint8_t> Pixels;
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));
return handle;
}
void EnsureFilamentResources(filament::Engine& engine) {
if (Engine_ != nullptr && Engine_ != &engine) Shutdown();
if (Engine_ != nullptr) return;
Engine_ = &engine;
Scene_ = Engine_->createScene();
View_ = Engine_->createView();
View_->setScene(Scene_);
View_->setPostProcessingEnabled(false);
View_->setBlendMode(filament::View::BlendMode::TRANSLUCENT);
View_->setShadowingEnabled(false);
const utils::Entity cameraEntity = utils::EntityManager::get().create();
Camera_ = Engine_->createCamera(cameraEntity);
View_->setCamera(Camera_);
const std::vector<char> package = MetaCoreLoadRuntimeUiMaterialPackage();
if (!package.empty()) Material_ = filament::Material::Builder().package(package.data(), package.size()).build(*Engine_);
CreateWhiteTexture();
}
[[nodiscard]] const filament::Texture* ResolveTexture(filament::Engine& engine, Rml::TextureHandle handle) {
const auto found = Textures_.find(handle);
if (found == Textures_.end()) return WhiteTexture_;
Texture& texture = found->second;
if (texture.FilamentTexture != nullptr) return texture.FilamentTexture;
if (texture.Width <= 0 || texture.Height <= 0 || texture.Pixels.empty()) return WhiteTexture_;
void* pixelBytes = std::malloc(texture.Pixels.size());
if (pixelBytes == nullptr) return nullptr;
std::memcpy(pixelBytes, texture.Pixels.data(), texture.Pixels.size());
filament::Texture::PixelBufferDescriptor descriptor(pixelBytes, texture.Pixels.size(),
filament::Texture::Format::RGBA, filament::Texture::Type::UBYTE,
[](void* buffer, std::size_t, void*) { std::free(buffer); }, nullptr);
texture.FilamentTexture = filament::Texture::Builder()
.width(static_cast<uint32_t>(texture.Width))
.height(static_cast<uint32_t>(texture.Height))
.levels(1)
.format(filament::Texture::InternalFormat::RGBA8)
.sampler(filament::Texture::Sampler::SAMPLER_2D)
.build(engine);
texture.FilamentTexture->setImage(engine, 0, std::move(descriptor));
return texture.FilamentTexture;
}
void CreateWhiteTexture() {
if (Engine_ == nullptr || WhiteTexture_ != nullptr) return;
std::array<std::uint8_t, 4> pixels{255, 255, 255, 255};
void* pixelBytes = std::malloc(pixels.size());
if (pixelBytes == nullptr) return;
std::memcpy(pixelBytes, pixels.data(), pixels.size());
filament::Texture::PixelBufferDescriptor descriptor(pixelBytes, pixels.size(),
filament::Texture::Format::RGBA, filament::Texture::Type::UBYTE,
[](void* buffer, std::size_t, void*) { std::free(buffer); }, nullptr);
WhiteTexture_ = filament::Texture::Builder()
.width(1)
.height(1)
.levels(1)
.format(filament::Texture::InternalFormat::RGBA8)
.sampler(filament::Texture::Sampler::SAMPLER_2D)
.build(*Engine_);
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;
for (GpuVertex& vertex : translatedVertices) {
vertex.Position[0] += translation.x;
vertex.Position[1] += translation.y;
}
auto* vertexBuffer = filament::VertexBuffer::Builder()
.vertexCount(static_cast<uint32_t>(translatedVertices.size()))
.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);
auto* indexBuffer = filament::IndexBuffer::Builder()
.indexCount(static_cast<uint32_t>(geometry.Indices.size()))
.bufferType(filament::IndexBuffer::IndexType::UINT)
.build(engine);
const std::size_t vertexBytes = translatedVertices.size() * sizeof(GpuVertex);
void* copiedVertices = std::malloc(vertexBytes);
const std::size_t indexBytes = geometry.Indices.size() * sizeof(std::uint32_t);
void* copiedIndices = std::malloc(indexBytes);
if (copiedVertices == nullptr || copiedIndices == nullptr) {
std::free(copiedVertices);
std::free(copiedIndices);
engine.destroy(vertexBuffer);
engine.destroy(indexBuffer);
return false;
}
std::memcpy(copiedVertices, translatedVertices.data(), vertexBytes);
std::memcpy(copiedIndices, geometry.Indices.data(), indexBytes);
vertexBuffer->setBufferAt(engine, 0, filament::VertexBuffer::BufferDescriptor(copiedVertices, vertexBytes,
[](void* buffer, std::size_t, void*) { std::free(buffer); }, nullptr));
indexBuffer->setBuffer(engine, filament::IndexBuffer::BufferDescriptor(copiedIndices, indexBytes,
[](void* buffer, std::size_t, void*) { std::free(buffer); }, nullptr));
FrameVertexBuffers_.push_back(vertexBuffer);
FrameIndexBuffers_.push_back(indexBuffer);
return true;
}
void ClearFrameResources() {
if (Engine_ == nullptr) return;
if (FrameRenderable_) {
Engine_->getRenderableManager().destroy(FrameRenderable_);
if (Scene_ != nullptr) Scene_->remove(FrameRenderable_);
utils::EntityManager::get().destroy(FrameRenderable_);
FrameRenderable_ = {};
}
for (filament::MaterialInstance* instance : FrameMaterialInstances_) Engine_->destroy(instance);
for (filament::VertexBuffer* buffer : FrameVertexBuffers_) Engine_->destroy(buffer);
for (filament::IndexBuffer* buffer : FrameIndexBuffers_) Engine_->destroy(buffer);
FrameMaterialInstances_.clear();
FrameVertexBuffers_.clear();
FrameIndexBuffers_.clear();
}
std::filesystem::path UiRoot_;
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;
Rml::Rectanglei Scissor_{};
filament::Engine* Engine_ = nullptr;
filament::Scene* Scene_ = nullptr;
filament::View* View_ = nullptr;
filament::Camera* Camera_ = nullptr;
filament::Material* Material_ = nullptr;
filament::Texture* WhiteTexture_ = nullptr;
utils::Entity FrameRenderable_{};
std::vector<filament::MaterialInstance*> FrameMaterialInstances_;
std::vector<filament::VertexBuffer*> FrameVertexBuffers_;
std::vector<filament::IndexBuffer*> FrameIndexBuffers_;
};
class MetaCoreRmlFileInterface final : public Rml::FileInterface {
public:
explicit MetaCoreRmlFileInterface(std::filesystem::path root) : Root_(std::move(root)) {}
Rml::FileHandle Open(const Rml::String& path) override {
std::filesystem::path candidate(path);
if (candidate.is_relative()) candidate = Root_ / candidate;
if (!IsPathInsideRoot(Root_, candidate)) return 0;
auto* file = new std::ifstream(candidate, std::ios::binary);
if (!file->is_open()) { delete file; return 0; }
return reinterpret_cast<Rml::FileHandle>(file);
}
void Close(Rml::FileHandle file) override { delete reinterpret_cast<std::ifstream*>(file); }
size_t Read(void* buffer, size_t size, Rml::FileHandle file) override {
auto& input = *reinterpret_cast<std::ifstream*>(file);
input.read(static_cast<char*>(buffer), static_cast<std::streamsize>(size));
return static_cast<size_t>(input.gcount());
}
bool Seek(Rml::FileHandle file, long offset, int origin) override {
auto& input = *reinterpret_cast<std::ifstream*>(file);
input.seekg(offset, static_cast<std::ios_base::seekdir>(origin));
return input.good();
}
size_t Tell(Rml::FileHandle file) override {
return static_cast<size_t>(reinterpret_cast<std::ifstream*>(file)->tellg());
}
private:
std::filesystem::path Root_;
};
} // namespace
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)) {}
void ProcessEvent(Rml::Event& event) override {
Rml::Element* target = event.GetTargetElement();
if (target == nullptr) return;
const std::string action = target->GetAttribute<Rml::String>("data-metacore-action", "");
MetaCoreRuntimeUiEvent uiEvent;
const auto state = std::find_if(Owner_.Documents_.begin(), Owner_.Documents_.end(), [&](const DocumentState& value) {
return value.Entry.DocumentId == DocumentId_;
});
if (state != Owner_.Documents_.end()) {
uiEvent.InstanceId = state->Entry.InstanceId;
uiEvent.DocumentAssetGuid = state->Entry.UiDocumentAssetGuid;
uiEvent.SceneObjectId = state->Entry.SceneObjectId;
}
uiEvent.DocumentId = DocumentId_;
uiEvent.ElementId = target->GetId();
uiEvent.Action = action;
uiEvent.Type = event.GetType();
uiEvent.Value = target->GetAttribute<Rml::String>("value", "");
uiEvent.EventType = EventTypeFromRml(uiEvent.Type);
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 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;
const auto previous = Owner_.Values_.find(writePath);
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 &&
uiEvent.EventType != MetaCoreRuntimeUiEventType::Blur &&
uiEvent.EventType != MetaCoreRuntimeUiEventType::PointerEnter &&
uiEvent.EventType != MetaCoreRuntimeUiEventType::PointerLeave &&
uiEvent.EventType != MetaCoreRuntimeUiEventType::Scroll) return;
Owner_.Events_.push_back(uiEvent);
if (Owner_.Callback_) Owner_.Callback_(uiEvent);
}
private:
Impl& Owner_;
std::string DocumentId_;
};
MetaCoreWindow* Window_ = nullptr;
MetaCoreEditorViewportRenderer* ViewportRenderer_ = nullptr;
std::filesystem::path UiRoot_;
MetaCoreRmlFilamentRenderInterface RenderInterface_;
std::unique_ptr<MetaCoreRmlFileInterface> FileInterface_;
std::unique_ptr<MetaCoreRmlSystemInterface> SystemInterface_;
Rml::Context* Context_ = nullptr;
std::vector<DocumentState> Documents_;
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_;
EventCallback Callback_;
MetaCoreRuntimeUiInputSource CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Programmatic;
std::string Locale_{"en-US"};
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"}) {
Rml::ElementList elements;
state.Document->QuerySelectorAll(elements, "[data-metacore-bind-" + target + "]");
for (Rml::Element* element : elements) {
const std::string path = element->GetAttribute<Rml::String>("data-metacore-bind-" + target, "");
if (onlyPath != nullptr && path != *onlyPath) continue;
const auto value = Values_.find(path);
if (value == Values_.end()) {
const std::string fallback = element->GetAttribute<Rml::String>("data-metacore-fallback-" + target, "");
if (fallback.empty()) continue;
if (target == "text") element->SetInnerRML(EscapeRmlText(fallback));
else element->SetAttribute(target == "enabled" ? "disabled" : target, fallback);
continue;
}
const std::string 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(*converted, format);
if (target == "text") element->SetInnerRML(EscapeRmlText(text));
else if (target == "value") element->SetAttribute("value", text);
else if (target == "visible") {
if (converted->BoolValue) element->RemoveAttribute("hidden"); else element->SetAttribute("hidden", "true");
} else if (target == "enabled") {
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);
}
}
Rml::ElementList legacyElements;
state.Document->QuerySelectorAll(legacyElements, "[data-metacore-bind]");
for (Rml::Element* element : legacyElements) {
const std::string path = element->GetAttribute<Rml::String>("data-metacore-bind", "");
if (onlyPath != nullptr && path != *onlyPath) continue;
const auto value = Values_.find(path);
if (value == Values_.end()) continue;
element->SetInnerRML(EscapeRmlText(FormatUiValue(value->second,
element->GetAttribute<Rml::String>("data-metacore-format", ""))));
}
}
if (focusInvalidated) (void)NavigateFocus(1);
}
bool LoadEntry(const MetaCoreRuntimeUiDocumentEntry& entry) {
if (Context_ == nullptr) return false;
std::filesystem::path relativePath = entry.RmlPath;
if (relativePath.empty() && entry.UiDocumentAssetGuid.IsValid())
relativePath = std::filesystem::path("Compiled") / entry.UiDocumentAssetGuid.ToString() / "document.rml";
const std::filesystem::path absolutePath = UiRoot_ / relativePath;
if (entry.DocumentId.empty() || relativePath.empty() || !IsPathInsideRoot(UiRoot_, absolutePath)) {
Errors_.push_back("Invalid UI manifest entry: " + entry.DocumentId);
return false;
}
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(std::move(state));
ApplyLocalization(Documents_.back());
return true;
}
};
MetaCoreRuntimeUiSystem::MetaCoreRuntimeUiSystem() : Impl_(std::make_unique<Impl>()) {}
MetaCoreRuntimeUiSystem::~MetaCoreRuntimeUiSystem() { Shutdown(); }
bool MetaCoreRuntimeUiSystem::Initialize(MetaCoreWindow& window, const std::filesystem::path&, const std::filesystem::path& uiRoot, const MetaCoreRuntimeUiManifest& manifest) {
Shutdown();
if (uiRoot.empty() || !std::filesystem::is_directory(uiRoot)) return false;
if (manifest.SchemaVersion > METACORE_UI_SCHEMA_VERSION ||
manifest.GeneratorVersion > METACORE_UI_GENERATOR_VERSION) {
Impl_->Errors_.push_back("Runtime UI manifest version is newer than this engine");
return false;
}
Impl_->Window_ = &window;
Impl_->UiRoot_ = std::filesystem::weakly_canonical(uiRoot);
Impl_->RenderInterface_.SetUiRoot(Impl_->UiRoot_);
Impl_->FileInterface_ = std::make_unique<MetaCoreRmlFileInterface>(Impl_->UiRoot_);
Impl_->SystemInterface_ = std::make_unique<MetaCoreRmlSystemInterface>(window);
Rml::SetRenderInterface(&Impl_->RenderInterface_);
Rml::SetFileInterface(Impl_->FileInterface_.get());
Rml::SetSystemInterface(Impl_->SystemInterface_.get());
if (!Rml::Initialise()) return false;
const auto [width, height] = window.GetFramebufferSize();
Impl_->Context_ = Rml::CreateContext("MetaCoreRuntime", {width, height});
if (Impl_->Context_ == nullptr) {
Rml::Shutdown();
return false;
}
std::vector<MetaCoreRuntimeUiDocumentEntry> entries = manifest.Documents;
std::sort(entries.begin(), entries.end(), [](const auto& lhs, const auto& rhs) { return lhs.Layer < rhs.Layer; });
std::uint64_t generatedInstanceId = 1;
for (MetaCoreRuntimeUiDocumentEntry& entry : entries) {
if (!entry.UiDocumentAssetGuid.IsValid() && !entry.UiDocumentAssetGuidString.empty())
if (const auto parsed = MetaCoreAssetGuid::Parse(entry.UiDocumentAssetGuidString); parsed.has_value())
entry.UiDocumentAssetGuid = *parsed;
if (entry.InstanceId == 0) entry.InstanceId = generatedInstanceId++;
(void)Impl_->LoadEntry(entry);
}
Impl_->Initialized_ = true;
return true;
}
void MetaCoreRuntimeUiSystem::AttachToViewportRenderer(MetaCoreEditorViewportRenderer& viewportRenderer) {
Impl_->ViewportRenderer_ = &viewportRenderer;
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();
Impl_->RenderInterface_.Shutdown();
Impl_->Documents_.clear();
Impl_->Listeners_.clear();
Impl_->Context_ = nullptr;
Impl_->FileInterface_.reset();
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();
}
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;
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 (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 (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 ? 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 = !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) {
const std::string path = update.DataPointId.starts_with("runtime.") ? update.DataPointId : "runtime." + update.DataPointId;
Impl_->Values_[path] = ToUiValue(update.Value);
Impl_->Values_[update.DataPointId] = ToUiValue(update.Value);
}
Impl_->ApplyBindings();
}
bool MetaCoreRuntimeUiSystem::SetValue(const std::string& sourcePath, const MetaCoreRuntimeUiValue& value) {
if (!Impl_->Initialized_ || sourcePath.empty()) return false;
Impl_->Values_[sourcePath] = value;
Impl_->ApplyBindings(&sourcePath);
return true;
}
std::optional<MetaCoreRuntimeUiValue> MetaCoreRuntimeUiSystem::GetValue(const std::string& sourcePath) const {
const auto found = Impl_->Values_.find(sourcePath);
return found == Impl_->Values_.end() ? std::nullopt : std::optional<MetaCoreRuntimeUiValue>(found->second);
}
bool MetaCoreRuntimeUiSystem::SetListRows(const std::string& sourcePath, std::vector<MetaCoreRuntimeUiRow> rows) {
if (!Impl_->Initialized_ || sourcePath.empty()) return false;
Impl_->Lists_[sourcePath] = std::move(rows);
for (auto& state : Impl_->Documents_) {
if (state.Document == nullptr) continue;
Rml::ElementList elements;
state.Document->QuerySelectorAll(elements, "[data-metacore-bind-items]");
for (Rml::Element* element : elements) {
if (element->GetAttribute<Rml::String>("data-metacore-bind-items", "") != sourcePath) continue;
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]) {
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());
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;
}
}
}
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;
if (Rml::Element* element = state.Document->GetElementById(elementId); element != nullptr) {
element->Focus();
return true;
}
}
return false;
}
bool MetaCoreRuntimeUiSystem::CreateInstance(const MetaCoreRuntimeUiDocumentEntry& entry) {
if (!Impl_->Initialized_ || entry.InstanceId == 0 ||
std::any_of(Impl_->Documents_.begin(), Impl_->Documents_.end(), [&](const auto& value) {
return value.Entry.InstanceId == entry.InstanceId;
})) return false;
return Impl_->LoadEntry(entry);
}
bool MetaCoreRuntimeUiSystem::DestroyInstance(std::uint64_t instanceId) {
const auto found = std::find_if(Impl_->Documents_.begin(), Impl_->Documents_.end(), [&](const auto& value) {
return value.Entry.InstanceId == instanceId;
});
if (found == Impl_->Documents_.end()) return false;
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;
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;
}
void MetaCoreRuntimeUiSystem::SetLocale(std::string locale) {
if (locale.empty()) return;
Impl_->Locale_ = std::move(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) {
Impl_->Theme_ = std::move(theme);
for (auto& state : Impl_->Documents_) if (state.Document != nullptr) state.Document->SetAttribute("data-metacore-theme", Impl_->Theme_);
}
const std::string& MetaCoreRuntimeUiSystem::GetLocale() const { return Impl_->Locale_; }
const std::string& MetaCoreRuntimeUiSystem::GetTheme() const { return Impl_->Theme_; }
std::vector<MetaCoreRuntimeUiEvent> MetaCoreRuntimeUiSystem::ConsumeEvents() { return std::exchange(Impl_->Events_, {}); }
void MetaCoreRuntimeUiSystem::SetEventCallback(EventCallback callback) { Impl_->Callback_ = std::move(callback); }
bool MetaCoreRuntimeUiSystem::IsInitialized() const { return Impl_->Initialized_; }
std::size_t MetaCoreRuntimeUiSystem::GetLoadedDocumentCount() const { return Impl_->Documents_.size(); }
const std::vector<std::string>& MetaCoreRuntimeUiSystem::GetErrors() const { return Impl_->Errors_; }
std::vector<MetaCoreUiDiagnostic> MetaCoreRuntimeUiSystem::GetDiagnostics() const { return Impl_->Diagnostics_; }
} // namespace MetaCore