280 lines
9.6 KiB
C++
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 += "&"; break;
|
|
case '<': escaped += "<"; break;
|
|
case '>': escaped += ">"; break;
|
|
case '"': escaped += """; break;
|
|
case '\'': escaped += "'"; 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
|