933 lines
44 KiB
C++
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 += "&"; break;
|
|
case '<': result += "<"; break;
|
|
case '>': result += ">"; break;
|
|
case '"': result += """; break;
|
|
default: result += character; break;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] std::string FormatUiValue(const MetaCoreRuntimeUiValue& value, const std::string& format) {
|
|
std::ostringstream stream;
|
|
if (!format.empty()) stream << std::fixed << std::setprecision(std::max(0, std::atoi(format.c_str())));
|
|
switch (value.Type) {
|
|
case 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
|