1616 lines
81 KiB
C++
1616 lines
81 KiB
C++
#include "MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h"
|
|
|
|
#include "MetaCorePlatform/MetaCoreInput.h"
|
|
#include "MetaCorePlatform/MetaCoreWindow.h"
|
|
#include "MetaCoreRender/MetaCoreEditorViewportRenderer.h"
|
|
#include "stb/stb_image.h"
|
|
|
|
#include <RmlUi/Core.h>
|
|
#include <RmlUi/Core/Elements/ElementFormControlSelect.h>
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <GLFW/glfw3.h>
|
|
#include <glm/gtc/type_ptr.hpp>
|
|
|
|
#include <filament/Camera.h>
|
|
#include <filament/Engine.h>
|
|
#include <filament/IndexBuffer.h>
|
|
#include <filament/Material.h>
|
|
#include <filament/MaterialInstance.h>
|
|
#include <filament/RenderableManager.h>
|
|
#include <filament/RenderTarget.h>
|
|
#include <filament/Renderer.h>
|
|
#include <filament/Scene.h>
|
|
#include <filament/Texture.h>
|
|
#include <filament/TextureSampler.h>
|
|
#include <filament/TransformManager.h>
|
|
#include <filament/VertexBuffer.h>
|
|
#include <filament/View.h>
|
|
#include <filament/Viewport.h>
|
|
|
|
#include <utils/EntityManager.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <limits>
|
|
#include <sstream>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace MetaCore {
|
|
|
|
namespace {
|
|
|
|
[[nodiscard]] bool IsPathInsideRoot(const std::filesystem::path& root, const std::filesystem::path& candidate) {
|
|
std::error_code error;
|
|
const std::filesystem::path canonicalRoot = std::filesystem::weakly_canonical(root, error);
|
|
if (error) return false;
|
|
const std::filesystem::path canonicalCandidate = std::filesystem::weakly_canonical(candidate, error);
|
|
if (error) return false;
|
|
const std::filesystem::path relative = canonicalCandidate.lexically_relative(canonicalRoot);
|
|
return !relative.empty() && !relative.is_absolute() && *relative.begin() != "..";
|
|
}
|
|
|
|
[[nodiscard]] std::string EscapeRmlText(std::string_view value) {
|
|
std::string result;
|
|
result.reserve(value.size());
|
|
for (char character : value) {
|
|
switch (character) {
|
|
case '&': result += "&"; 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]] std::optional<MetaCoreRuntimeUiValue> ParseUiValue(std::string_view text, MetaCoreUiInputValueType type) {
|
|
MetaCoreRuntimeUiValue result;
|
|
if (type == MetaCoreUiInputValueType::Text) {
|
|
result.Type = MetaCoreRuntimeUiValueType::String;
|
|
result.StringValue = std::string(text);
|
|
return result;
|
|
}
|
|
const std::string owned(text);
|
|
char* end = nullptr;
|
|
if (type == MetaCoreUiInputValueType::Integer) {
|
|
const long long value = std::strtoll(owned.c_str(), &end, 10);
|
|
if (end == owned.c_str() || *end != '\0') return std::nullopt;
|
|
result.Type = MetaCoreRuntimeUiValueType::Int64;
|
|
result.Int64Value = static_cast<std::int64_t>(value);
|
|
return result;
|
|
}
|
|
const double value = std::strtod(owned.c_str(), &end);
|
|
if (end == owned.c_str() || *end != '\0' || !std::isfinite(value)) return std::nullopt;
|
|
result.Type = MetaCoreRuntimeUiValueType::Double;
|
|
result.DoubleValue = value;
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] std::optional<MetaCoreRuntimeUiValue> ConvertUiValue(
|
|
const MetaCoreRuntimeUiValue& source,
|
|
std::string_view converter
|
|
) {
|
|
if (converter.empty()) return source;
|
|
MetaCoreRuntimeUiValue result;
|
|
if (converter == "not" && source.Type == MetaCoreRuntimeUiValueType::Bool) {
|
|
result.Type = MetaCoreRuntimeUiValueType::Bool;
|
|
result.BoolValue = !source.BoolValue;
|
|
return result;
|
|
}
|
|
if (converter == "to_string") {
|
|
result.Type = MetaCoreRuntimeUiValueType::String;
|
|
result.StringValue = FormatUiValue(source, {});
|
|
return result;
|
|
}
|
|
if ((converter == "uppercase" || converter == "lowercase") &&
|
|
source.Type == MetaCoreRuntimeUiValueType::String) {
|
|
result = source;
|
|
std::transform(result.StringValue.begin(), result.StringValue.end(), result.StringValue.begin(),
|
|
[&](unsigned char value) {
|
|
return static_cast<char>(converter == "uppercase" ? std::toupper(value) : std::tolower(value));
|
|
});
|
|
return result;
|
|
}
|
|
if (converter == "to_bool") {
|
|
result.Type = MetaCoreRuntimeUiValueType::Bool;
|
|
if (source.Type == MetaCoreRuntimeUiValueType::Bool) result.BoolValue = source.BoolValue;
|
|
else if (source.Type == MetaCoreRuntimeUiValueType::Int64) result.BoolValue = source.Int64Value != 0;
|
|
else if (source.Type == MetaCoreRuntimeUiValueType::Double) result.BoolValue = source.DoubleValue != 0.0;
|
|
else if (source.Type == MetaCoreRuntimeUiValueType::String &&
|
|
(source.StringValue == "true" || source.StringValue == "1")) result.BoolValue = true;
|
|
else if (source.Type != MetaCoreRuntimeUiValueType::String ||
|
|
(source.StringValue != "false" && source.StringValue != "0")) return std::nullopt;
|
|
return result;
|
|
}
|
|
if (converter == "to_int" || converter == "to_double") {
|
|
double numeric = 0.0;
|
|
if (source.Type == MetaCoreRuntimeUiValueType::Int64) numeric = static_cast<double>(source.Int64Value);
|
|
else if (source.Type == MetaCoreRuntimeUiValueType::Double) numeric = source.DoubleValue;
|
|
else if (source.Type == MetaCoreRuntimeUiValueType::Bool) numeric = source.BoolValue ? 1.0 : 0.0;
|
|
else if (source.Type == MetaCoreRuntimeUiValueType::String) {
|
|
char* end = nullptr;
|
|
numeric = std::strtod(source.StringValue.c_str(), &end);
|
|
if (end == source.StringValue.c_str() || *end != '\0' || !std::isfinite(numeric)) return std::nullopt;
|
|
} else return std::nullopt;
|
|
if (converter == "to_int") {
|
|
result.Type = MetaCoreRuntimeUiValueType::Int64;
|
|
result.Int64Value = static_cast<std::int64_t>(numeric);
|
|
} else {
|
|
result.Type = MetaCoreRuntimeUiValueType::Double;
|
|
result.DoubleValue = numeric;
|
|
}
|
|
return result;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
[[nodiscard]] bool UiValuesEqual(const MetaCoreRuntimeUiValue& lhs, const MetaCoreRuntimeUiValue& rhs) {
|
|
if (lhs.Type != rhs.Type) return false;
|
|
switch (lhs.Type) {
|
|
case MetaCoreRuntimeUiValueType::Bool: return lhs.BoolValue == rhs.BoolValue;
|
|
case MetaCoreRuntimeUiValueType::Int64: return lhs.Int64Value == rhs.Int64Value;
|
|
case MetaCoreRuntimeUiValueType::Double: return lhs.DoubleValue == rhs.DoubleValue;
|
|
case MetaCoreRuntimeUiValueType::String: return lhs.StringValue == rhs.StringValue;
|
|
case MetaCoreRuntimeUiValueType::Vec3: return lhs.Vec3Value == rhs.Vec3Value;
|
|
case MetaCoreRuntimeUiValueType::AssetRef: return lhs.AssetValue == rhs.AssetValue;
|
|
case MetaCoreRuntimeUiValueType::GameObjectRef: return lhs.ObjectValue == rhs.ObjectValue;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
[[nodiscard]] MetaCoreRuntimeUiValue ToUiValue(const MetaCoreRuntimeDataValue& value) {
|
|
MetaCoreRuntimeUiValue result;
|
|
result.Type = static_cast<MetaCoreRuntimeUiValueType>(value.Type);
|
|
result.BoolValue = value.BoolValue;
|
|
result.Int64Value = value.Int64Value;
|
|
result.DoubleValue = value.DoubleValue;
|
|
result.StringValue = value.StringValue;
|
|
result.Vec3Value = value.Vec3Value;
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] MetaCoreRuntimeUiEventType EventTypeFromRml(std::string_view type) {
|
|
if (type == "mouseover") return MetaCoreRuntimeUiEventType::PointerEnter;
|
|
if (type == "mouseout") return MetaCoreRuntimeUiEventType::PointerLeave;
|
|
if (type == "submit") return MetaCoreRuntimeUiEventType::Submit;
|
|
if (type == "change") return MetaCoreRuntimeUiEventType::ValueChanged;
|
|
if (type == "scroll") return MetaCoreRuntimeUiEventType::Scroll;
|
|
if (type == "focus") return MetaCoreRuntimeUiEventType::Focus;
|
|
if (type == "blur") return MetaCoreRuntimeUiEventType::Blur;
|
|
return MetaCoreRuntimeUiEventType::Click;
|
|
}
|
|
|
|
[[nodiscard]] Rml::Input::KeyIdentifier MetaCoreMapGlfwKeyToRml(int key) {
|
|
if (key >= GLFW_KEY_0 && key <= GLFW_KEY_9) {
|
|
return static_cast<Rml::Input::KeyIdentifier>(Rml::Input::KI_0 + (key - GLFW_KEY_0));
|
|
}
|
|
if (key >= GLFW_KEY_A && key <= GLFW_KEY_Z) {
|
|
return static_cast<Rml::Input::KeyIdentifier>(Rml::Input::KI_A + (key - GLFW_KEY_A));
|
|
}
|
|
switch (key) {
|
|
case GLFW_KEY_SPACE: return Rml::Input::KI_SPACE;
|
|
case GLFW_KEY_BACKSPACE: return Rml::Input::KI_BACK;
|
|
case GLFW_KEY_TAB: return Rml::Input::KI_TAB;
|
|
case GLFW_KEY_ENTER: return Rml::Input::KI_RETURN;
|
|
case GLFW_KEY_ESCAPE: return Rml::Input::KI_ESCAPE;
|
|
case GLFW_KEY_LEFT: return Rml::Input::KI_LEFT;
|
|
case GLFW_KEY_UP: return Rml::Input::KI_UP;
|
|
case GLFW_KEY_RIGHT: return Rml::Input::KI_RIGHT;
|
|
case GLFW_KEY_DOWN: return Rml::Input::KI_DOWN;
|
|
case GLFW_KEY_HOME: return Rml::Input::KI_HOME;
|
|
case GLFW_KEY_END: return Rml::Input::KI_END;
|
|
case GLFW_KEY_PAGE_UP: return Rml::Input::KI_PRIOR;
|
|
case GLFW_KEY_PAGE_DOWN: return Rml::Input::KI_NEXT;
|
|
case GLFW_KEY_INSERT: return Rml::Input::KI_INSERT;
|
|
case GLFW_KEY_DELETE: return Rml::Input::KI_DELETE;
|
|
case GLFW_KEY_LEFT_SHIFT: return Rml::Input::KI_LSHIFT;
|
|
case GLFW_KEY_RIGHT_SHIFT: return Rml::Input::KI_RSHIFT;
|
|
case GLFW_KEY_LEFT_CONTROL: return Rml::Input::KI_LCONTROL;
|
|
case GLFW_KEY_RIGHT_CONTROL: return Rml::Input::KI_RCONTROL;
|
|
case GLFW_KEY_LEFT_ALT: return Rml::Input::KI_LMENU;
|
|
case GLFW_KEY_RIGHT_ALT: return Rml::Input::KI_RMENU;
|
|
default: return Rml::Input::KI_UNKNOWN;
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] int MetaCoreMapGlfwModifiersToRml(int modifiers) {
|
|
int result = 0;
|
|
if ((modifiers & GLFW_MOD_CONTROL) != 0) result |= Rml::Input::KM_CTRL;
|
|
if ((modifiers & GLFW_MOD_SHIFT) != 0) result |= Rml::Input::KM_SHIFT;
|
|
if ((modifiers & GLFW_MOD_ALT) != 0) result |= Rml::Input::KM_ALT;
|
|
if ((modifiers & GLFW_MOD_SUPER) != 0) result |= Rml::Input::KM_META;
|
|
if ((modifiers & GLFW_MOD_CAPS_LOCK) != 0) result |= Rml::Input::KM_CAPSLOCK;
|
|
if ((modifiers & GLFW_MOD_NUM_LOCK) != 0) result |= Rml::Input::KM_NUMLOCK;
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] std::vector<char> MetaCoreLoadRuntimeUiMaterialPackage() {
|
|
for (const std::filesystem::path& candidate : {
|
|
std::filesystem::path("rml_ui.filamat"),
|
|
std::filesystem::path("Engine/Materials/rml_ui.filamat"),
|
|
std::filesystem::path("../rml_ui.filamat"),
|
|
std::filesystem::path("uiBlit.filamat"),
|
|
std::filesystem::path("Engine/Materials/uiBlit.filamat"),
|
|
std::filesystem::path("../uiBlit.filamat")
|
|
}) {
|
|
std::ifstream input(candidate, std::ios::binary);
|
|
if (!input.is_open()) {
|
|
continue;
|
|
}
|
|
std::vector<char> package((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
|
|
if (!package.empty()) {
|
|
return package;
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
class MetaCoreRmlSystemInterface final : public Rml::SystemInterface {
|
|
public:
|
|
explicit MetaCoreRmlSystemInterface(MetaCoreWindow& window) : Window_(window) {}
|
|
|
|
void SetClipboardText(const Rml::String& text) override { Window_.SetClipboardText(text); }
|
|
void GetClipboardText(Rml::String& text) override { text = Window_.GetClipboardText(); }
|
|
|
|
private:
|
|
MetaCoreWindow& Window_;
|
|
};
|
|
|
|
class MetaCoreRmlFilamentRenderInterface final : public Rml::RenderInterface {
|
|
public:
|
|
struct DrawCommand {
|
|
Rml::CompiledGeometryHandle Geometry = 0;
|
|
Rml::Vector2f Translation{};
|
|
Rml::TextureHandle Texture = 0;
|
|
bool ScissorEnabled = false;
|
|
Rml::Rectanglei Scissor{};
|
|
};
|
|
|
|
Rml::CompiledGeometryHandle CompileGeometry(Rml::Span<const Rml::Vertex> vertices, Rml::Span<const int> indices) override {
|
|
if (vertices.empty() || indices.empty() || vertices.size() > std::numeric_limits<std::uint32_t>::max()) {
|
|
return 0;
|
|
}
|
|
|
|
Geometry geometry;
|
|
geometry.Vertices.reserve(vertices.size());
|
|
for (const Rml::Vertex& vertex : vertices) {
|
|
geometry.Vertices.push_back({
|
|
vertex.position.x,
|
|
vertex.position.y,
|
|
vertex.tex_coord.x,
|
|
vertex.tex_coord.y,
|
|
{vertex.colour.red, vertex.colour.green, vertex.colour.blue, vertex.colour.alpha}
|
|
});
|
|
}
|
|
geometry.Indices.reserve(indices.size());
|
|
for (const int index : indices) {
|
|
if (index < 0 || static_cast<std::size_t>(index) >= vertices.size()) return 0;
|
|
geometry.Indices.push_back(static_cast<std::uint32_t>(index));
|
|
}
|
|
|
|
const Rml::CompiledGeometryHandle handle = NextGeometryHandle_++;
|
|
Geometries_.emplace(handle, std::move(geometry));
|
|
return handle;
|
|
}
|
|
|
|
void RenderGeometry(Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation, Rml::TextureHandle texture) override {
|
|
if (!Geometries_.contains(geometry)) return;
|
|
DrawCommand command{geometry, translation, texture, ScissorEnabled_, Scissor_};
|
|
if (ActiveWorldInstance_ != 0) WorldDrawCommands_[ActiveWorldInstance_].push_back(command);
|
|
else DrawCommands_.push_back(command);
|
|
}
|
|
|
|
void ReleaseGeometry(Rml::CompiledGeometryHandle geometry) override { Geometries_.erase(geometry); }
|
|
|
|
Rml::TextureHandle LoadTexture(Rml::Vector2i& dimensions, const Rml::String& source) override {
|
|
std::filesystem::path path(source);
|
|
if (path.is_relative()) path = UiRoot_ / path;
|
|
if (!IsPathInsideRoot(UiRoot_, path)) return 0;
|
|
|
|
int width = 0;
|
|
int height = 0;
|
|
int channels = 0;
|
|
stbi_uc* pixels = stbi_load(path.string().c_str(), &width, &height, &channels, STBI_rgb_alpha);
|
|
if (pixels == nullptr || width <= 0 || height <= 0) {
|
|
stbi_image_free(pixels);
|
|
return 0;
|
|
}
|
|
|
|
Texture texture;
|
|
texture.Width = width;
|
|
texture.Height = height;
|
|
texture.Pixels.assign(pixels, pixels + static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4U);
|
|
stbi_image_free(pixels);
|
|
dimensions = {width, height};
|
|
return StoreTexture(std::move(texture));
|
|
}
|
|
|
|
Rml::TextureHandle GenerateTexture(Rml::Span<const Rml::byte> source, Rml::Vector2i dimensions) override {
|
|
if (dimensions.x <= 0 || dimensions.y <= 0) return 0;
|
|
const std::size_t byteCount = static_cast<std::size_t>(dimensions.x) * static_cast<std::size_t>(dimensions.y) * 4U;
|
|
if (source.size() != byteCount) return 0;
|
|
Texture texture;
|
|
texture.Width = dimensions.x;
|
|
texture.Height = dimensions.y;
|
|
texture.Pixels.assign(source.begin(), source.end());
|
|
return StoreTexture(std::move(texture));
|
|
}
|
|
|
|
void ReleaseTexture(Rml::TextureHandle texture) override {
|
|
const auto found = Textures_.find(texture);
|
|
if (found == Textures_.end()) return;
|
|
if (Engine_ != nullptr && found->second.FilamentTexture != nullptr) {
|
|
Engine_->destroy(found->second.FilamentTexture);
|
|
}
|
|
Textures_.erase(found);
|
|
}
|
|
|
|
void EnableScissorRegion(bool enable) override { ScissorEnabled_ = enable; }
|
|
void SetScissorRegion(Rml::Rectanglei region) override { Scissor_ = region; }
|
|
|
|
void BeginFrame() { DrawCommands_.clear(); WorldDrawCommands_.clear(); ActiveWorldInstance_ = 0; }
|
|
void BeginWorld(std::uint64_t instanceId) { ActiveWorldInstance_ = instanceId; }
|
|
void EndWorld() { ActiveWorldInstance_ = 0; }
|
|
|
|
void Render(filament::Engine& engine, filament::Renderer& renderer, uint32_t width, uint32_t height,
|
|
filament::RenderTarget* renderTarget = nullptr) {
|
|
if (width == 0 || height == 0 || DrawCommands_.empty()) return;
|
|
EnsureFilamentResources(engine);
|
|
if (Scene_ == nullptr || View_ == nullptr || Material_ == nullptr || Camera_ == nullptr) return;
|
|
|
|
ClearFrameResources();
|
|
View_->setViewport({0, 0, width, height});
|
|
View_->setRenderTarget(renderTarget);
|
|
if (renderTarget != nullptr) {
|
|
filament::Renderer::ClearOptions clear;
|
|
clear.clear = true;
|
|
clear.clearColor = {0.0F, 0.0F, 0.0F, 0.0F};
|
|
renderer.setClearOptions(clear);
|
|
}
|
|
Camera_->setProjection(filament::Camera::Projection::ORTHO, 0.0, static_cast<double>(width),
|
|
static_cast<double>(height), 0.0, 0.0, 1.0);
|
|
|
|
std::vector<DrawCommand> commands;
|
|
commands.reserve(DrawCommands_.size());
|
|
for (const DrawCommand& command : DrawCommands_) {
|
|
if (Geometries_.contains(command.Geometry)) commands.push_back(command);
|
|
}
|
|
if (commands.empty()) return;
|
|
|
|
FrameRenderable_ = utils::EntityManager::get().create();
|
|
filament::RenderableManager::Builder builder(static_cast<uint32_t>(commands.size()));
|
|
builder.boundingBox({{0.0f, 0.0f, -1.0f}, {static_cast<float>(width), static_cast<float>(height), 1.0f}})
|
|
.culling(false)
|
|
.castShadows(false)
|
|
.receiveShadows(false);
|
|
|
|
for (std::size_t primitiveIndex = 0; primitiveIndex < commands.size(); ++primitiveIndex) {
|
|
const DrawCommand& command = commands[primitiveIndex];
|
|
const Geometry& geometry = Geometries_.at(command.Geometry);
|
|
if (!CreateFrameGeometry(engine, geometry, command.Translation)) continue;
|
|
|
|
filament::MaterialInstance* materialInstance = Material_->createInstance();
|
|
FrameMaterialInstances_.push_back(materialInstance);
|
|
const filament::Texture* texture = ResolveTexture(engine, command.Texture);
|
|
if (texture != nullptr) {
|
|
materialInstance->setParameter("albedo", texture, filament::TextureSampler(
|
|
filament::TextureSampler::MinFilter::LINEAR,
|
|
filament::TextureSampler::MagFilter::LINEAR
|
|
));
|
|
}
|
|
if (command.ScissorEnabled) {
|
|
const uint32_t left = static_cast<uint32_t>(std::max(0, command.Scissor.Left()));
|
|
const uint32_t bottom = static_cast<uint32_t>(std::max(0, static_cast<int>(height) - command.Scissor.Bottom()));
|
|
const uint32_t scissorWidth = static_cast<uint32_t>(std::max(0, command.Scissor.Width()));
|
|
const uint32_t scissorHeight = static_cast<uint32_t>(std::max(0, command.Scissor.Height()));
|
|
materialInstance->setScissor(left, bottom, scissorWidth, scissorHeight);
|
|
} else {
|
|
materialInstance->unsetScissor();
|
|
}
|
|
|
|
builder.geometry(static_cast<uint8_t>(primitiveIndex), filament::RenderableManager::PrimitiveType::TRIANGLES,
|
|
FrameVertexBuffers_.back(), FrameIndexBuffers_.back(), 0, static_cast<uint32_t>(geometry.Indices.size()))
|
|
.material(static_cast<uint8_t>(primitiveIndex), materialInstance)
|
|
.blendOrder(static_cast<uint16_t>(primitiveIndex), static_cast<uint16_t>(primitiveIndex));
|
|
}
|
|
|
|
if (FrameVertexBuffers_.empty()) {
|
|
utils::EntityManager::get().destroy(FrameRenderable_);
|
|
FrameRenderable_ = {};
|
|
return;
|
|
}
|
|
builder.build(engine, FrameRenderable_);
|
|
Scene_->addEntity(FrameRenderable_);
|
|
renderer.render(View_);
|
|
View_->setRenderTarget(nullptr);
|
|
}
|
|
|
|
struct WorldSurface {
|
|
std::uint64_t InstanceId = 0;
|
|
std::uint64_t SceneObjectId = 0;
|
|
std::int32_t Width = 1;
|
|
std::int32_t Height = 1;
|
|
glm::vec3 WorldSize{1.0F};
|
|
};
|
|
|
|
void RenderWorldSurfaces(
|
|
filament::Engine& engine,
|
|
filament::Renderer& renderer,
|
|
filament::Scene& mainScene,
|
|
const std::vector<WorldSurface>& surfaces,
|
|
const std::function<bool(std::uint64_t, glm::mat4&)>& worldMatrixProvider
|
|
) {
|
|
EnsureFilamentResources(engine);
|
|
std::unordered_set<std::uint64_t> active;
|
|
for (const WorldSurface& surface : surfaces) {
|
|
const auto commands = WorldDrawCommands_.find(surface.InstanceId);
|
|
if (commands == WorldDrawCommands_.end() || commands->second.empty()) continue;
|
|
active.insert(surface.InstanceId);
|
|
WorldTarget& target = EnsureWorldTarget(engine, mainScene, surface);
|
|
std::swap(DrawCommands_, commands->second);
|
|
Render(engine, renderer, static_cast<std::uint32_t>(surface.Width),
|
|
static_cast<std::uint32_t>(surface.Height), target.Target);
|
|
std::swap(DrawCommands_, commands->second);
|
|
glm::mat4 world(1.0F);
|
|
if (worldMatrixProvider(surface.SceneObjectId, world) && target.SurfaceEntity) {
|
|
auto& transforms = engine.getTransformManager();
|
|
const auto instance = transforms.getInstance(target.SurfaceEntity);
|
|
if (instance) transforms.setTransform(instance,
|
|
*reinterpret_cast<const filament::math::mat4f*>(glm::value_ptr(world)));
|
|
}
|
|
}
|
|
for (auto iterator = WorldTargets_.begin(); iterator != WorldTargets_.end();) {
|
|
if (!active.contains(iterator->first)) {
|
|
DestroyWorldTarget(iterator->second);
|
|
iterator = WorldTargets_.erase(iterator);
|
|
} else ++iterator;
|
|
}
|
|
}
|
|
|
|
void Shutdown() {
|
|
if (Engine_ == nullptr) return;
|
|
ClearFrameResources();
|
|
for (auto& [_, target] : WorldTargets_) DestroyWorldTarget(target);
|
|
WorldTargets_.clear();
|
|
for (auto& [_, texture] : Textures_) {
|
|
if (texture.FilamentTexture != nullptr) Engine_->destroy(texture.FilamentTexture);
|
|
}
|
|
Textures_.clear();
|
|
if (WhiteTexture_ != nullptr) Engine_->destroy(WhiteTexture_);
|
|
if (Material_ != nullptr) Engine_->destroy(Material_);
|
|
if (View_ != nullptr) Engine_->destroy(View_);
|
|
if (Camera_ != nullptr) {
|
|
const utils::Entity cameraEntity = Camera_->getEntity();
|
|
Engine_->destroyCameraComponent(cameraEntity);
|
|
utils::EntityManager::get().destroy(cameraEntity);
|
|
}
|
|
if (Scene_ != nullptr) Engine_->destroy(Scene_);
|
|
Material_ = nullptr;
|
|
WhiteTexture_ = nullptr;
|
|
View_ = nullptr;
|
|
Camera_ = nullptr;
|
|
Scene_ = nullptr;
|
|
Engine_ = nullptr;
|
|
}
|
|
|
|
void SetUiRoot(std::filesystem::path root) { UiRoot_ = std::move(root); }
|
|
|
|
private:
|
|
struct GpuVertex {
|
|
float Position[2];
|
|
float Uv[2];
|
|
std::array<std::uint8_t, 4> Color{};
|
|
};
|
|
|
|
struct Geometry {
|
|
std::vector<GpuVertex> Vertices;
|
|
std::vector<std::uint32_t> Indices;
|
|
};
|
|
|
|
struct Texture {
|
|
int Width = 0;
|
|
int Height = 0;
|
|
std::vector<std::uint8_t> Pixels;
|
|
filament::Texture* FilamentTexture = nullptr;
|
|
};
|
|
|
|
struct WorldTarget {
|
|
filament::Texture* Color = nullptr;
|
|
filament::RenderTarget* Target = nullptr;
|
|
filament::VertexBuffer* VertexBuffer = nullptr;
|
|
filament::IndexBuffer* IndexBuffer = nullptr;
|
|
filament::MaterialInstance* MaterialInstance = nullptr;
|
|
filament::Scene* MainScene = nullptr;
|
|
utils::Entity SurfaceEntity{};
|
|
std::int32_t Width = 0;
|
|
std::int32_t Height = 0;
|
|
glm::vec3 WorldSize{0.0F};
|
|
};
|
|
|
|
[[nodiscard]] Rml::TextureHandle StoreTexture(Texture texture) {
|
|
const Rml::TextureHandle handle = NextTextureHandle_++;
|
|
Textures_.emplace(handle, std::move(texture));
|
|
return handle;
|
|
}
|
|
|
|
void EnsureFilamentResources(filament::Engine& engine) {
|
|
if (Engine_ != nullptr && Engine_ != &engine) Shutdown();
|
|
if (Engine_ != nullptr) return;
|
|
Engine_ = &engine;
|
|
Scene_ = Engine_->createScene();
|
|
View_ = Engine_->createView();
|
|
View_->setScene(Scene_);
|
|
View_->setPostProcessingEnabled(false);
|
|
View_->setBlendMode(filament::View::BlendMode::TRANSLUCENT);
|
|
View_->setShadowingEnabled(false);
|
|
const utils::Entity cameraEntity = utils::EntityManager::get().create();
|
|
Camera_ = Engine_->createCamera(cameraEntity);
|
|
View_->setCamera(Camera_);
|
|
|
|
const std::vector<char> package = MetaCoreLoadRuntimeUiMaterialPackage();
|
|
if (!package.empty()) Material_ = filament::Material::Builder().package(package.data(), package.size()).build(*Engine_);
|
|
CreateWhiteTexture();
|
|
}
|
|
|
|
[[nodiscard]] const filament::Texture* ResolveTexture(filament::Engine& engine, Rml::TextureHandle handle) {
|
|
const auto found = Textures_.find(handle);
|
|
if (found == Textures_.end()) return WhiteTexture_;
|
|
Texture& texture = found->second;
|
|
if (texture.FilamentTexture != nullptr) return texture.FilamentTexture;
|
|
if (texture.Width <= 0 || texture.Height <= 0 || texture.Pixels.empty()) return WhiteTexture_;
|
|
|
|
void* pixelBytes = std::malloc(texture.Pixels.size());
|
|
if (pixelBytes == nullptr) return nullptr;
|
|
std::memcpy(pixelBytes, texture.Pixels.data(), texture.Pixels.size());
|
|
filament::Texture::PixelBufferDescriptor descriptor(pixelBytes, texture.Pixels.size(),
|
|
filament::Texture::Format::RGBA, filament::Texture::Type::UBYTE,
|
|
[](void* buffer, std::size_t, void*) { std::free(buffer); }, nullptr);
|
|
texture.FilamentTexture = filament::Texture::Builder()
|
|
.width(static_cast<uint32_t>(texture.Width))
|
|
.height(static_cast<uint32_t>(texture.Height))
|
|
.levels(1)
|
|
.format(filament::Texture::InternalFormat::RGBA8)
|
|
.sampler(filament::Texture::Sampler::SAMPLER_2D)
|
|
.build(engine);
|
|
texture.FilamentTexture->setImage(engine, 0, std::move(descriptor));
|
|
return texture.FilamentTexture;
|
|
}
|
|
|
|
void CreateWhiteTexture() {
|
|
if (Engine_ == nullptr || WhiteTexture_ != nullptr) return;
|
|
std::array<std::uint8_t, 4> pixels{255, 255, 255, 255};
|
|
void* pixelBytes = std::malloc(pixels.size());
|
|
if (pixelBytes == nullptr) return;
|
|
std::memcpy(pixelBytes, pixels.data(), pixels.size());
|
|
filament::Texture::PixelBufferDescriptor descriptor(pixelBytes, pixels.size(),
|
|
filament::Texture::Format::RGBA, filament::Texture::Type::UBYTE,
|
|
[](void* buffer, std::size_t, void*) { std::free(buffer); }, nullptr);
|
|
WhiteTexture_ = filament::Texture::Builder()
|
|
.width(1)
|
|
.height(1)
|
|
.levels(1)
|
|
.format(filament::Texture::InternalFormat::RGBA8)
|
|
.sampler(filament::Texture::Sampler::SAMPLER_2D)
|
|
.build(*Engine_);
|
|
WhiteTexture_->setImage(*Engine_, 0, std::move(descriptor));
|
|
}
|
|
|
|
WorldTarget& EnsureWorldTarget(filament::Engine& engine, filament::Scene& mainScene, const WorldSurface& surface) {
|
|
auto& target = WorldTargets_[surface.InstanceId];
|
|
if (target.Target != nullptr && (target.Width != surface.Width || target.Height != surface.Height ||
|
|
target.WorldSize != surface.WorldSize || target.MainScene != &mainScene)) {
|
|
DestroyWorldTarget(target);
|
|
}
|
|
if (target.Target != nullptr) return target;
|
|
target.Width = surface.Width;
|
|
target.Height = surface.Height;
|
|
target.WorldSize = surface.WorldSize;
|
|
target.MainScene = &mainScene;
|
|
target.Color = filament::Texture::Builder()
|
|
.width(static_cast<std::uint32_t>(surface.Width))
|
|
.height(static_cast<std::uint32_t>(surface.Height))
|
|
.levels(1)
|
|
.format(filament::Texture::InternalFormat::RGBA8)
|
|
.usage(filament::Texture::Usage::COLOR_ATTACHMENT | filament::Texture::Usage::SAMPLEABLE)
|
|
.sampler(filament::Texture::Sampler::SAMPLER_2D)
|
|
.build(engine);
|
|
target.Target = filament::RenderTarget::Builder()
|
|
.texture(filament::RenderTarget::AttachmentPoint::COLOR, target.Color)
|
|
.build(engine);
|
|
const float halfWidth = std::max(0.001F, surface.WorldSize.x) * 0.5F;
|
|
const float halfHeight = std::max(0.001F, surface.WorldSize.y) * 0.5F;
|
|
const std::array<GpuVertex, 4> vertices{{
|
|
{{-halfWidth, -halfHeight}, {0.0F, 1.0F}, {255, 255, 255, 255}},
|
|
{{ halfWidth, -halfHeight}, {1.0F, 1.0F}, {255, 255, 255, 255}},
|
|
{{ halfWidth, halfHeight}, {1.0F, 0.0F}, {255, 255, 255, 255}},
|
|
{{-halfWidth, halfHeight}, {0.0F, 0.0F}, {255, 255, 255, 255}}
|
|
}};
|
|
const std::array<std::uint32_t, 6> indices{{0, 1, 2, 0, 2, 3}};
|
|
target.VertexBuffer = filament::VertexBuffer::Builder().vertexCount(4).bufferCount(1)
|
|
.attribute(filament::VertexAttribute::POSITION, 0, filament::VertexBuffer::AttributeType::FLOAT2, 0, sizeof(GpuVertex))
|
|
.attribute(filament::VertexAttribute::UV0, 0, filament::VertexBuffer::AttributeType::FLOAT2, sizeof(float) * 2, sizeof(GpuVertex))
|
|
.attribute(filament::VertexAttribute::COLOR, 0, filament::VertexBuffer::AttributeType::UBYTE4, sizeof(float) * 4, sizeof(GpuVertex))
|
|
.normalized(filament::VertexAttribute::COLOR).build(engine);
|
|
target.IndexBuffer = filament::IndexBuffer::Builder().indexCount(6)
|
|
.bufferType(filament::IndexBuffer::IndexType::UINT).build(engine);
|
|
auto* vertexCopy = new std::array<GpuVertex, 4>(vertices);
|
|
auto* indexCopy = new std::array<std::uint32_t, 6>(indices);
|
|
target.VertexBuffer->setBufferAt(engine, 0, filament::VertexBuffer::BufferDescriptor(
|
|
vertexCopy, sizeof(*vertexCopy), [](void* buffer, std::size_t, void*) {
|
|
delete static_cast<std::array<GpuVertex, 4>*>(buffer);
|
|
}, nullptr));
|
|
target.IndexBuffer->setBuffer(engine, filament::IndexBuffer::BufferDescriptor(
|
|
indexCopy, sizeof(*indexCopy), [](void* buffer, std::size_t, void*) {
|
|
delete static_cast<std::array<std::uint32_t, 6>*>(buffer);
|
|
}, nullptr));
|
|
target.MaterialInstance = Material_->createInstance();
|
|
target.MaterialInstance->setParameter("albedo", target.Color, filament::TextureSampler(
|
|
filament::TextureSampler::MinFilter::LINEAR, filament::TextureSampler::MagFilter::LINEAR));
|
|
target.SurfaceEntity = utils::EntityManager::get().create();
|
|
filament::RenderableManager::Builder(1).boundingBox({{0.0F, 0.0F, 0.0F}, {halfWidth, halfHeight, 0.01F}})
|
|
.culling(false).castShadows(false).receiveShadows(false)
|
|
.geometry(0, filament::RenderableManager::PrimitiveType::TRIANGLES,
|
|
target.VertexBuffer, target.IndexBuffer, 0, 6)
|
|
.material(0, target.MaterialInstance).build(engine, target.SurfaceEntity);
|
|
engine.getTransformManager().create(target.SurfaceEntity);
|
|
mainScene.addEntity(target.SurfaceEntity);
|
|
return target;
|
|
}
|
|
|
|
void DestroyWorldTarget(WorldTarget& target) {
|
|
if (Engine_ == nullptr) return;
|
|
if (target.MainScene != nullptr && target.SurfaceEntity) target.MainScene->remove(target.SurfaceEntity);
|
|
if (target.SurfaceEntity) {
|
|
Engine_->getRenderableManager().destroy(target.SurfaceEntity);
|
|
Engine_->getTransformManager().destroy(target.SurfaceEntity);
|
|
utils::EntityManager::get().destroy(target.SurfaceEntity);
|
|
}
|
|
if (target.MaterialInstance != nullptr) Engine_->destroy(target.MaterialInstance);
|
|
if (target.VertexBuffer != nullptr) Engine_->destroy(target.VertexBuffer);
|
|
if (target.IndexBuffer != nullptr) Engine_->destroy(target.IndexBuffer);
|
|
if (target.Target != nullptr) Engine_->destroy(target.Target);
|
|
if (target.Color != nullptr) Engine_->destroy(target.Color);
|
|
target = {};
|
|
}
|
|
|
|
[[nodiscard]] bool CreateFrameGeometry(filament::Engine& engine, const Geometry& geometry, Rml::Vector2f translation) {
|
|
if (geometry.Vertices.empty() || geometry.Indices.empty()) return false;
|
|
std::vector<GpuVertex> translatedVertices = geometry.Vertices;
|
|
for (GpuVertex& vertex : translatedVertices) {
|
|
vertex.Position[0] += translation.x;
|
|
vertex.Position[1] += translation.y;
|
|
}
|
|
|
|
auto* vertexBuffer = filament::VertexBuffer::Builder()
|
|
.vertexCount(static_cast<uint32_t>(translatedVertices.size()))
|
|
.bufferCount(1)
|
|
.attribute(filament::VertexAttribute::POSITION, 0, filament::VertexBuffer::AttributeType::FLOAT2, 0, sizeof(GpuVertex))
|
|
.attribute(filament::VertexAttribute::UV0, 0, filament::VertexBuffer::AttributeType::FLOAT2, sizeof(float) * 2, sizeof(GpuVertex))
|
|
.attribute(filament::VertexAttribute::COLOR, 0, filament::VertexBuffer::AttributeType::UBYTE4, sizeof(float) * 4, sizeof(GpuVertex))
|
|
.normalized(filament::VertexAttribute::COLOR)
|
|
.build(engine);
|
|
auto* indexBuffer = filament::IndexBuffer::Builder()
|
|
.indexCount(static_cast<uint32_t>(geometry.Indices.size()))
|
|
.bufferType(filament::IndexBuffer::IndexType::UINT)
|
|
.build(engine);
|
|
|
|
const std::size_t vertexBytes = translatedVertices.size() * sizeof(GpuVertex);
|
|
void* copiedVertices = std::malloc(vertexBytes);
|
|
const std::size_t indexBytes = geometry.Indices.size() * sizeof(std::uint32_t);
|
|
void* copiedIndices = std::malloc(indexBytes);
|
|
if (copiedVertices == nullptr || copiedIndices == nullptr) {
|
|
std::free(copiedVertices);
|
|
std::free(copiedIndices);
|
|
engine.destroy(vertexBuffer);
|
|
engine.destroy(indexBuffer);
|
|
return false;
|
|
}
|
|
std::memcpy(copiedVertices, translatedVertices.data(), vertexBytes);
|
|
std::memcpy(copiedIndices, geometry.Indices.data(), indexBytes);
|
|
vertexBuffer->setBufferAt(engine, 0, filament::VertexBuffer::BufferDescriptor(copiedVertices, vertexBytes,
|
|
[](void* buffer, std::size_t, void*) { std::free(buffer); }, nullptr));
|
|
indexBuffer->setBuffer(engine, filament::IndexBuffer::BufferDescriptor(copiedIndices, indexBytes,
|
|
[](void* buffer, std::size_t, void*) { std::free(buffer); }, nullptr));
|
|
FrameVertexBuffers_.push_back(vertexBuffer);
|
|
FrameIndexBuffers_.push_back(indexBuffer);
|
|
return true;
|
|
}
|
|
|
|
void ClearFrameResources() {
|
|
if (Engine_ == nullptr) return;
|
|
if (FrameRenderable_) {
|
|
Engine_->getRenderableManager().destroy(FrameRenderable_);
|
|
if (Scene_ != nullptr) Scene_->remove(FrameRenderable_);
|
|
utils::EntityManager::get().destroy(FrameRenderable_);
|
|
FrameRenderable_ = {};
|
|
}
|
|
for (filament::MaterialInstance* instance : FrameMaterialInstances_) Engine_->destroy(instance);
|
|
for (filament::VertexBuffer* buffer : FrameVertexBuffers_) Engine_->destroy(buffer);
|
|
for (filament::IndexBuffer* buffer : FrameIndexBuffers_) Engine_->destroy(buffer);
|
|
FrameMaterialInstances_.clear();
|
|
FrameVertexBuffers_.clear();
|
|
FrameIndexBuffers_.clear();
|
|
}
|
|
|
|
std::filesystem::path UiRoot_;
|
|
std::unordered_map<Rml::CompiledGeometryHandle, Geometry> Geometries_;
|
|
std::unordered_map<Rml::TextureHandle, Texture> Textures_;
|
|
std::vector<DrawCommand> DrawCommands_;
|
|
std::unordered_map<std::uint64_t, std::vector<DrawCommand>> WorldDrawCommands_;
|
|
std::uint64_t ActiveWorldInstance_ = 0;
|
|
std::unordered_map<std::uint64_t, WorldTarget> WorldTargets_;
|
|
Rml::CompiledGeometryHandle NextGeometryHandle_ = 1;
|
|
Rml::TextureHandle NextTextureHandle_ = 1;
|
|
bool ScissorEnabled_ = false;
|
|
Rml::Rectanglei Scissor_{};
|
|
|
|
filament::Engine* Engine_ = nullptr;
|
|
filament::Scene* Scene_ = nullptr;
|
|
filament::View* View_ = nullptr;
|
|
filament::Camera* Camera_ = nullptr;
|
|
filament::Material* Material_ = nullptr;
|
|
filament::Texture* WhiteTexture_ = nullptr;
|
|
utils::Entity FrameRenderable_{};
|
|
std::vector<filament::MaterialInstance*> FrameMaterialInstances_;
|
|
std::vector<filament::VertexBuffer*> FrameVertexBuffers_;
|
|
std::vector<filament::IndexBuffer*> FrameIndexBuffers_;
|
|
};
|
|
|
|
class MetaCoreRmlFileInterface final : public Rml::FileInterface {
|
|
public:
|
|
explicit MetaCoreRmlFileInterface(std::filesystem::path root) : Root_(std::move(root)) {}
|
|
|
|
Rml::FileHandle Open(const Rml::String& path) override {
|
|
std::filesystem::path candidate(path);
|
|
if (candidate.is_relative()) candidate = Root_ / candidate;
|
|
if (!IsPathInsideRoot(Root_, candidate)) return 0;
|
|
auto* file = new std::ifstream(candidate, std::ios::binary);
|
|
if (!file->is_open()) { delete file; return 0; }
|
|
return reinterpret_cast<Rml::FileHandle>(file);
|
|
}
|
|
void Close(Rml::FileHandle file) override { delete reinterpret_cast<std::ifstream*>(file); }
|
|
size_t Read(void* buffer, size_t size, Rml::FileHandle file) override {
|
|
auto& input = *reinterpret_cast<std::ifstream*>(file);
|
|
input.read(static_cast<char*>(buffer), static_cast<std::streamsize>(size));
|
|
return static_cast<size_t>(input.gcount());
|
|
}
|
|
bool Seek(Rml::FileHandle file, long offset, int origin) override {
|
|
auto& input = *reinterpret_cast<std::ifstream*>(file);
|
|
input.seekg(offset, static_cast<std::ios_base::seekdir>(origin));
|
|
return input.good();
|
|
}
|
|
size_t Tell(Rml::FileHandle file) override {
|
|
return static_cast<size_t>(reinterpret_cast<std::ifstream*>(file)->tellg());
|
|
}
|
|
private:
|
|
std::filesystem::path Root_;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
class MetaCoreRuntimeUiSystem::Impl {
|
|
public:
|
|
struct Provider {
|
|
ProviderReadCallback Reader{};
|
|
ProviderWriteCallback Writer{};
|
|
};
|
|
|
|
struct DocumentState {
|
|
MetaCoreRuntimeUiDocumentEntry Entry{};
|
|
Rml::Context* Context = nullptr;
|
|
std::string ContextName{};
|
|
Rml::ElementDocument* Document = nullptr;
|
|
std::string DefaultLocale{"en-US"};
|
|
std::string FallbackLocale{"en-US"};
|
|
std::unordered_map<std::string, std::unordered_map<std::string, std::string>> Localization{};
|
|
MetaCoreUiDocument CanvasSettings{};
|
|
};
|
|
|
|
static std::pair<std::string, std::string_view> SplitProviderPath(std::string_view path) {
|
|
const std::size_t separator = path.find('.');
|
|
if (separator == std::string_view::npos) return {std::string(path), {}};
|
|
return {std::string(path.substr(0, separator)), path.substr(separator + 1)};
|
|
}
|
|
|
|
class EventListener final : public Rml::EventListener {
|
|
public:
|
|
EventListener(Impl& owner, std::string documentId) : Owner_(owner), DocumentId_(std::move(documentId)) {}
|
|
void ProcessEvent(Rml::Event& event) override {
|
|
Rml::Element* target = event.GetTargetElement();
|
|
if (target == nullptr) return;
|
|
const std::string action = target->GetAttribute<Rml::String>("data-metacore-action", "");
|
|
MetaCoreRuntimeUiEvent uiEvent;
|
|
const auto state = std::find_if(Owner_.Documents_.begin(), Owner_.Documents_.end(), [&](const DocumentState& value) {
|
|
return value.Entry.DocumentId == DocumentId_;
|
|
});
|
|
if (state != Owner_.Documents_.end()) {
|
|
uiEvent.InstanceId = state->Entry.InstanceId;
|
|
uiEvent.DocumentAssetGuid = state->Entry.UiDocumentAssetGuid;
|
|
uiEvent.SceneObjectId = state->Entry.SceneObjectId;
|
|
}
|
|
uiEvent.DocumentId = DocumentId_;
|
|
uiEvent.ElementId = target->GetId();
|
|
uiEvent.Action = action;
|
|
uiEvent.Type = event.GetType();
|
|
uiEvent.Value = target->GetAttribute<Rml::String>("value", "");
|
|
uiEvent.EventType = EventTypeFromRml(uiEvent.Type);
|
|
const std::string kind = target->GetAttribute<Rml::String>("data-metacore-kind", "");
|
|
if (uiEvent.EventType == MetaCoreRuntimeUiEventType::ValueChanged && kind == "list") {
|
|
uiEvent.EventType = MetaCoreRuntimeUiEventType::SelectionChanged;
|
|
if (const auto* select = dynamic_cast<Rml::ElementFormControlSelect*>(target))
|
|
uiEvent.Value = std::to_string(select->GetSelection());
|
|
else uiEvent.Value = target->GetAttribute<Rml::String>("data-metacore-selected-index", "-1");
|
|
target->SetAttribute("data-metacore-selected-index", uiEvent.Value);
|
|
}
|
|
uiEvent.InputSource = Owner_.CurrentInputSource_;
|
|
uiEvent.Consumed = !event.IsPropagating();
|
|
const char* bindingTarget = uiEvent.EventType == MetaCoreRuntimeUiEventType::SelectionChanged
|
|
? "selected-index" : "value";
|
|
const std::string writePath = target->GetAttribute<Rml::String>(
|
|
std::string("data-metacore-bind-") + bindingTarget, "");
|
|
const std::string writeMode = target->GetAttribute<Rml::String>(
|
|
std::string("data-metacore-bind-mode-") + bindingTarget, "");
|
|
if ((uiEvent.EventType == MetaCoreRuntimeUiEventType::ValueChanged ||
|
|
uiEvent.EventType == MetaCoreRuntimeUiEventType::SelectionChanged) &&
|
|
writeMode == "two-way" && !writePath.empty()) {
|
|
uiEvent.Action = writePath;
|
|
const auto previous = Owner_.Values_.find(writePath);
|
|
if (previous != Owner_.Values_.end()) {
|
|
uiEvent.OldValue = FormatUiValue(previous->second, {});
|
|
uiEvent.OldTypedValue = previous->second;
|
|
}
|
|
const MetaCoreUiInputValueType inputType =
|
|
uiEvent.EventType == MetaCoreRuntimeUiEventType::SelectionChanged
|
|
? MetaCoreUiInputValueType::Integer
|
|
: static_cast<MetaCoreUiInputValueType>(
|
|
target->GetAttribute<int>("data-metacore-input-type", 0));
|
|
if (const auto value = ParseUiValue(uiEvent.Value, inputType); value.has_value()) {
|
|
uiEvent.NewTypedValue = *value;
|
|
const auto [providerName, relativePath] = Impl::SplitProviderPath(writePath);
|
|
const auto provider = Owner_.Providers_.find(providerName);
|
|
const bool providerKnown = provider != Owner_.Providers_.end();
|
|
const bool providerWritable = providerKnown && static_cast<bool>(provider->second.Writer);
|
|
const bool writeSucceeded = providerWritable && provider->second.Writer(relativePath, *value);
|
|
if ((providerKnown && !writeSucceeded) ||
|
|
(!providerKnown && !writePath.starts_with("script."))) {
|
|
uiEvent.EventType = MetaCoreRuntimeUiEventType::WriteRequest;
|
|
} else {
|
|
Owner_.Values_[writePath] = *value;
|
|
}
|
|
} else {
|
|
Owner_.Diagnostics_.push_back({
|
|
DocumentId_, target->GetId(), writePath, "UI value failed basic type validation", true
|
|
});
|
|
}
|
|
}
|
|
if (uiEvent.Action.empty() &&
|
|
uiEvent.EventType != MetaCoreRuntimeUiEventType::Focus &&
|
|
uiEvent.EventType != MetaCoreRuntimeUiEventType::Blur &&
|
|
uiEvent.EventType != MetaCoreRuntimeUiEventType::PointerEnter &&
|
|
uiEvent.EventType != MetaCoreRuntimeUiEventType::PointerLeave &&
|
|
uiEvent.EventType != MetaCoreRuntimeUiEventType::Scroll) return;
|
|
Owner_.Events_.push_back(uiEvent);
|
|
if (Owner_.Callback_) Owner_.Callback_(uiEvent);
|
|
}
|
|
private:
|
|
Impl& Owner_;
|
|
std::string DocumentId_;
|
|
};
|
|
|
|
MetaCoreWindow* Window_ = nullptr;
|
|
MetaCoreEditorViewportRenderer* ViewportRenderer_ = nullptr;
|
|
std::filesystem::path UiRoot_;
|
|
MetaCoreRmlFilamentRenderInterface RenderInterface_;
|
|
std::unique_ptr<MetaCoreRmlFileInterface> FileInterface_;
|
|
std::unique_ptr<MetaCoreRmlSystemInterface> SystemInterface_;
|
|
Rml::Context* Context_ = nullptr;
|
|
std::vector<DocumentState> Documents_;
|
|
std::vector<std::unique_ptr<EventListener>> Listeners_;
|
|
std::unordered_map<std::string, MetaCoreRuntimeUiValue> Values_;
|
|
std::unordered_map<std::string, std::vector<MetaCoreRuntimeUiRow>> Lists_;
|
|
std::unordered_map<std::string, Provider> Providers_;
|
|
std::unordered_set<std::string> ProviderPaths_;
|
|
std::vector<MetaCoreRuntimeUiEvent> Events_;
|
|
std::vector<std::string> Errors_;
|
|
std::vector<MetaCoreUiDiagnostic> Diagnostics_;
|
|
EventCallback Callback_;
|
|
MetaCoreRuntimeUiInputSource CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Programmatic;
|
|
std::string Locale_{"en-US"};
|
|
std::string Theme_{};
|
|
bool Initialized_ = false;
|
|
|
|
void RefreshProviders() {
|
|
std::vector<std::string> changed;
|
|
for (const std::string& path : ProviderPaths_) {
|
|
const auto [providerName, relativePath] = SplitProviderPath(path);
|
|
const auto provider = Providers_.find(providerName);
|
|
if (provider == Providers_.end() || !provider->second.Reader) continue;
|
|
const auto value = provider->second.Reader(relativePath);
|
|
if (!value.has_value()) {
|
|
if (std::none_of(Diagnostics_.begin(), Diagnostics_.end(), [&](const MetaCoreUiDiagnostic& diagnostic) {
|
|
return diagnostic.BindingPath == path && diagnostic.Message == "Binding provider source is unavailable";
|
|
})) Diagnostics_.push_back({{}, {}, path, "Binding provider source is unavailable", true});
|
|
continue;
|
|
}
|
|
const auto previous = Values_.find(path);
|
|
if (previous == Values_.end() || !UiValuesEqual(previous->second, *value)) {
|
|
Values_[path] = *value;
|
|
changed.push_back(path);
|
|
}
|
|
}
|
|
for (const std::string& path : changed) ApplyBindings(&path);
|
|
}
|
|
|
|
[[nodiscard]] bool HasInputEnabledDocument() const {
|
|
return std::any_of(Documents_.begin(), Documents_.end(), [](const DocumentState& state) {
|
|
return state.Entry.Visible && state.Entry.InputEnabled && state.Document != nullptr;
|
|
});
|
|
}
|
|
|
|
bool NavigateFocus(int direction, Rml::Context* restrictedContext = nullptr) {
|
|
struct Candidate {
|
|
std::int32_t Layer = 0;
|
|
int Order = 0;
|
|
std::size_t Sequence = 0;
|
|
Rml::Element* Element = nullptr;
|
|
};
|
|
std::vector<Candidate> candidates;
|
|
std::size_t sequence = 0;
|
|
for (const DocumentState& state : Documents_) {
|
|
if (restrictedContext != nullptr && state.Context != restrictedContext) continue;
|
|
if (!state.Entry.Visible || !state.Entry.InputEnabled || state.Document == nullptr) continue;
|
|
Rml::ElementList elements;
|
|
state.Document->QuerySelectorAll(elements, "[tabindex]");
|
|
for (Rml::Element* element : elements) {
|
|
if (!element->IsVisible(true) || element->HasAttribute("disabled")) continue;
|
|
candidates.push_back({
|
|
state.Entry.Layer,
|
|
element->GetAttribute<int>("tabindex", 100000),
|
|
sequence++,
|
|
element
|
|
});
|
|
}
|
|
}
|
|
if (candidates.empty()) {
|
|
for (const DocumentState& state : Documents_)
|
|
if (state.Context != nullptr && state.Context->GetFocusElement() != nullptr)
|
|
state.Context->GetFocusElement()->Blur();
|
|
return false;
|
|
}
|
|
std::stable_sort(candidates.begin(), candidates.end(), [](const Candidate& lhs, const Candidate& rhs) {
|
|
if (lhs.Layer != rhs.Layer) return lhs.Layer > rhs.Layer;
|
|
if (lhs.Order != rhs.Order) return lhs.Order < rhs.Order;
|
|
return lhs.Sequence < rhs.Sequence;
|
|
});
|
|
Rml::Element* focused = nullptr;
|
|
for (const DocumentState& state : Documents_)
|
|
if (state.Context != nullptr && state.Context->GetFocusElement() != nullptr)
|
|
focused = state.Context->GetFocusElement();
|
|
auto current = std::find_if(candidates.begin(), candidates.end(), [&](const Candidate& value) {
|
|
return value.Element == focused;
|
|
});
|
|
std::ptrdiff_t index = current == candidates.end() ? (direction > 0 ? -1 : 0) :
|
|
std::distance(candidates.begin(), current);
|
|
index = (index + direction + static_cast<std::ptrdiff_t>(candidates.size())) %
|
|
static_cast<std::ptrdiff_t>(candidates.size());
|
|
candidates[static_cast<std::size_t>(index)].Element->Focus();
|
|
return true;
|
|
}
|
|
|
|
void ApplyLocalization(DocumentState& state) {
|
|
if (state.Document == nullptr || state.Localization.empty()) return;
|
|
const auto findText = [&](std::string_view key) -> std::optional<std::string> {
|
|
for (const std::string* locale : {&Locale_, &state.FallbackLocale, &state.DefaultLocale}) {
|
|
const auto localeValues = state.Localization.find(*locale);
|
|
if (localeValues == state.Localization.end()) continue;
|
|
const auto value = localeValues->second.find(std::string(key));
|
|
if (value != localeValues->second.end()) return value->second;
|
|
}
|
|
return std::nullopt;
|
|
};
|
|
Rml::ElementList elements;
|
|
state.Document->QuerySelectorAll(elements, "[data-metacore-loc]");
|
|
for (Rml::Element* element : elements) {
|
|
const std::string key = element->GetAttribute<Rml::String>("data-metacore-loc", "");
|
|
if (const auto value = findText(key); value.has_value()) element->SetInnerRML(EscapeRmlText(*value));
|
|
else Diagnostics_.push_back({state.Entry.DocumentId, element->GetId(), key,
|
|
"Localization key cannot be resolved for locale " + Locale_, true});
|
|
}
|
|
}
|
|
|
|
void ApplyCanvasLayouts(int framebufferWidth, int framebufferHeight) {
|
|
if (Window_ == nullptr) return;
|
|
const auto [windowWidth, windowHeight] = Window_->GetWindowSize();
|
|
const float dpiScaleX = static_cast<float>(framebufferWidth) / static_cast<float>(std::max(1, windowWidth));
|
|
const float dpiScaleY = static_cast<float>(framebufferHeight) / static_cast<float>(std::max(1, windowHeight));
|
|
for (DocumentState& state : Documents_) {
|
|
if (state.Document == nullptr) continue;
|
|
const float width = state.Entry.RenderMode == 1U && state.Entry.TargetWidth > 0
|
|
? static_cast<float>(state.Entry.TargetWidth) : static_cast<float>(framebufferWidth);
|
|
const float height = state.Entry.RenderMode == 1U && state.Entry.TargetHeight > 0
|
|
? static_cast<float>(state.Entry.TargetHeight) : static_cast<float>(framebufferHeight);
|
|
const MetaCoreUiCanvasLayout layout = MetaCoreComputeUiCanvasLayout(state.CanvasSettings, {
|
|
width, height, 96.0F * std::max(dpiScaleX, dpiScaleY), 0.0F, 0.0F, 0.0F, 0.0F
|
|
});
|
|
Rml::Element* body = state.Document->QuerySelector("body");
|
|
if (body == nullptr) continue;
|
|
body->SetProperty("position", "absolute");
|
|
body->SetProperty("left", std::to_string(layout.ViewportLeft) + "px");
|
|
body->SetProperty("top", std::to_string(layout.ViewportTop) + "px");
|
|
body->SetProperty("width", std::to_string(layout.LogicalWidth) + "px");
|
|
body->SetProperty("height", std::to_string(layout.LogicalHeight) + "px");
|
|
body->SetProperty("transform-origin", "0 0");
|
|
body->SetProperty("transform", "scale(" + std::to_string(layout.Scale) + ")");
|
|
}
|
|
}
|
|
|
|
void ApplyBindings(const std::string* onlyPath = nullptr) {
|
|
bool focusInvalidated = false;
|
|
for (DocumentState& state : Documents_) {
|
|
if (state.Document == nullptr) continue;
|
|
for (const std::string target : {"text", "value", "visible", "enabled", "selected-index"}) {
|
|
Rml::ElementList elements;
|
|
state.Document->QuerySelectorAll(elements, "[data-metacore-bind-" + target + "]");
|
|
for (Rml::Element* element : elements) {
|
|
const std::string path = element->GetAttribute<Rml::String>("data-metacore-bind-" + target, "");
|
|
if (onlyPath != nullptr && path != *onlyPath) continue;
|
|
const auto value = Values_.find(path);
|
|
if (value == Values_.end()) {
|
|
const std::string fallback = element->GetAttribute<Rml::String>("data-metacore-fallback-" + target, "");
|
|
if (fallback.empty()) continue;
|
|
if (target == "text") element->SetInnerRML(EscapeRmlText(fallback));
|
|
else element->SetAttribute(target == "enabled" ? "disabled" : target, fallback);
|
|
continue;
|
|
}
|
|
const std::string converter = element->GetAttribute<Rml::String>(
|
|
"data-metacore-converter-" + target, "");
|
|
const auto converted = ConvertUiValue(value->second, converter);
|
|
const bool targetTypeValid = converted.has_value() &&
|
|
((target != "visible" && target != "enabled") ||
|
|
converted->Type == MetaCoreRuntimeUiValueType::Bool) &&
|
|
(target != "selected-index" ||
|
|
converted->Type == MetaCoreRuntimeUiValueType::Int64);
|
|
if (!targetTypeValid) {
|
|
const std::string message = "Binding conversion or target type is invalid";
|
|
if (std::none_of(Diagnostics_.begin(), Diagnostics_.end(), [&](const MetaCoreUiDiagnostic& diagnostic) {
|
|
return diagnostic.Document == state.Entry.DocumentId &&
|
|
diagnostic.NodeId == element->GetId() && diagnostic.BindingPath == path &&
|
|
diagnostic.Message == message;
|
|
})) Diagnostics_.push_back({state.Entry.DocumentId, element->GetId(), path, message, true});
|
|
continue;
|
|
}
|
|
const std::string format = element->GetAttribute<Rml::String>("data-metacore-format-" + target, "");
|
|
const std::string text = FormatUiValue(*converted, format);
|
|
if (target == "text") element->SetInnerRML(EscapeRmlText(text));
|
|
else if (target == "value") element->SetAttribute("value", text);
|
|
else if (target == "visible") {
|
|
if (converted->BoolValue) element->RemoveAttribute("hidden"); else element->SetAttribute("hidden", "true");
|
|
} else if (target == "enabled") {
|
|
if (converted->BoolValue) {
|
|
element->RemoveAttribute("disabled");
|
|
} else {
|
|
element->SetAttribute("disabled", "true");
|
|
if (state.Context != nullptr && state.Context->GetFocusElement() == element) {
|
|
element->Blur();
|
|
focusInvalidated = true;
|
|
}
|
|
}
|
|
} else element->SetAttribute("data-metacore-selected-index", text);
|
|
}
|
|
}
|
|
Rml::ElementList legacyElements;
|
|
state.Document->QuerySelectorAll(legacyElements, "[data-metacore-bind]");
|
|
for (Rml::Element* element : legacyElements) {
|
|
const std::string path = element->GetAttribute<Rml::String>("data-metacore-bind", "");
|
|
if (onlyPath != nullptr && path != *onlyPath) continue;
|
|
const auto value = Values_.find(path);
|
|
if (value == Values_.end()) continue;
|
|
element->SetInnerRML(EscapeRmlText(FormatUiValue(value->second,
|
|
element->GetAttribute<Rml::String>("data-metacore-format", ""))));
|
|
}
|
|
}
|
|
if (focusInvalidated) (void)NavigateFocus(1);
|
|
}
|
|
|
|
bool LoadEntry(const MetaCoreRuntimeUiDocumentEntry& entry) {
|
|
if (Context_ == nullptr) return false;
|
|
std::filesystem::path relativePath = entry.RmlPath;
|
|
if (relativePath.empty() && entry.UiDocumentAssetGuid.IsValid())
|
|
relativePath = std::filesystem::path("Compiled") / entry.UiDocumentAssetGuid.ToString() / "document.rml";
|
|
const std::filesystem::path absolutePath = UiRoot_ / relativePath;
|
|
if (entry.DocumentId.empty() || relativePath.empty() || !IsPathInsideRoot(UiRoot_, absolutePath)) {
|
|
Errors_.push_back("Invalid UI manifest entry: " + entry.DocumentId);
|
|
return false;
|
|
}
|
|
DocumentState state;
|
|
state.Entry = entry;
|
|
state.Context = Context_;
|
|
if (entry.RenderMode == 1U) {
|
|
state.ContextName = "MetaCoreWorldUi-" + std::to_string(entry.InstanceId);
|
|
state.Context = Rml::CreateContext(state.ContextName, {
|
|
std::max(1, entry.TargetWidth), std::max(1, entry.TargetHeight)
|
|
});
|
|
if (state.Context == nullptr) {
|
|
Errors_.push_back("Failed to create World Space RmlUi context: " + entry.DocumentId);
|
|
return false;
|
|
}
|
|
}
|
|
const std::filesystem::path documentDirectory = absolutePath.parent_path();
|
|
const std::filesystem::path localizationPath = documentDirectory / "localization.json";
|
|
if (std::filesystem::is_regular_file(localizationPath)) {
|
|
try {
|
|
std::ifstream input(localizationPath);
|
|
nlohmann::json root;
|
|
input >> root;
|
|
state.DefaultLocale = root.value("default", "en-US");
|
|
state.FallbackLocale = root.value("fallback", state.DefaultLocale);
|
|
if (root.contains("locales") && root["locales"].is_object()) {
|
|
for (auto locale = root["locales"].begin(); locale != root["locales"].end(); ++locale) {
|
|
if (!locale.value().is_object()) continue;
|
|
auto& values = state.Localization[locale.key()];
|
|
for (auto value = locale.value().begin(); value != locale.value().end(); ++value)
|
|
if (value.value().is_string()) values[value.key()] = value.value().get<std::string>();
|
|
}
|
|
}
|
|
} catch (...) {
|
|
Diagnostics_.push_back({entry.DocumentId, {}, {}, "Compiled localization data is invalid", true});
|
|
return false;
|
|
}
|
|
}
|
|
const std::filesystem::path fontDirectory = documentDirectory / "Assets";
|
|
std::error_code fontError;
|
|
if (std::filesystem::is_directory(fontDirectory, fontError)) {
|
|
std::vector<std::filesystem::path> fonts;
|
|
for (std::filesystem::directory_iterator iterator(fontDirectory, fontError), end;
|
|
!fontError && iterator != end; iterator.increment(fontError)) {
|
|
if (iterator->is_regular_file() && iterator->path().extension() == ".font") fonts.push_back(iterator->path());
|
|
}
|
|
std::sort(fonts.begin(), fonts.end());
|
|
bool fallback = false;
|
|
for (const auto& font : fonts) {
|
|
if (!Rml::LoadFontFace(font.string(), fallback)) {
|
|
Diagnostics_.push_back({entry.DocumentId, {}, {}, "Failed to load font: " + font.filename().string(), true});
|
|
}
|
|
fallback = true;
|
|
}
|
|
}
|
|
Rml::ElementDocument* document = state.Context->LoadDocument(absolutePath.string());
|
|
if (document == nullptr) {
|
|
Errors_.push_back("Failed to load RML: " + relativePath.generic_string());
|
|
if (!state.ContextName.empty()) (void)Rml::RemoveContext(state.ContextName);
|
|
return false;
|
|
}
|
|
state.Document = document;
|
|
for (const char* target : {"text", "value", "visible", "enabled", "items", "selected-index"}) {
|
|
Rml::ElementList boundElements;
|
|
document->QuerySelectorAll(boundElements, std::string("[data-metacore-bind-") + target + "]");
|
|
for (Rml::Element* element : boundElements) {
|
|
const std::string path = element->GetAttribute<Rml::String>(
|
|
std::string("data-metacore-bind-") + target, "");
|
|
if (!path.empty()) ProviderPaths_.insert(path);
|
|
}
|
|
}
|
|
if (Rml::Element* body = document->QuerySelector("body"); body != nullptr) {
|
|
const std::string reference = body->GetAttribute<Rml::String>("data-metacore-reference", "1920x1080");
|
|
const std::size_t separator = reference.find('x');
|
|
if (separator != std::string::npos) {
|
|
state.CanvasSettings.ReferenceWidth = std::max(1, std::atoi(reference.substr(0, separator).c_str()));
|
|
state.CanvasSettings.ReferenceHeight = std::max(1, std::atoi(reference.substr(separator + 1).c_str()));
|
|
}
|
|
state.CanvasSettings.ScaleMode = static_cast<MetaCoreUiCanvasScaleMode>(
|
|
body->GetAttribute<int>("data-metacore-scale-mode", 1));
|
|
state.CanvasSettings.MatchWidthOrHeight = body->GetAttribute<float>("data-metacore-match", 0.5F);
|
|
state.CanvasSettings.ReferenceDpi = body->GetAttribute<float>("data-metacore-reference-dpi", 96.0F);
|
|
state.CanvasSettings.MinScale = body->GetAttribute<float>("data-metacore-min-scale", 0.5F);
|
|
state.CanvasSettings.MaxScale = body->GetAttribute<float>("data-metacore-max-scale", 4.0F);
|
|
state.CanvasSettings.MinAspectRatio = body->GetAttribute<float>("data-metacore-min-aspect", 0.0F);
|
|
state.CanvasSettings.MaxAspectRatio = body->GetAttribute<float>("data-metacore-max-aspect", 0.0F);
|
|
state.CanvasSettings.UseSafeArea =
|
|
body->GetAttribute<Rml::String>("data-metacore-safe-area", "true") == "true";
|
|
}
|
|
if (entry.Visible) document->Show(); else document->Hide();
|
|
Listeners_.push_back(std::make_unique<EventListener>(*this, entry.DocumentId));
|
|
for (const char* type : {"mouseover", "mouseout", "click", "submit", "change", "scroll", "focus", "blur"})
|
|
document->AddEventListener(type, Listeners_.back().get());
|
|
Documents_.push_back(std::move(state));
|
|
ApplyLocalization(Documents_.back());
|
|
return true;
|
|
}
|
|
};
|
|
|
|
MetaCoreRuntimeUiSystem::MetaCoreRuntimeUiSystem() : Impl_(std::make_unique<Impl>()) {}
|
|
MetaCoreRuntimeUiSystem::~MetaCoreRuntimeUiSystem() { Shutdown(); }
|
|
|
|
bool MetaCoreRuntimeUiSystem::Initialize(MetaCoreWindow& window, const std::filesystem::path&, const std::filesystem::path& uiRoot, const MetaCoreRuntimeUiManifest& manifest) {
|
|
Shutdown();
|
|
if (uiRoot.empty() || !std::filesystem::is_directory(uiRoot)) return false;
|
|
if (manifest.SchemaVersion > METACORE_UI_SCHEMA_VERSION ||
|
|
manifest.GeneratorVersion > METACORE_UI_GENERATOR_VERSION) {
|
|
Impl_->Errors_.push_back("Runtime UI manifest version is newer than this engine");
|
|
return false;
|
|
}
|
|
Impl_->Window_ = &window;
|
|
Impl_->UiRoot_ = std::filesystem::weakly_canonical(uiRoot);
|
|
Impl_->RenderInterface_.SetUiRoot(Impl_->UiRoot_);
|
|
Impl_->FileInterface_ = std::make_unique<MetaCoreRmlFileInterface>(Impl_->UiRoot_);
|
|
Impl_->SystemInterface_ = std::make_unique<MetaCoreRmlSystemInterface>(window);
|
|
Rml::SetRenderInterface(&Impl_->RenderInterface_);
|
|
Rml::SetFileInterface(Impl_->FileInterface_.get());
|
|
Rml::SetSystemInterface(Impl_->SystemInterface_.get());
|
|
if (!Rml::Initialise()) return false;
|
|
const auto [width, height] = window.GetFramebufferSize();
|
|
Impl_->Context_ = Rml::CreateContext("MetaCoreRuntime", {width, height});
|
|
if (Impl_->Context_ == nullptr) {
|
|
Rml::Shutdown();
|
|
return false;
|
|
}
|
|
|
|
std::vector<MetaCoreRuntimeUiDocumentEntry> entries = manifest.Documents;
|
|
std::sort(entries.begin(), entries.end(), [](const auto& lhs, const auto& rhs) { return lhs.Layer < rhs.Layer; });
|
|
std::uint64_t generatedInstanceId = 1;
|
|
for (MetaCoreRuntimeUiDocumentEntry& entry : entries) {
|
|
if (!entry.UiDocumentAssetGuid.IsValid() && !entry.UiDocumentAssetGuidString.empty())
|
|
if (const auto parsed = MetaCoreAssetGuid::Parse(entry.UiDocumentAssetGuidString); parsed.has_value())
|
|
entry.UiDocumentAssetGuid = *parsed;
|
|
if (entry.InstanceId == 0) entry.InstanceId = generatedInstanceId++;
|
|
(void)Impl_->LoadEntry(entry);
|
|
}
|
|
Impl_->Initialized_ = true;
|
|
return true;
|
|
}
|
|
|
|
void MetaCoreRuntimeUiSystem::AttachToViewportRenderer(MetaCoreEditorViewportRenderer& viewportRenderer) {
|
|
Impl_->ViewportRenderer_ = &viewportRenderer;
|
|
viewportRenderer.SetRuntimeOverlayRenderCallback([this](filament::Engine& engine, filament::Renderer& renderer, uint32_t width, uint32_t height) {
|
|
if (Impl_ && Impl_->Initialized_) Impl_->RenderInterface_.Render(engine, renderer, width, height);
|
|
});
|
|
viewportRenderer.SetRuntimeWorldUiRenderCallback([this, &viewportRenderer](
|
|
filament::Engine& engine, filament::Renderer& renderer, filament::Scene& scene) {
|
|
if (!Impl_ || !Impl_->Initialized_) return;
|
|
std::vector<MetaCoreRmlFilamentRenderInterface::WorldSurface> surfaces;
|
|
for (const auto& state : Impl_->Documents_) {
|
|
if (state.Entry.RenderMode != 1U || !state.Entry.Visible || state.Document == nullptr) continue;
|
|
surfaces.push_back({
|
|
state.Entry.InstanceId,
|
|
state.Entry.SceneObjectId,
|
|
std::max(1, state.Entry.TargetWidth),
|
|
std::max(1, state.Entry.TargetHeight),
|
|
state.Entry.WorldSize
|
|
});
|
|
}
|
|
Impl_->RenderInterface_.RenderWorldSurfaces(engine, renderer, scene, surfaces,
|
|
[&viewportRenderer](std::uint64_t objectId, glm::mat4& matrix) {
|
|
return viewportRenderer.TryGetObjectWorldMatrix(objectId, matrix);
|
|
});
|
|
});
|
|
}
|
|
|
|
void MetaCoreRuntimeUiSystem::Shutdown() {
|
|
if (!Impl_) return;
|
|
if (Impl_->ViewportRenderer_ != nullptr) {
|
|
Impl_->ViewportRenderer_->SetRuntimeOverlayRenderCallback({});
|
|
Impl_->ViewportRenderer_->SetRuntimeWorldUiRenderCallback({});
|
|
Impl_->ViewportRenderer_ = nullptr;
|
|
}
|
|
if (Impl_->Initialized_) Rml::Shutdown();
|
|
Impl_->RenderInterface_.Shutdown();
|
|
Impl_->Documents_.clear();
|
|
Impl_->Listeners_.clear();
|
|
Impl_->Context_ = nullptr;
|
|
Impl_->FileInterface_.reset();
|
|
Impl_->SystemInterface_.reset();
|
|
Impl_->Values_.clear();
|
|
Impl_->Lists_.clear();
|
|
Impl_->Providers_.clear();
|
|
Impl_->ProviderPaths_.clear();
|
|
Impl_->Diagnostics_.clear();
|
|
Impl_->Initialized_ = false;
|
|
}
|
|
|
|
void MetaCoreRuntimeUiSystem::Update(float) {
|
|
if (!Impl_->Initialized_ || Impl_->Context_ == nullptr || Impl_->Window_ == nullptr) return;
|
|
Impl_->RefreshProviders();
|
|
const auto [width, height] = Impl_->Window_->GetFramebufferSize();
|
|
Impl_->Context_->SetDimensions({width, height});
|
|
Impl_->ApplyCanvasLayouts(width, height);
|
|
Impl_->RenderInterface_.BeginFrame();
|
|
Impl_->Context_->Update();
|
|
for (auto& worldState : Impl_->Documents_) {
|
|
if (worldState.Entry.RenderMode != 1U || !worldState.Entry.Visible ||
|
|
worldState.Document == nullptr || worldState.Context == nullptr) continue;
|
|
worldState.Context->SetDimensions({
|
|
std::max(1, worldState.Entry.TargetWidth),
|
|
std::max(1, worldState.Entry.TargetHeight)
|
|
});
|
|
worldState.Context->Update();
|
|
Impl_->RenderInterface_.BeginWorld(worldState.Entry.InstanceId);
|
|
worldState.Context->Render();
|
|
Impl_->RenderInterface_.EndWorld();
|
|
}
|
|
Impl_->Context_->Render();
|
|
}
|
|
|
|
MetaCoreInputConsumption MetaCoreRuntimeUiSystem::ProcessInput() {
|
|
MetaCoreInputConsumption consumed;
|
|
if (!Impl_->Initialized_ || Impl_->Context_ == nullptr || Impl_->Window_ == nullptr || !Impl_->HasInputEnabledDocument()) return consumed;
|
|
MetaCoreInput& input = Impl_->Window_->GetInput();
|
|
Rml::Context* keyboardContext = Impl_->Context_;
|
|
for (const auto& state : Impl_->Documents_)
|
|
if (state.Entry.RenderMode == 1U && state.Context != nullptr &&
|
|
state.Context->GetFocusElement() != nullptr) keyboardContext = state.Context;
|
|
const glm::vec2 cursor = input.GetCursorPosition();
|
|
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Mouse;
|
|
const bool hasScreenUi = std::any_of(Impl_->Documents_.begin(), Impl_->Documents_.end(), [](const auto& state) {
|
|
return state.Entry.RenderMode == 0U && state.Entry.Visible && state.Entry.InputEnabled && state.Document != nullptr;
|
|
});
|
|
if (hasScreenUi) consumed.Mouse = !Impl_->Context_->ProcessMouseMove(static_cast<int>(cursor.x), static_cast<int>(cursor.y), 0);
|
|
for (int button = 0; button < 3; ++button) {
|
|
const auto mouseButton = static_cast<MetaCoreMouseButton>(button);
|
|
if (hasScreenUi && input.WasMouseButtonPressed(mouseButton)) consumed.Mouse = !Impl_->Context_->ProcessMouseButtonDown(button, 0) || consumed.Mouse;
|
|
if (hasScreenUi && input.WasMouseButtonReleased(mouseButton)) consumed.Mouse = !Impl_->Context_->ProcessMouseButtonUp(button, 0) || consumed.Mouse;
|
|
}
|
|
if (hasScreenUi && std::abs(input.GetMouseWheelDelta()) > 0.001F) consumed.Mouse = !Impl_->Context_->ProcessMouseWheel(input.GetMouseWheelDelta(), 0) || consumed.Mouse;
|
|
for (const MetaCoreKeyEvent& event : input.GetKeyEvents()) {
|
|
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Keyboard;
|
|
if (event.Pressed && event.Key == GLFW_KEY_TAB) {
|
|
consumed.Keyboard = Impl_->NavigateFocus((event.Modifiers & GLFW_MOD_SHIFT) != 0 ? -1 : 1, keyboardContext) || consumed.Keyboard;
|
|
continue;
|
|
}
|
|
if (event.Pressed && event.Key == GLFW_KEY_ESCAPE && keyboardContext->GetFocusElement() != nullptr) {
|
|
keyboardContext->GetFocusElement()->Blur();
|
|
consumed.Keyboard = true;
|
|
continue;
|
|
}
|
|
if (event.Pressed && (event.Key == GLFW_KEY_LEFT || event.Key == GLFW_KEY_UP ||
|
|
event.Key == GLFW_KEY_RIGHT || event.Key == GLFW_KEY_DOWN)) {
|
|
Rml::Element* focus = keyboardContext->GetFocusElement();
|
|
const std::string kind = focus != nullptr
|
|
? focus->GetAttribute<Rml::String>("data-metacore-kind", "") : "";
|
|
if (kind != "input") {
|
|
const int direction = event.Key == GLFW_KEY_LEFT || event.Key == GLFW_KEY_UP ? -1 : 1;
|
|
consumed.Keyboard = Impl_->NavigateFocus(direction, keyboardContext) || consumed.Keyboard;
|
|
continue;
|
|
}
|
|
}
|
|
const Rml::Input::KeyIdentifier key = MetaCoreMapGlfwKeyToRml(event.Key);
|
|
const int modifiers = MetaCoreMapGlfwModifiersToRml(event.Modifiers);
|
|
const bool propagated = event.Pressed ? keyboardContext->ProcessKeyDown(key, modifiers) : keyboardContext->ProcessKeyUp(key, modifiers);
|
|
consumed.Keyboard = !propagated || consumed.Keyboard;
|
|
}
|
|
for (const char32_t codePoint : input.GetTextInput()) {
|
|
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Keyboard;
|
|
consumed.Keyboard = !keyboardContext->ProcessTextInput(static_cast<Rml::Character>(codePoint)) || consumed.Keyboard;
|
|
}
|
|
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Programmatic;
|
|
return consumed;
|
|
}
|
|
|
|
bool MetaCoreRuntimeUiSystem::ProcessWorldPointer(
|
|
std::uint64_t sceneObjectId,
|
|
glm::vec2 surfaceUv,
|
|
MetaCoreRuntimeUiWorldPointerType type,
|
|
std::uint32_t button,
|
|
float scrollDelta
|
|
) {
|
|
if (!Impl_->Initialized_ || Impl_->Context_ == nullptr || Impl_->Window_ == nullptr ||
|
|
sceneObjectId == 0 || surfaceUv.x < 0.0F || surfaceUv.y < 0.0F ||
|
|
surfaceUv.x > 1.0F || surfaceUv.y > 1.0F) return false;
|
|
const auto state = std::max_element(Impl_->Documents_.begin(), Impl_->Documents_.end(),
|
|
[&](const auto& lhs, const auto& rhs) {
|
|
const bool lhsMatch = lhs.Entry.RenderMode == 1U && lhs.Entry.SceneObjectId == sceneObjectId &&
|
|
lhs.Entry.Visible && lhs.Entry.InputEnabled && lhs.Document != nullptr;
|
|
const bool rhsMatch = rhs.Entry.RenderMode == 1U && rhs.Entry.SceneObjectId == sceneObjectId &&
|
|
rhs.Entry.Visible && rhs.Entry.InputEnabled && rhs.Document != nullptr;
|
|
if (lhsMatch != rhsMatch) return !lhsMatch;
|
|
return lhs.Entry.Layer < rhs.Entry.Layer;
|
|
});
|
|
if (state == Impl_->Documents_.end() || state->Entry.RenderMode != 1U ||
|
|
state->Entry.SceneObjectId != sceneObjectId || !state->Entry.Visible ||
|
|
!state->Entry.InputEnabled || state->Document == nullptr) return false;
|
|
const int width = std::max(1, state->Entry.TargetWidth);
|
|
const int height = std::max(1, state->Entry.TargetHeight);
|
|
const int x = std::clamp(static_cast<int>(surfaceUv.x * static_cast<float>(width)), 0, width - 1);
|
|
const int y = std::clamp(static_cast<int>(surfaceUv.y * static_cast<float>(height)), 0, height - 1);
|
|
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::WorldPointer;
|
|
if (state->Context == nullptr) return false;
|
|
state->Context->SetDimensions({width, height});
|
|
bool propagated = state->Context->ProcessMouseMove(x, y, 0);
|
|
if (type == MetaCoreRuntimeUiWorldPointerType::Down)
|
|
propagated = state->Context->ProcessMouseButtonDown(static_cast<int>(button), 0) && propagated;
|
|
else if (type == MetaCoreRuntimeUiWorldPointerType::Up)
|
|
propagated = state->Context->ProcessMouseButtonUp(static_cast<int>(button), 0) && propagated;
|
|
else if (type == MetaCoreRuntimeUiWorldPointerType::Scroll)
|
|
propagated = state->Context->ProcessMouseWheel(scrollDelta, 0) && propagated;
|
|
else if (type == MetaCoreRuntimeUiWorldPointerType::Cancel)
|
|
propagated = state->Context->ProcessMouseLeave() && propagated;
|
|
Impl_->CurrentInputSource_ = MetaCoreRuntimeUiInputSource::Programmatic;
|
|
return !propagated;
|
|
}
|
|
|
|
void MetaCoreRuntimeUiSystem::ApplyDataUpdates(const std::vector<MetaCoreRuntimeDataUpdate>& updates) {
|
|
if (!Impl_->Initialized_) return;
|
|
for (const MetaCoreRuntimeDataUpdate& update : updates) {
|
|
const std::string path = update.DataPointId.starts_with("runtime.") ? update.DataPointId : "runtime." + update.DataPointId;
|
|
Impl_->Values_[path] = ToUiValue(update.Value);
|
|
Impl_->Values_[update.DataPointId] = ToUiValue(update.Value);
|
|
}
|
|
Impl_->ApplyBindings();
|
|
}
|
|
|
|
bool MetaCoreRuntimeUiSystem::SetValue(const std::string& sourcePath, const MetaCoreRuntimeUiValue& value) {
|
|
if (!Impl_->Initialized_ || sourcePath.empty()) return false;
|
|
Impl_->Values_[sourcePath] = value;
|
|
Impl_->ApplyBindings(&sourcePath);
|
|
return true;
|
|
}
|
|
|
|
std::optional<MetaCoreRuntimeUiValue> MetaCoreRuntimeUiSystem::GetValue(const std::string& sourcePath) const {
|
|
const auto found = Impl_->Values_.find(sourcePath);
|
|
return found == Impl_->Values_.end() ? std::nullopt : std::optional<MetaCoreRuntimeUiValue>(found->second);
|
|
}
|
|
|
|
bool MetaCoreRuntimeUiSystem::SetListRows(const std::string& sourcePath, std::vector<MetaCoreRuntimeUiRow> rows) {
|
|
if (!Impl_->Initialized_ || sourcePath.empty()) return false;
|
|
Impl_->Lists_[sourcePath] = std::move(rows);
|
|
for (auto& state : Impl_->Documents_) {
|
|
if (state.Document == nullptr) continue;
|
|
Rml::ElementList elements;
|
|
state.Document->QuerySelectorAll(elements, "[data-metacore-bind-items]");
|
|
for (Rml::Element* element : elements) {
|
|
if (element->GetAttribute<Rml::String>("data-metacore-bind-items", "") != sourcePath) continue;
|
|
auto* select = dynamic_cast<Rml::ElementFormControlSelect*>(element);
|
|
if (select == nullptr) continue;
|
|
select->RemoveAll();
|
|
std::size_t index = 0;
|
|
for (const auto& row : Impl_->Lists_[sourcePath]) {
|
|
const auto value = row.find("value");
|
|
const auto label = row.contains("label") ? row.find("label") :
|
|
(row.contains("text") ? row.find("text") : row.end());
|
|
std::vector<std::string> keys;
|
|
keys.reserve(row.size());
|
|
for (const auto& [key, _] : row) keys.push_back(key);
|
|
std::sort(keys.begin(), keys.end());
|
|
std::ostringstream markup;
|
|
if (label != row.end()) {
|
|
markup << EscapeRmlText(FormatUiValue(label->second, {}));
|
|
} else {
|
|
bool first = true;
|
|
for (const std::string& key : keys) {
|
|
if (!first) markup << " | ";
|
|
first = false;
|
|
markup << EscapeRmlText(FormatUiValue(row.at(key), {}));
|
|
}
|
|
}
|
|
(void)select->Add(markup.str(), value == row.end() ? std::to_string(index) :
|
|
FormatUiValue(value->second, {}));
|
|
++index;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool MetaCoreRuntimeUiSystem::RegisterProvider(
|
|
std::string providerName,
|
|
ProviderReadCallback reader,
|
|
ProviderWriteCallback writer
|
|
) {
|
|
if (providerName.empty() || providerName.find('.') != std::string::npos || !reader) return false;
|
|
Impl_->Providers_.insert_or_assign(std::move(providerName), Impl::Provider{std::move(reader), std::move(writer)});
|
|
return true;
|
|
}
|
|
|
|
bool MetaCoreRuntimeUiSystem::UnregisterProvider(std::string_view providerName) {
|
|
return Impl_->Providers_.erase(std::string(providerName)) > 0;
|
|
}
|
|
|
|
void MetaCoreRuntimeUiSystem::RefreshProviders() {
|
|
if (Impl_->Initialized_) Impl_->RefreshProviders();
|
|
}
|
|
|
|
bool MetaCoreRuntimeUiSystem::RequestFocus(std::uint64_t instanceId, const std::string& elementId) {
|
|
for (auto& state : Impl_->Documents_) {
|
|
if (state.Entry.InstanceId != instanceId || state.Document == nullptr) continue;
|
|
if (Rml::Element* element = state.Document->GetElementById(elementId); element != nullptr) {
|
|
element->Focus();
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool MetaCoreRuntimeUiSystem::CreateInstance(const MetaCoreRuntimeUiDocumentEntry& entry) {
|
|
if (!Impl_->Initialized_ || entry.InstanceId == 0 ||
|
|
std::any_of(Impl_->Documents_.begin(), Impl_->Documents_.end(), [&](const auto& value) {
|
|
return value.Entry.InstanceId == entry.InstanceId;
|
|
})) return false;
|
|
return Impl_->LoadEntry(entry);
|
|
}
|
|
|
|
bool MetaCoreRuntimeUiSystem::DestroyInstance(std::uint64_t instanceId) {
|
|
const auto found = std::find_if(Impl_->Documents_.begin(), Impl_->Documents_.end(), [&](const auto& value) {
|
|
return value.Entry.InstanceId == instanceId;
|
|
});
|
|
if (found == Impl_->Documents_.end()) return false;
|
|
const bool ownedFocus = found->Context != nullptr && found->Context->GetFocusElement() != nullptr &&
|
|
found->Context->GetFocusElement()->GetOwnerDocument() == found->Document;
|
|
if (ownedFocus) found->Context->GetFocusElement()->Blur();
|
|
if (found->Document != nullptr) found->Document->Close();
|
|
if (!found->ContextName.empty()) (void)Rml::RemoveContext(found->ContextName);
|
|
Impl_->Documents_.erase(found);
|
|
if (ownedFocus) (void)Impl_->NavigateFocus(1);
|
|
return true;
|
|
}
|
|
|
|
bool MetaCoreRuntimeUiSystem::SetDocumentVisible(const std::string& documentId, bool visible) {
|
|
for (Impl::DocumentState& state : Impl_->Documents_) {
|
|
if (state.Entry.DocumentId != documentId || state.Document == nullptr) continue;
|
|
const bool ownedFocus = state.Context != nullptr && state.Context->GetFocusElement() != nullptr &&
|
|
state.Context->GetFocusElement()->GetOwnerDocument() == state.Document;
|
|
if (visible) state.Document->Show();
|
|
else {
|
|
if (ownedFocus) state.Context->GetFocusElement()->Blur();
|
|
state.Document->Hide();
|
|
}
|
|
state.Entry.Visible = visible;
|
|
if (!visible && ownedFocus) (void)Impl_->NavigateFocus(1);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void MetaCoreRuntimeUiSystem::SetLocale(std::string locale) {
|
|
if (locale.empty()) return;
|
|
Impl_->Locale_ = std::move(locale);
|
|
for (auto& state : Impl_->Documents_) {
|
|
if (state.Document == nullptr) continue;
|
|
state.Document->SetAttribute("data-metacore-locale", Impl_->Locale_);
|
|
Impl_->ApplyLocalization(state);
|
|
}
|
|
}
|
|
|
|
void MetaCoreRuntimeUiSystem::SetTheme(std::string theme) {
|
|
Impl_->Theme_ = std::move(theme);
|
|
for (auto& state : Impl_->Documents_) if (state.Document != nullptr) state.Document->SetAttribute("data-metacore-theme", Impl_->Theme_);
|
|
}
|
|
|
|
const std::string& MetaCoreRuntimeUiSystem::GetLocale() const { return Impl_->Locale_; }
|
|
const std::string& MetaCoreRuntimeUiSystem::GetTheme() const { return Impl_->Theme_; }
|
|
|
|
std::vector<MetaCoreRuntimeUiEvent> MetaCoreRuntimeUiSystem::ConsumeEvents() { return std::exchange(Impl_->Events_, {}); }
|
|
void MetaCoreRuntimeUiSystem::SetEventCallback(EventCallback callback) { Impl_->Callback_ = std::move(callback); }
|
|
bool MetaCoreRuntimeUiSystem::IsInitialized() const { return Impl_->Initialized_; }
|
|
std::size_t MetaCoreRuntimeUiSystem::GetLoadedDocumentCount() const { return Impl_->Documents_.size(); }
|
|
const std::vector<std::string>& MetaCoreRuntimeUiSystem::GetErrors() const { return Impl_->Errors_; }
|
|
std::vector<MetaCoreUiDiagnostic> MetaCoreRuntimeUiSystem::GetDiagnostics() const { return Impl_->Diagnostics_; }
|
|
|
|
} // namespace MetaCore
|