MetaCore/Source/MetaCoreScene/Private/MetaCoreUiRmlCompiler.cpp

280 lines
9.6 KiB
C++

#include "MetaCoreScene/MetaCoreUiRmlCompiler.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <sstream>
#include <string>
#include <unordered_map>
#include <unordered_set>
namespace MetaCore {
namespace {
[[nodiscard]] std::string MetaCoreEscapeRml(std::string_view value) {
std::string escaped;
escaped.reserve(value.size());
for (char c : value) {
switch (c) {
case '&': escaped += "&amp;"; break;
case '<': escaped += "&lt;"; break;
case '>': escaped += "&gt;"; break;
case '"': escaped += "&quot;"; break;
case '\'': escaped += "&apos;"; break;
default: escaped.push_back(c); break;
}
}
return escaped;
}
[[nodiscard]] std::string MetaCoreSanitizeIdentifier(std::string_view value, std::string_view fallback) {
std::string sanitized;
sanitized.reserve(value.size());
for (char c : value) {
const auto uc = static_cast<unsigned char>(c);
if (std::isalnum(uc) != 0) {
sanitized.push_back(static_cast<char>(std::tolower(uc)));
} else if (c == '_' || c == '-' || c == '.') {
sanitized.push_back(c == '.' ? '-' : c);
} else {
sanitized.push_back('-');
}
}
while (!sanitized.empty() && sanitized.front() == '-') {
sanitized.erase(sanitized.begin());
}
while (!sanitized.empty() && sanitized.back() == '-') {
sanitized.pop_back();
}
if (sanitized.empty()) {
sanitized = std::string(fallback);
}
if (std::isdigit(static_cast<unsigned char>(sanitized.front())) != 0) {
sanitized.insert(sanitized.begin(), 'n');
}
return sanitized;
}
[[nodiscard]] std::string MetaCoreUiNodeTypeClass(MetaCoreUiNodeType type) {
switch (type) {
case MetaCoreUiNodeType::Text: return "mcui-text";
case MetaCoreUiNodeType::Image: return "mcui-image";
case MetaCoreUiNodeType::Button: return "mcui-button";
case MetaCoreUiNodeType::Panel:
default: return "mcui-panel";
}
}
[[nodiscard]] std::string MetaCoreUiTagName(MetaCoreUiNodeType type) {
switch (type) {
case MetaCoreUiNodeType::Text: return "p";
case MetaCoreUiNodeType::Button: return "button";
case MetaCoreUiNodeType::Image:
case MetaCoreUiNodeType::Panel:
default: return "div";
}
}
[[nodiscard]] int MetaCoreColorChannel(float value) {
const float clamped = std::clamp(value, 0.0F, 1.0F);
return static_cast<int>(std::round(clamped * 255.0F));
}
[[nodiscard]] std::string MetaCoreRcssColor(const glm::vec3& color) {
std::ostringstream stream;
stream << "rgb("
<< MetaCoreColorChannel(color.r) << ", "
<< MetaCoreColorChannel(color.g) << ", "
<< MetaCoreColorChannel(color.b) << ")";
return stream.str();
}
[[nodiscard]] std::string MetaCoreRcssTextAlign(MetaCoreUiHorizontalAlignment alignment) {
switch (alignment) {
case MetaCoreUiHorizontalAlignment::Center: return "center";
case MetaCoreUiHorizontalAlignment::Right: return "right";
case MetaCoreUiHorizontalAlignment::Stretch:
case MetaCoreUiHorizontalAlignment::Left:
default: return "left";
}
}
[[nodiscard]] bool MetaCoreIsStretch(float minAnchor, float maxAnchor, float size) {
return std::abs(minAnchor) <= 0.0001F &&
std::abs(maxAnchor - 1.0F) <= 0.0001F &&
std::abs(size) <= 0.0001F;
}
void MetaCoreAppendNodeRcss(
std::ostringstream& output,
const MetaCoreUiNodeDocument& node,
std::string_view className
) {
output << "." << className << " {\n";
output << " position: absolute;\n";
output << " box-sizing: border-box;\n";
if (!node.Visible) {
output << " display: none;\n";
}
const auto& rect = node.RectTransform;
if (MetaCoreIsStretch(rect.AnchorMin.x, rect.AnchorMax.x, rect.Size.x)) {
output << " left: 0px;\n";
output << " right: 0px;\n";
} else {
output << " left: " << rect.Position.x << "px;\n";
output << " width: " << std::max(rect.Size.x, 0.0F) << "px;\n";
}
if (MetaCoreIsStretch(rect.AnchorMin.y, rect.AnchorMax.y, rect.Size.y)) {
output << " top: 0px;\n";
output << " bottom: 0px;\n";
} else {
output << " top: " << rect.Position.y << "px;\n";
output << " height: " << std::max(rect.Size.y, 0.0F) << "px;\n";
}
output << " padding: " << std::max(node.Style.Padding.y, 0.0F)
<< "px " << std::max(node.Style.Padding.x, 0.0F) << "px;\n";
output << " background-color: " << MetaCoreRcssColor(node.Style.BackgroundColor) << ";\n";
output << " color: " << MetaCoreRcssColor(node.Style.TextColor) << ";\n";
output << " font-size: " << std::max(node.Style.FontSize, 1.0F) << "px;\n";
output << " text-align: " << MetaCoreRcssTextAlign(node.Style.HorizontalAlignment) << ";\n";
if (node.Type == MetaCoreUiNodeType::Button) {
output << " border-width: 0px;\n";
}
if (node.Type == MetaCoreUiNodeType::Image) {
output << " background-color: " << MetaCoreRcssColor(node.Style.TintColor) << ";\n";
}
output << "}\n\n";
}
void MetaCoreAppendRmlNode(
std::ostringstream& output,
const MetaCoreUiNodeDocument& node,
const std::unordered_map<std::string, const MetaCoreUiNodeDocument*>& nodesById,
const std::unordered_map<std::string, std::string>& classById,
std::unordered_set<std::string>& activeStack,
int depth
) {
if (activeStack.contains(node.Id)) {
return;
}
activeStack.insert(node.Id);
const std::string indent(static_cast<std::size_t>(depth) * 2U, ' ');
const std::string tagName = MetaCoreUiTagName(node.Type);
const auto classIterator = classById.find(node.Id);
const std::string className = classIterator != classById.end() ? classIterator->second : "mcui-node";
const std::string nodeId = MetaCoreBuildRuntimeUiNodeElementId(node.Id);
output << indent << "<" << tagName
<< " id=\"" << MetaCoreEscapeRml(nodeId) << "\""
<< " class=\"mcui-node " << MetaCoreUiNodeTypeClass(node.Type) << " " << className << "\""
<< " data-metacore-id=\"" << MetaCoreEscapeRml(node.Id) << "\"";
if (node.Type == MetaCoreUiNodeType::Image && node.Style.ImageAssetGuid.IsValid()) {
output << " data-image-guid=\"" << MetaCoreEscapeRml(node.Style.ImageAssetGuid.ToString()) << "\"";
}
output << ">";
if (node.Type == MetaCoreUiNodeType::Text || node.Type == MetaCoreUiNodeType::Button) {
output << MetaCoreEscapeRml(node.Text);
}
if (!node.Children.empty()) {
output << "\n";
for (const std::string& childId : node.Children) {
const auto childIterator = nodesById.find(childId);
if (childIterator != nodesById.end()) {
MetaCoreAppendRmlNode(output, *childIterator->second, nodesById, classById, activeStack, depth + 1);
}
}
output << indent;
}
output << "</" << tagName << ">\n";
activeStack.erase(node.Id);
}
} // namespace
std::string MetaCoreBuildRuntimeUiNodeElementId(std::string_view nodeId) {
return MetaCoreSanitizeIdentifier(nodeId, "node");
}
MetaCoreCompiledRmlUiDocument MetaCoreCompileUiDocumentToRml(
const MetaCoreUiDocument& document,
std::string_view stylesheetHref
) {
std::unordered_map<std::string, const MetaCoreUiNodeDocument*> nodesById;
std::unordered_map<std::string, std::string> classById;
nodesById.reserve(document.Nodes.size());
classById.reserve(document.Nodes.size());
for (std::size_t index = 0; index < document.Nodes.size(); ++index) {
const MetaCoreUiNodeDocument& node = document.Nodes[index];
if (!node.Id.empty()) {
nodesById[node.Id] = &node;
classById[node.Id] = "mcui-node-" + std::to_string(index);
}
}
MetaCoreCompiledRmlUiDocument compiled;
compiled.StylesheetHref = std::string(stylesheetHref);
std::ostringstream rml;
rml << "<rml>\n";
rml << " <head>\n";
rml << " <link type=\"text/rcss\" href=\"" << MetaCoreEscapeRml(stylesheetHref) << "\" />\n";
rml << " </head>\n";
rml << " <body>\n";
rml << " <div id=\"metacore-ui-root\" class=\"mcui-document\" data-metacore-document=\""
<< MetaCoreEscapeRml(document.Name) << "\">\n";
std::unordered_set<std::string> activeStack;
for (const std::string& rootId : document.RootNodeIds) {
const auto rootIterator = nodesById.find(rootId);
if (rootIterator != nodesById.end()) {
MetaCoreAppendRmlNode(rml, *rootIterator->second, nodesById, classById, activeStack, 3);
}
}
rml << " </div>\n";
rml << " </body>\n";
rml << "</rml>\n";
compiled.Rml = rml.str();
std::ostringstream rcss;
rcss << "body {\n";
rcss << " margin: 0px;\n";
rcss << " padding: 0px;\n";
rcss << "}\n\n";
rcss << ".mcui-document {\n";
rcss << " position: relative;\n";
rcss << " width: " << std::max(document.ReferenceWidth, 1) << "px;\n";
rcss << " height: " << std::max(document.ReferenceHeight, 1) << "px;\n";
rcss << "}\n\n";
rcss << ".mcui-node {\n";
rcss << " overflow: hidden;\n";
rcss << "}\n\n";
for (const MetaCoreUiNodeDocument& node : document.Nodes) {
if (node.Id.empty()) {
continue;
}
const auto classIterator = classById.find(node.Id);
if (classIterator != classById.end()) {
MetaCoreAppendNodeRcss(rcss, node, classIterator->second);
}
}
compiled.Rcss = rcss.str();
return compiled;
}
} // namespace MetaCore