#include "MetaCoreScene/MetaCoreUiRmlCompiler.h" #include #include #include #include #include #include #include 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(c); if (std::isalnum(uc) != 0) { sanitized.push_back(static_cast(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(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(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& nodesById, const std::unordered_map& classById, std::unordered_set& activeStack, int depth ) { if (activeStack.contains(node.Id)) { return; } activeStack.insert(node.Id); const std::string indent(static_cast(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 << "\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 nodesById; std::unordered_map 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 << "\n"; rml << " \n"; rml << " \n"; rml << " \n"; rml << " \n"; rml << "
\n"; std::unordered_set 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 << "
\n"; rml << " \n"; 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