MetaCore/Source/MetaCoreRuntimeUi/Private/MetaCoreRuntimeUiSystem.cpp
2026-07-27 10:36:59 +08:00

933 lines
44 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 <GLFW/glfw3.h>
#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/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 <cmath>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <limits>
#include <sstream>
#include <unordered_map>
#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]] 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;
DrawCommands_.push_back({geometry, translation, texture, ScissorEnabled_, Scissor_});
}
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(); }
void Render(filament::Engine& engine, filament::Renderer& renderer, uint32_t width, uint32_t height) {
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});
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_);
}
void Shutdown() {
if (Engine_ == nullptr) return;
ClearFrameResources();
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;
};
[[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));
}
[[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_;
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 DocumentState {
MetaCoreRuntimeUiDocumentEntry Entry{};
Rml::ElementDocument* Document = nullptr;
};
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);
uiEvent.InputSource = Owner_.CurrentInputSource_;
uiEvent.Consumed = !event.IsPropagating();
const std::string writePath = target->GetAttribute<Rml::String>("data-metacore-bind-value", "");
const std::string writeMode = target->GetAttribute<Rml::String>("data-metacore-bind-mode-value", "");
if (uiEvent.EventType == MetaCoreRuntimeUiEventType::ValueChanged &&
writeMode == "two-way" && !writePath.empty()) {
uiEvent.Action = writePath;
if (!writePath.starts_with("script.") && !writePath.starts_with("scene.")) {
uiEvent.EventType = MetaCoreRuntimeUiEventType::WriteRequest;
}
const auto previous = Owner_.Values_.find(writePath);
if (previous != Owner_.Values_.end()) uiEvent.OldValue = FormatUiValue(previous->second, {});
MetaCoreRuntimeUiValue value;
value.Type = MetaCoreRuntimeUiValueType::String;
value.StringValue = uiEvent.Value;
Owner_.Values_[writePath] = std::move(value);
}
if (uiEvent.Action.empty() &&
uiEvent.EventType != MetaCoreRuntimeUiEventType::Focus &&
uiEvent.EventType != MetaCoreRuntimeUiEventType::Blur &&
uiEvent.EventType != MetaCoreRuntimeUiEventType::PointerEnter &&
uiEvent.EventType != MetaCoreRuntimeUiEventType::PointerLeave &&
uiEvent.EventType != MetaCoreRuntimeUiEventType::Scroll) return;
Owner_.Events_.push_back(uiEvent);
if (Owner_.Callback_) Owner_.Callback_(uiEvent);
}
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::vector<MetaCoreRuntimeUiEvent> Events_;
std::vector<std::string> Errors_;
std::vector<MetaCoreUiDiagnostic> Diagnostics_;
EventCallback Callback_;
MetaCoreRuntimeUiInputSource CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Programmatic;
std::string Locale_{"en-US"};
std::string Theme_{};
bool Initialized_ = false;
[[nodiscard]] bool HasInputEnabledDocument() const {
return std::any_of(Documents_.begin(), Documents_.end(), [](const DocumentState& state) {
return state.Entry.Visible && state.Entry.InputEnabled && state.Document != nullptr;
});
}
void ApplyBindings(const std::string* onlyPath = nullptr) {
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 format = element->GetAttribute<Rml::String>("data-metacore-format-" + target, "");
const std::string text = FormatUiValue(value->second, format);
if (target == "text") element->SetInnerRML(EscapeRmlText(text));
else if (target == "value") element->SetAttribute("value", text);
else if (target == "visible") {
if (value->second.BoolValue) element->RemoveAttribute("hidden"); else element->SetAttribute("hidden", "true");
} else if (target == "enabled") {
if (value->second.BoolValue) element->RemoveAttribute("disabled"); else element->SetAttribute("disabled", "true");
} else element->SetAttribute("data-metacore-selected-index", text);
}
}
Rml::ElementList legacyElements;
state.Document->QuerySelectorAll(legacyElements, "[data-metacore-bind]");
for (Rml::Element* element : legacyElements) {
const std::string path = element->GetAttribute<Rml::String>("data-metacore-bind", "");
if (onlyPath != nullptr && path != *onlyPath) continue;
const auto value = Values_.find(path);
if (value == Values_.end()) continue;
element->SetInnerRML(EscapeRmlText(FormatUiValue(value->second,
element->GetAttribute<Rml::String>("data-metacore-format", ""))));
}
}
}
bool LoadEntry(const MetaCoreRuntimeUiDocumentEntry& entry) {
if (Context_ == nullptr) return false;
std::filesystem::path relativePath = entry.RmlPath;
if (relativePath.empty() && entry.UiDocumentAssetGuid.IsValid())
relativePath = std::filesystem::path("Compiled") / entry.UiDocumentAssetGuid.ToString() / "document.rml";
const std::filesystem::path absolutePath = UiRoot_ / relativePath;
if (entry.DocumentId.empty() || relativePath.empty() || !IsPathInsideRoot(UiRoot_, absolutePath)) {
Errors_.push_back("Invalid UI manifest entry: " + entry.DocumentId);
return false;
}
Rml::ElementDocument* document = Context_->LoadDocument(absolutePath.string());
if (document == nullptr) {
Errors_.push_back("Failed to load RML: " + relativePath.generic_string());
return false;
}
if (entry.Visible) document->Show(); else document->Hide();
Listeners_.push_back(std::make_unique<EventListener>(*this, entry.DocumentId));
for (const char* type : {"mouseover", "mouseout", "click", "submit", "change", "scroll", "focus", "blur"})
document->AddEventListener(type, Listeners_.back().get());
Documents_.push_back({entry, document});
return true;
}
};
MetaCoreRuntimeUiSystem::MetaCoreRuntimeUiSystem() : Impl_(std::make_unique<Impl>()) {}
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);
});
}
void MetaCoreRuntimeUiSystem::Shutdown() {
if (!Impl_) return;
if (Impl_->ViewportRenderer_ != nullptr) {
Impl_->ViewportRenderer_->SetRuntimeOverlayRenderCallback({});
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_->Diagnostics_.clear();
Impl_->Initialized_ = false;
}
void MetaCoreRuntimeUiSystem::Update(float) {
if (!Impl_->Initialized_ || Impl_->Context_ == nullptr || Impl_->Window_ == nullptr) return;
const auto [width, height] = Impl_->Window_->GetFramebufferSize();
Impl_->Context_->SetDimensions({width, height});
Impl_->RenderInterface_.BeginFrame();
Impl_->Context_->Update();
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();
const glm::vec2 cursor = input.GetCursorPosition();
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Mouse;
consumed.Mouse = !Impl_->Context_->ProcessMouseMove(static_cast<int>(cursor.x), static_cast<int>(cursor.y), 0);
for (int button = 0; button < 3; ++button) {
const auto mouseButton = static_cast<MetaCoreMouseButton>(button);
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 (std::abs(input.GetMouseWheelDelta()) > 0.001F) consumed.Mouse = !Impl_->Context_->ProcessMouseWheel(input.GetMouseWheelDelta(), 0) || consumed.Mouse;
for (const MetaCoreKeyEvent& event : input.GetKeyEvents()) {
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Keyboard;
const Rml::Input::KeyIdentifier key = MetaCoreMapGlfwKeyToRml(event.Key);
const int modifiers = MetaCoreMapGlfwModifiersToRml(event.Modifiers);
const bool propagated = event.Pressed ? Impl_->Context_->ProcessKeyDown(key, modifiers) : Impl_->Context_->ProcessKeyUp(key, modifiers);
consumed.Keyboard = !propagated || consumed.Keyboard;
}
for (const char32_t codePoint : input.GetTextInput()) {
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Keyboard;
consumed.Keyboard = !Impl_->Context_->ProcessTextInput(static_cast<Rml::Character>(codePoint)) || consumed.Keyboard;
}
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Programmatic;
return consumed;
}
void MetaCoreRuntimeUiSystem::ApplyDataUpdates(const std::vector<MetaCoreRuntimeDataUpdate>& updates) {
if (!Impl_->Initialized_) return;
for (const MetaCoreRuntimeDataUpdate& update : updates) {
const std::string path = update.DataPointId.starts_with("runtime.") ? update.DataPointId : "runtime." + update.DataPointId;
Impl_->Values_[path] = ToUiValue(update.Value);
Impl_->Values_[update.DataPointId] = ToUiValue(update.Value);
}
Impl_->ApplyBindings();
}
bool MetaCoreRuntimeUiSystem::SetValue(const std::string& sourcePath, const MetaCoreRuntimeUiValue& value) {
if (!Impl_->Initialized_ || sourcePath.empty()) return false;
Impl_->Values_[sourcePath] = value;
Impl_->ApplyBindings(&sourcePath);
return true;
}
std::optional<MetaCoreRuntimeUiValue> MetaCoreRuntimeUiSystem::GetValue(const std::string& sourcePath) const {
const auto found = Impl_->Values_.find(sourcePath);
return found == Impl_->Values_.end() ? std::nullopt : std::optional<MetaCoreRuntimeUiValue>(found->second);
}
bool MetaCoreRuntimeUiSystem::SetListRows(const std::string& sourcePath, std::vector<MetaCoreRuntimeUiRow> rows) {
if (!Impl_->Initialized_ || sourcePath.empty()) return false;
Impl_->Lists_[sourcePath] = std::move(rows);
for (auto& state : Impl_->Documents_) {
if (state.Document == nullptr) continue;
Rml::ElementList elements;
state.Document->QuerySelectorAll(elements, "[data-metacore-bind-items]");
for (Rml::Element* element : elements) {
if (element->GetAttribute<Rml::String>("data-metacore-bind-items", "") != sourcePath) continue;
std::ostringstream markup;
std::size_t index = 0;
for (const auto& row : Impl_->Lists_[sourcePath]) {
markup << "<div class=\"mcui-list-row\" data-index=\"" << index++ << "\">";
std::vector<std::string> keys;
keys.reserve(row.size());
for (const auto& [key, _] : row) keys.push_back(key);
std::sort(keys.begin(), keys.end());
for (const std::string& key : keys)
markup << "<span data-field=\"" << EscapeRmlText(key) << "\">"
<< EscapeRmlText(FormatUiValue(row.at(key), {})) << "</span>";
markup << "</div>";
}
element->SetInnerRML(markup.str());
}
}
return true;
}
bool MetaCoreRuntimeUiSystem::RequestFocus(std::uint64_t instanceId, const std::string& elementId) {
for (auto& state : Impl_->Documents_) {
if (state.Entry.InstanceId != instanceId || state.Document == nullptr) continue;
if (Rml::Element* element = state.Document->GetElementById(elementId); element != nullptr) {
element->Focus();
return true;
}
}
return false;
}
bool MetaCoreRuntimeUiSystem::CreateInstance(const MetaCoreRuntimeUiDocumentEntry& entry) {
if (!Impl_->Initialized_ || entry.InstanceId == 0 ||
std::any_of(Impl_->Documents_.begin(), Impl_->Documents_.end(), [&](const auto& value) {
return value.Entry.InstanceId == entry.InstanceId;
})) return false;
return Impl_->LoadEntry(entry);
}
bool MetaCoreRuntimeUiSystem::DestroyInstance(std::uint64_t instanceId) {
const auto found = std::find_if(Impl_->Documents_.begin(), Impl_->Documents_.end(), [&](const auto& value) {
return value.Entry.InstanceId == instanceId;
});
if (found == Impl_->Documents_.end()) return false;
if (found->Document != nullptr) found->Document->Close();
Impl_->Documents_.erase(found);
return true;
}
bool MetaCoreRuntimeUiSystem::SetDocumentVisible(const std::string& documentId, bool visible) {
for (Impl::DocumentState& state : Impl_->Documents_) {
if (state.Entry.DocumentId != documentId || state.Document == nullptr) continue;
if (visible) state.Document->Show(); else state.Document->Hide();
state.Entry.Visible = visible;
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) state.Document->SetAttribute("data-metacore-locale", Impl_->Locale_);
}
void MetaCoreRuntimeUiSystem::SetTheme(std::string theme) {
Impl_->Theme_ = std::move(theme);
for (auto& state : Impl_->Documents_) if (state.Document != nullptr) state.Document->SetAttribute("data-metacore-theme", Impl_->Theme_);
}
const std::string& MetaCoreRuntimeUiSystem::GetLocale() const { return Impl_->Locale_; }
const std::string& MetaCoreRuntimeUiSystem::GetTheme() const { return Impl_->Theme_; }
std::vector<MetaCoreRuntimeUiEvent> MetaCoreRuntimeUiSystem::ConsumeEvents() { return std::exchange(Impl_->Events_, {}); }
void MetaCoreRuntimeUiSystem::SetEventCallback(EventCallback callback) { Impl_->Callback_ = std::move(callback); }
bool MetaCoreRuntimeUiSystem::IsInitialized() const { return Impl_->Initialized_; }
std::size_t MetaCoreRuntimeUiSystem::GetLoadedDocumentCount() const { return Impl_->Documents_.size(); }
const std::vector<std::string>& MetaCoreRuntimeUiSystem::GetErrors() const { return Impl_->Errors_; }
std::vector<MetaCoreUiDiagnostic> MetaCoreRuntimeUiSystem::GetDiagnostics() const { return Impl_->Diagnostics_; }
} // namespace MetaCore