MetaCore/Source/MetaCoreRuntimeUi/Private/MetaCoreRuntimeUiSystem.cpp
2026-07-16 10:26:51 +08:00

720 lines
33 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 FormatValue(const MetaCoreRuntimeDataValue& value, const std::string& format) {
std::ostringstream stream;
if (!format.empty()) stream << std::fixed << std::setprecision(std::max(0, std::atoi(format.c_str())));
switch (value.Type) {
case MetaCoreRuntimeValueType::Bool: return value.BoolValue ? "true" : "false";
case MetaCoreRuntimeValueType::Int64: stream << value.Int64Value; break;
case MetaCoreRuntimeValueType::Double: stream << value.DoubleValue; break;
case MetaCoreRuntimeValueType::String: return value.StringValue;
case MetaCoreRuntimeValueType::Vec3:
stream << value.Vec3Value.x << ", " << value.Vec3Value.y << ", " << value.Vec3Value.z;
break;
}
return stream.str();
}
[[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", "");
if (action.empty()) return;
MetaCoreRuntimeUiEvent uiEvent;
uiEvent.DocumentId = DocumentId_;
uiEvent.ElementId = target->GetId();
uiEvent.Action = action;
uiEvent.Type = event.GetType();
uiEvent.Value = target->GetAttribute<Rml::String>("value", "");
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, MetaCoreRuntimeDataValue> Values_;
std::vector<MetaCoreRuntimeUiEvent> Events_;
std::vector<std::string> Errors_;
EventCallback Callback_;
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() {
for (DocumentState& state : Documents_) {
if (state.Document == nullptr) continue;
Rml::ElementList elements;
state.Document->QuerySelectorAll(elements, "[data-metacore-bind]");
for (Rml::Element* element : elements) {
const std::string dataPointId = element->GetAttribute<Rml::String>("data-metacore-bind", "");
const auto value = Values_.find(dataPointId);
if (value == Values_.end()) continue;
const std::string format = element->GetAttribute<Rml::String>("data-metacore-format", "");
element->SetInnerRML(FormatValue(value->second, format));
}
}
}
};
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;
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; });
for (const MetaCoreRuntimeUiDocumentEntry& entry : entries) {
const std::filesystem::path absolutePath = Impl_->UiRoot_ / entry.RmlPath;
if (entry.DocumentId.empty() || entry.RmlPath.empty() || !IsPathInsideRoot(Impl_->UiRoot_, absolutePath)) {
Impl_->Errors_.push_back("Invalid UI manifest entry: " + entry.DocumentId);
continue;
}
Rml::ElementDocument* document = Impl_->Context_->LoadDocument(absolutePath.string());
if (document == nullptr) {
Impl_->Errors_.push_back("Failed to load RML: " + entry.RmlPath.generic_string());
continue;
}
if (entry.Visible) document->Show(); else document->Hide();
Impl_->Listeners_.push_back(std::make_unique<Impl::EventListener>(*Impl_, entry.DocumentId));
document->AddEventListener("click", Impl_->Listeners_.back().get());
document->AddEventListener("submit", Impl_->Listeners_.back().get());
document->AddEventListener("change", Impl_->Listeners_.back().get());
Impl_->Documents_.push_back({entry, document});
}
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_->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();
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()) {
const Rml::Input::KeyIdentifier key = MetaCoreMapGlfwKeyToRml(event.Key);
const int modifiers = MetaCoreMapGlfwModifiersToRml(event.Modifiers);
const bool propagated = event.Pressed ? Impl_->Context_->ProcessKeyDown(key, modifiers) : Impl_->Context_->ProcessKeyUp(key, modifiers);
consumed.Keyboard = !propagated || consumed.Keyboard;
}
for (const char32_t codePoint : input.GetTextInput()) consumed.Keyboard = !Impl_->Context_->ProcessTextInput(static_cast<Rml::Character>(codePoint)) || consumed.Keyboard;
return consumed;
}
void MetaCoreRuntimeUiSystem::ApplyDataUpdates(const std::vector<MetaCoreRuntimeDataUpdate>& updates) {
if (!Impl_->Initialized_) return;
for (const MetaCoreRuntimeDataUpdate& update : updates) Impl_->Values_[update.DataPointId] = update.Value;
Impl_->ApplyBindings();
}
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;
}
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_; }
} // namespace MetaCore