#include #include #include #include #include #include #include #include #include #include #include #include #include namespace XCEngine { namespace UI { namespace Runtime { namespace { using XCEngine::Math::Color; using XCEngine::Resources::CompileUIDocument; using XCEngine::Resources::GetUIDocumentDefaultRootTag; using XCEngine::Resources::UIDocumentAttribute; using XCEngine::Resources::UIDocumentCompileRequest; using XCEngine::Resources::UIDocumentCompileResult; using XCEngine::Resources::UIDocumentKind; using XCEngine::Resources::UIDocumentNode; namespace Layout = XCEngine::UI::Layout; constexpr float kDefaultFontSize = 16.0f; constexpr float kSmallFontSize = 13.0f; constexpr float kApproximateGlyphWidth = 0.56f; struct RuntimeLayoutNode { const UIDocumentNode* source = nullptr; std::vector children = {}; UISize desiredSize = {}; UIRect rect = {}; }; UIColor ToUIColor(const Color& color) { return UIColor(color.r, color.g, color.b, color.a); } std::string ToStdString(const Containers::String& value) { return std::string(value.CStr()); } bool IsUtf8ContinuationByte(unsigned char value) { return (value & 0xC0u) == 0x80u; } std::size_t CountUtf8Codepoints(const std::string& text) { std::size_t count = 0u; for (unsigned char ch : text) { if (!IsUtf8ContinuationByte(ch)) { ++count; } } return count; } float MeasureTextWidth(const std::string& text, float fontSize) { return fontSize * kApproximateGlyphWidth * static_cast(CountUtf8Codepoints(text)); } float MeasureTextHeight(float fontSize) { return fontSize + 6.0f; } const UIDocumentAttribute* FindAttribute(const UIDocumentNode& node, const char* name) { for (const UIDocumentAttribute& attribute : node.attributes) { if (attribute.name == name) { return &attribute; } } return nullptr; } std::string GetAttribute( const UIDocumentNode& node, const char* name, const std::string& fallback = {}) { const UIDocumentAttribute* attribute = FindAttribute(node, name); return attribute != nullptr ? ToStdString(attribute->value) : fallback; } bool TryParseFloat(const std::string& text, float& outValue) { if (text.empty()) { return false; } char* end = nullptr; const float parsed = std::strtof(text.c_str(), &end); if (end == text.c_str()) { return false; } while (*end != '\0') { if (!std::isspace(static_cast(*end))) { return false; } ++end; } outValue = parsed; return true; } float ParseFloatAttribute( const UIDocumentNode& node, const char* name, float fallback) { float value = fallback; return TryParseFloat(GetAttribute(node, name), value) ? value : fallback; } Layout::UILayoutLength ParseLengthAttribute( const UIDocumentNode& node, const char* name) { const std::string value = GetAttribute(node, name); if (value == "stretch" || value == "fill") { return Layout::UILayoutLength::Stretch(); } float pixels = 0.0f; return TryParseFloat(value, pixels) ? Layout::UILayoutLength::Pixels(pixels) : Layout::UILayoutLength::Auto(); } Layout::UILayoutThickness ParsePadding( const UIDocumentNode& node, float fallback) { return Layout::UILayoutThickness::Uniform(ParseFloatAttribute(node, "padding", fallback)); } std::string ResolveNodeText(const UIDocumentNode& node) { const std::string text = GetAttribute(node, "text"); if (!text.empty()) { return text; } const std::string title = GetAttribute(node, "title"); if (!title.empty()) { return title; } return ToStdString(node.tagName); } bool IsHorizontalTag(const std::string& tagName) { return tagName == "Row"; } bool IsContainerTag(const UIDocumentNode& node) { if (node.children.Size() > 0u) { return true; } const std::string tagName = ToStdString(node.tagName); return tagName == "View" || tagName == "Column" || tagName == "Row" || tagName == "Card" || tagName == "Button"; } Color ResolveBackgroundColor(const UIDocumentNode& node) { const std::string tone = GetAttribute(node, "tone"); const std::string tagName = ToStdString(node.tagName); if (tagName == "View") { return Color(0.08f, 0.09f, 0.11f, 1.0f); } if (tone == "accent") { return Color(0.19f, 0.31f, 0.52f, 1.0f); } if (tone == "accent-alt") { return Color(0.24f, 0.26f, 0.39f, 1.0f); } if (tagName == "Button") { return Color(0.20f, 0.23f, 0.29f, 1.0f); } return Color(0.13f, 0.15f, 0.18f, 1.0f); } Color ResolveBorderColor(const UIDocumentNode& node) { const std::string tone = GetAttribute(node, "tone"); if (tone == "accent") { return Color(0.31f, 0.56f, 0.90f, 1.0f); } if (tone == "accent-alt") { return Color(0.47f, 0.51f, 0.76f, 1.0f); } return Color(0.25f, 0.28f, 0.33f, 1.0f); } RuntimeLayoutNode BuildLayoutTree(const UIDocumentNode& source) { RuntimeLayoutNode node = {}; node.source = &source; node.children.reserve(source.children.Size()); for (const UIDocumentNode& child : source.children) { node.children.push_back(BuildLayoutTree(child)); } return node; } UISize MeasureNode(RuntimeLayoutNode& node) { const UIDocumentNode& source = *node.source; const std::string tagName = ToStdString(source.tagName); if (tagName == "Text") { const std::string text = ResolveNodeText(source); node.desiredSize = UISize( MeasureTextWidth(text, kDefaultFontSize), MeasureTextHeight(kDefaultFontSize)); return node.desiredSize; } if (!IsContainerTag(source)) { node.desiredSize = UISize( (std::max)(160.0f, MeasureTextWidth(ResolveNodeText(source), kDefaultFontSize) + 24.0f), 44.0f); return node.desiredSize; } Layout::UIStackLayoutOptions options = {}; options.axis = IsHorizontalTag(tagName) ? Layout::UILayoutAxis::Horizontal : Layout::UILayoutAxis::Vertical; options.spacing = ParseFloatAttribute( source, "gap", options.axis == Layout::UILayoutAxis::Horizontal ? 10.0f : 8.0f); options.padding = ParsePadding( source, tagName == "View" ? 16.0f : 12.0f); std::vector items = {}; items.reserve(node.children.size()); for (RuntimeLayoutNode& child : node.children) { Layout::UILayoutItem item = {}; item.desiredContentSize = MeasureNode(child); item.width = ParseLengthAttribute(*child.source, "width"); item.height = ParseLengthAttribute(*child.source, "height"); items.push_back(item); } const auto measured = Layout::MeasureStackLayout(options, items); node.desiredSize = measured.desiredSize; const std::string title = GetAttribute(source, "title"); const std::string subtitle = GetAttribute(source, "subtitle"); float headerHeight = 0.0f; if (!title.empty()) { headerHeight += MeasureTextHeight(kDefaultFontSize); } if (!subtitle.empty()) { if (headerHeight > 0.0f) { headerHeight += 4.0f; } headerHeight += MeasureTextHeight(kSmallFontSize); } node.desiredSize.width = (std::max)( node.desiredSize.width, MeasureTextWidth(ResolveNodeText(source), kDefaultFontSize) + options.padding.Horizontal()); node.desiredSize.height += headerHeight; float explicitWidth = 0.0f; if (TryParseFloat(GetAttribute(source, "width"), explicitWidth)) { node.desiredSize.width = explicitWidth; } float explicitHeight = 0.0f; if (TryParseFloat(GetAttribute(source, "height"), explicitHeight)) { node.desiredSize.height = explicitHeight; } return node.desiredSize; } void ArrangeNode(RuntimeLayoutNode& node, const UIRect& rect) { node.rect = rect; const UIDocumentNode& source = *node.source; if (!IsContainerTag(source)) { return; } const std::string tagName = ToStdString(source.tagName); Layout::UIStackLayoutOptions options = {}; options.axis = IsHorizontalTag(tagName) ? Layout::UILayoutAxis::Horizontal : Layout::UILayoutAxis::Vertical; options.spacing = ParseFloatAttribute( source, "gap", options.axis == Layout::UILayoutAxis::Horizontal ? 10.0f : 8.0f); options.padding = ParsePadding( source, tagName == "View" ? 16.0f : 12.0f); float headerHeight = 0.0f; if (!GetAttribute(source, "title").empty()) { headerHeight += MeasureTextHeight(kDefaultFontSize); } if (!GetAttribute(source, "subtitle").empty()) { if (headerHeight > 0.0f) { headerHeight += 4.0f; } headerHeight += MeasureTextHeight(kSmallFontSize); } UIRect contentRect = rect; contentRect.y += headerHeight; contentRect.height = (std::max)(0.0f, rect.height - headerHeight); std::vector items = {}; items.reserve(node.children.size()); for (RuntimeLayoutNode& child : node.children) { Layout::UILayoutItem item = {}; item.desiredContentSize = child.desiredSize; item.width = ParseLengthAttribute(*child.source, "width"); item.height = ParseLengthAttribute(*child.source, "height"); items.push_back(item); } const auto arranged = Layout::ArrangeStackLayout(options, items, contentRect); for (std::size_t index = 0; index < node.children.size(); ++index) { ArrangeNode(node.children[index], arranged.children[index].arrangedRect); } } void EmitNode( const RuntimeLayoutNode& node, UIDrawList& drawList, UIScreenFrameStats& stats) { const UIDocumentNode& source = *node.source; const std::string tagName = ToStdString(source.tagName); ++stats.nodeCount; if (tagName == "View" || tagName == "Card" || tagName == "Button") { drawList.AddFilledRect(node.rect, ToUIColor(ResolveBackgroundColor(source)), 10.0f); ++stats.filledRectCommandCount; if (tagName != "View") { drawList.AddRectOutline(node.rect, ToUIColor(ResolveBorderColor(source)), 1.0f, 10.0f); } } const std::string title = GetAttribute(source, "title"); const std::string subtitle = GetAttribute(source, "subtitle"); float textY = node.rect.y + 12.0f; if (!title.empty()) { drawList.AddText( UIPoint(node.rect.x + 12.0f, textY), title, ToUIColor(Color(0.94f, 0.95f, 0.97f, 1.0f)), kDefaultFontSize); ++stats.textCommandCount; textY += MeasureTextHeight(kDefaultFontSize) + 2.0f; } if (!subtitle.empty()) { drawList.AddText( UIPoint(node.rect.x + 12.0f, textY), subtitle, ToUIColor(Color(0.67f, 0.70f, 0.76f, 1.0f)), kSmallFontSize); ++stats.textCommandCount; } if (tagName == "Text") { drawList.AddText( UIPoint(node.rect.x, node.rect.y), ResolveNodeText(source), ToUIColor(Color(0.92f, 0.94f, 0.97f, 1.0f)), kDefaultFontSize); ++stats.textCommandCount; } if (tagName == "Button" && title.empty() && subtitle.empty()) { drawList.AddText( UIPoint(node.rect.x + 12.0f, node.rect.y + 12.0f), ResolveNodeText(source), ToUIColor(Color(0.95f, 0.97f, 1.0f, 1.0f)), kDefaultFontSize); ++stats.textCommandCount; } for (const RuntimeLayoutNode& child : node.children) { EmitNode(child, drawList, stats); } } std::string ResolveDisplayName( const UIScreenAsset& asset, const UIDocumentCompileResult& viewResult) { if (!asset.screenId.empty()) { return asset.screenId; } if (!viewResult.document.displayName.Empty()) { return ToStdString(viewResult.document.displayName); } if (!viewResult.document.sourcePath.Empty()) { return std::filesystem::path(viewResult.document.sourcePath.CStr()).stem().string(); } return "UIScreen"; } void AppendDependencies( const Containers::Array& dependencies, std::unordered_set& seenDependencies, std::vector& outDependencies) { for (const Containers::String& dependency : dependencies) { const std::string value = ToStdString(dependency); if (value.empty()) { continue; } if (seenDependencies.insert(value).second) { outDependencies.push_back(value); } } } } // namespace UIScreenLoadResult UIDocumentScreenHost::LoadScreen(const UIScreenAsset& asset) { UIScreenLoadResult result = {}; if (!asset.IsValid()) { result.errorMessage = "UIScreenAsset is invalid."; return result; } UIDocumentCompileRequest viewRequest = {}; viewRequest.kind = UIDocumentKind::View; viewRequest.path = Containers::String(asset.documentPath.c_str()); viewRequest.expectedRootTag = GetUIDocumentDefaultRootTag(viewRequest.kind); UIDocumentCompileResult viewResult = {}; if (!CompileUIDocument(viewRequest, viewResult) || !viewResult.succeeded || !viewResult.document.valid) { result.errorMessage = viewResult.errorMessage.Empty() ? "Failed to compile UI screen view document." : ToStdString(viewResult.errorMessage); return result; } result.succeeded = true; result.document.sourcePath = asset.documentPath; result.document.displayName = ResolveDisplayName(asset, viewResult); result.document.viewDocument = viewResult.document; std::unordered_set seenDependencies = {}; AppendDependencies( viewResult.document.dependencies, seenDependencies, result.document.dependencies); if (!asset.themePath.empty()) { UIDocumentCompileRequest themeRequest = {}; themeRequest.kind = UIDocumentKind::Theme; themeRequest.path = Containers::String(asset.themePath.c_str()); themeRequest.expectedRootTag = GetUIDocumentDefaultRootTag(themeRequest.kind); UIDocumentCompileResult themeResult = {}; if (!CompileUIDocument(themeRequest, themeResult) || !themeResult.succeeded || !themeResult.document.valid) { result = {}; result.errorMessage = themeResult.errorMessage.Empty() ? "Failed to compile UI screen theme document." : ToStdString(themeResult.errorMessage); return result; } result.document.themeDocument = themeResult.document; result.document.hasThemeDocument = true; if (seenDependencies.insert(asset.themePath).second) { result.document.dependencies.push_back(asset.themePath); } AppendDependencies( themeResult.document.dependencies, seenDependencies, result.document.dependencies); } return result; } UIScreenFrameResult UIDocumentScreenHost::BuildFrame( const UIScreenDocument& document, const UIScreenFrameInput& input) { UIScreenFrameResult result = {}; if (!document.viewDocument.valid || document.viewDocument.rootNode.tagName.Empty()) { result.errorMessage = "UIScreenDocument does not contain a compiled view document."; return result; } RuntimeLayoutNode root = BuildLayoutTree(document.viewDocument.rootNode); MeasureNode(root); UIRect viewportRect = input.viewportRect; if (viewportRect.width <= 0.0f) { viewportRect.width = (std::max)(640.0f, root.desiredSize.width); } if (viewportRect.height <= 0.0f) { viewportRect.height = (std::max)(360.0f, root.desiredSize.height); } ArrangeNode(root, viewportRect); UIDrawList& drawList = result.drawData.EmplaceDrawList(document.displayName); EmitNode(root, drawList, result.stats); result.stats.documentLoaded = true; result.stats.drawListCount = result.drawData.GetDrawListCount(); result.stats.commandCount = result.drawData.GetTotalCommandCount(); result.stats.inputEventCount = input.events.size(); result.stats.presentedFrameIndex = input.frameIndex; return result; } } // namespace Runtime } // namespace UI } // namespace XCEngine